ff35f0d95f
The original unpack logs window was a flat, monolithic scroll of every
unrar invocation glued together as one continuous textfield. That is
fine for a sanity check but hostile to actually finding a failed
extraction in a session with 30+ games: empty lines from unrar bloated
the view, there was no way to focus on a single game, no filtering, and
no way to narrow in on the entries that actually failed.
This rewrites the viewer to be a proper debugging surface while keeping
the backend untouched -- it still consumes the existing
`get_unpack_logs` command and `unpack-logs-updated` event.
User-visible changes:
* Empty / whitespace-only lines are stripped from stdout and stderr
before rendering, so unrar's padding no longer drowns out real output.
* Two-pane layout: a sidebar lists every captured invocation (badge,
archive basename, finish time); the right pane shows the selected
entry's metadata, stdout and stderr.
* "Errors only" checkbox filters the sidebar to entries whose `success`
flag is false (sidecar exit != 0 or one of the pre-spawn failure
paths). This is the primary affordance for "find the unpack that
broke".
* Regex input filters lines (not entries) -- both per-log when viewing
one, and across the list: entries that contribute zero matching lines
are hidden, and the remaining ones display a per-entry match counter
next to the badge. Regex is case-insensitive; a bad pattern reddens
the input and renders the parser error inline rather than silently
dropping all matches.
* Prev / Next buttons plus arrow keys (and j/k) step through the
filtered list one entry at a time, with the active row auto-scrolled
into view. Selection is tracked by the entry's index in the full log
ring so it survives filter toggles and live appends.
Code organization:
The component, its types, helpers (`basename`, `nonEmptyLines`,
`formatLogTime`, `isUnpackLogsView`) and its CSS are moved out of
`App.tsx` / `App.css` into a dedicated `UnpackLogsWindow.tsx` +
`UnpackLogsWindow.css` pair. The viewer has no shared state with the
main window and lives behind its own `?view=unpack-logs` route, so
keeping ~200 lines of debug-UI plumbing inside `App.tsx` was just
noise. `App.tsx` now imports `UnpackLogsWindow` and `isUnpackLogsView`
and otherwise looks the same as before.
Intentionally out of scope:
* No backend changes. The Rust side already records everything needed;
this is purely a presentation improvement.
* No "view all logs concatenated" mode. The flat view was what we just
replaced -- if it is ever wanted back, it can be added as a third
pane mode.
* Regex is applied to displayed lines only, not to archive paths or
meta. Filtering by archive name is easy enough via the basename in
the sidebar; adding a second filter for it now would be premature.
* Logs are still process-local and capped at `MAX_UNPACK_LOGS` (100)
in the Rust state -- unchanged from b35755f.
Test plan:
* `tsc --noEmit` and `vite build` are clean.
* Manual: trigger several successful and failed unpacks (rename one
archive between handshake and extraction to force a canonicalize
failure), open Unpack Logs, and verify:
- empty lines are gone from stdout/stderr,
- sidebar lists every entry with the right OK/FAIL badge,
- "Errors only" hides the OK rows,
- typing a regex narrows lines in the open entry, hides entries
with no matches, and shows the per-entry match counts,
- an invalid regex (e.g. `[`) reddens the field and shows the
parser error rather than crashing,
- arrow keys / j / k step through the filtered list and the
active row scrolls into view,
- new entries arriving via `unpack-logs-updated` while the window
is open keep the current selection rather than jumping.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
352 lines
6.4 KiB
CSS
352 lines
6.4 KiB
CSS
body {
|
|
background-color: #000313;
|
|
font-family: Arial, sans-serif;
|
|
color: #D5DBFE;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.fixed-header {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background-color: #000313;
|
|
z-index: 1000;
|
|
padding-top: 20px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
h1.align-center {
|
|
margin: 0;
|
|
padding: 10px 0;
|
|
}
|
|
|
|
.main-header {
|
|
width: 100%;
|
|
}
|
|
|
|
.grid-container {
|
|
margin-top: 160px; /* Adjust based on your header height */
|
|
padding: 20px;
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: linear-gradient(to bottom, black, #000938);
|
|
color: white;
|
|
border: 1px solid #444;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
transition: background 0.3s;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
|
cursor: pointer;
|
|
/* max-width: 280px; */
|
|
}
|
|
|
|
.item:hover {
|
|
background: linear-gradient(to bottom, black, #3849AB);
|
|
}
|
|
|
|
.item img {
|
|
width: 280px; /* Fixed width */
|
|
height: 200px; /* Fixed height */
|
|
object-fit: cover;
|
|
display: block; /* Removes any unwanted spacing */
|
|
margin: 0 auto; /* Centers the image if container is wider */
|
|
}
|
|
|
|
.item-name {
|
|
text-align: center;
|
|
margin: 10px 0;
|
|
font-weight: bold;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.description {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 0 10px 10px 10px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.badges {
|
|
display: flex;
|
|
min-height: 24px;
|
|
gap: 6px;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 0 10px 8px;
|
|
}
|
|
|
|
.badge {
|
|
border: 1px solid #4866b9;
|
|
border-radius: 4px;
|
|
color: #D5DBFE;
|
|
font-size: 12px;
|
|
line-height: 1;
|
|
padding: 5px 7px;
|
|
}
|
|
|
|
.badge.local-only {
|
|
border-color: #8b6f2a;
|
|
color: #f1d58a;
|
|
}
|
|
|
|
.desc-text {
|
|
text-align: left;
|
|
}
|
|
|
|
.size-text {
|
|
text-align: right;
|
|
}
|
|
|
|
.align-center {
|
|
text-align: center;
|
|
}
|
|
|
|
.play-button {
|
|
margin-top: auto;
|
|
margin-bottom: 2px;
|
|
padding: 15px 30px;
|
|
background: linear-gradient(45deg, #09305a, #37529c);
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
text-decoration: none;
|
|
border-radius: 25px;
|
|
border: 1px solid transparent;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 8px 15px rgba(0, 191, 255, 0.2);
|
|
}
|
|
|
|
.play-button:hover {
|
|
background: linear-gradient(45deg, #09305a, #4866b9);
|
|
box-shadow: 0 20px 25px rgba(0, 191, 255, 0.6);
|
|
border: 1px solid rgba(0, 191, 255, 0.6);
|
|
animation: flicker 0.2s infinite alternate;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.play-button.unavailable {
|
|
background: linear-gradient(45deg, #330000, #550000);
|
|
color: #ffb4b4;
|
|
border: 1px solid #550000;
|
|
box-shadow: none;
|
|
cursor: default;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.play-button.unavailable:hover {
|
|
background: linear-gradient(45deg, #330000, #550000);
|
|
box-shadow: none;
|
|
border: 1px solid #550000;
|
|
animation: none;
|
|
transform: none;
|
|
}
|
|
|
|
.uninstall-button {
|
|
align-self: center;
|
|
width: 34px;
|
|
height: 34px;
|
|
margin: 6px 0 0;
|
|
border-radius: 50%;
|
|
border: 1px solid #6c2942;
|
|
background: #2a0714;
|
|
color: #ffb4c8;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.uninstall-button:hover {
|
|
border-color: #ff6d9d;
|
|
background: #4d1025;
|
|
}
|
|
|
|
@keyframes flicker {
|
|
0% { opacity: 1; }
|
|
50% { opacity: 0.8; }
|
|
100% { opacity: 1; }
|
|
}
|
|
|
|
.search-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.no-directory-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 40px 20px;
|
|
gap: 20px;
|
|
}
|
|
|
|
.no-directory-message {
|
|
color: #8892b0;
|
|
font-size: 18px;
|
|
text-align: center;
|
|
}
|
|
|
|
.no-directory-button {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.search-input {
|
|
width: 100%;
|
|
max-width: 400px;
|
|
padding: 10px 15px;
|
|
font-size: 16px;
|
|
color: #D5DBFE;
|
|
background: #000938;
|
|
border: 1px solid #444;
|
|
border-radius: 25px;
|
|
outline: none;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.search-input:focus {
|
|
border-color: #4866b9;
|
|
box-shadow: 0 0 10px rgba(0, 191, 255, 0.2);
|
|
}
|
|
|
|
.search-input::placeholder {
|
|
color: #8892b0;
|
|
}
|
|
|
|
.search-settings-wrapper {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto 1fr;
|
|
align-items: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.settings-container {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.settings-button {
|
|
padding: 8px 16px;
|
|
background: linear-gradient(45deg, #09305a, #37529c);
|
|
color: #D5DBFE;
|
|
border: 1px solid transparent;
|
|
border-radius: 20px;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 8px rgba(0, 191, 255, 0.2);
|
|
}
|
|
|
|
.settings-button:hover {
|
|
background: linear-gradient(45deg, #09305a, #4866b9);
|
|
box-shadow: 0 8px 12px rgba(0, 191, 255, 0.4);
|
|
border: 1px solid rgba(0, 191, 255, 0.6);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.settings-text {
|
|
color: #8892b0;
|
|
font-size: 14px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 300px;
|
|
}
|
|
|
|
.no-games-message {
|
|
grid-column: 1 / -1;
|
|
text-align: center;
|
|
color: #8892b0;
|
|
font-size: 18px;
|
|
padding: 40px 20px;
|
|
margin: 20px 0;
|
|
background: linear-gradient(to bottom, rgba(0, 9, 56, 0.3), rgba(0, 9, 56, 0.1));
|
|
border: 1px solid #444;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.item-info {
|
|
min-height: 18px;
|
|
margin: 8px 10px 16px;
|
|
font-size: 0.85em;
|
|
color: #8892b0;
|
|
text-align: center;
|
|
}
|
|
|
|
.item-info.error {
|
|
color: #ff6666;
|
|
}
|
|
|
|
.filter-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.filter-button {
|
|
padding: 8px 16px;
|
|
background: linear-gradient(45deg, #09305a, #37529c);
|
|
color: #D5DBFE;
|
|
border: 1px solid transparent;
|
|
border-radius: 20px;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 8px rgba(0, 191, 255, 0.2);
|
|
}
|
|
|
|
.filter-button:hover {
|
|
background: linear-gradient(45deg, #09305a, #4866b9);
|
|
box-shadow: 0 8px 12px rgba(0, 191, 255, 0.4);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.filter-button.active {
|
|
background: linear-gradient(45deg, #09305a, #4866b9);
|
|
border: 1px solid rgba(0, 191, 255, 0.6);
|
|
box-shadow: 0 8px 12px rgba(0, 191, 255, 0.4);
|
|
}
|
|
|
|
.item-info {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
min-height: 18px;
|
|
margin: 8px 10px 16px;
|
|
font-size: 0.85em;
|
|
color: #8892b0;
|
|
text-align: left;
|
|
}
|
|
|
|
.status-left {
|
|
flex: 1;
|
|
text-align: left;
|
|
}
|
|
|
|
.status-right {
|
|
text-align: right;
|
|
}
|
|
|
|
.peer-count {
|
|
font-weight: bold;
|
|
color: #4866b9;
|
|
}
|
|
|
|
.top-left-peer-count {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
z-index: 1001;
|
|
}
|