mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-11 17:16:18 -07:00
- Episode rows gain Play/Queue actions (and matching context-menu items); queued rows show their place in line, with a queue count and Clear queue in the episode header - Queue lives in the main process (`anime-browser-queue.ts`) so it survives the browser window closing and advances on mpv's end-file even when nobody is watching; streams resolve at play time so a signed URL cannot expire while queued - Holds mpv's keep-open off while the queue waits and restores it once empty; queueing with nothing playing just plays immediately - Adds anime-browser-queue and episode-queue unit tests, IPC channels/contracts, and doc updates
452 lines
8.7 KiB
CSS
452 lines
8.7 KiB
CSS
/* ---------- detail page ---------- */
|
|
|
|
/*
|
|
* The detail view is a page, not a sidebar: it takes over the whole content
|
|
* region while the results grid waits, hidden, behind the Back button.
|
|
*/
|
|
.detail {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
background: var(--panel);
|
|
overflow-y: auto;
|
|
padding: 20px clamp(20px, 5vw, 56px) 32px;
|
|
animation: detail-in 0.28s ease both;
|
|
}
|
|
|
|
@keyframes detail-in {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.detail-body {
|
|
max-width: 1040px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.detail-back {
|
|
padding: 6px 14px;
|
|
font-size: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.detail-head {
|
|
display: flex;
|
|
gap: clamp(18px, 3vw, 36px);
|
|
margin-bottom: 28px;
|
|
}
|
|
|
|
.detail-cover {
|
|
flex: none;
|
|
align-self: flex-start;
|
|
width: clamp(140px, 18vw, 232px);
|
|
height: auto;
|
|
border-radius: 12px;
|
|
border: 1px solid var(--line);
|
|
background: var(--ctp-crust);
|
|
box-shadow: 0 18px 40px -24px var(--shadow);
|
|
}
|
|
|
|
.detail-title {
|
|
margin: 0 0 10px;
|
|
font-size: clamp(19px, 2.4vw, 27px);
|
|
font-weight: 600;
|
|
line-height: 1.25;
|
|
}
|
|
|
|
.detail-chips {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.chip {
|
|
padding: 2px 8px;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--line);
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.chip.status {
|
|
border-color: rgba(166, 218, 149, 0.4);
|
|
color: var(--ok);
|
|
}
|
|
|
|
/* Which extension answered — the one chip that is always present. */
|
|
.chip.source {
|
|
border-color: color-mix(in srgb, var(--accent) 40%, transparent);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.detail-description {
|
|
margin: 0;
|
|
font-size: 13px;
|
|
max-width: 72ch;
|
|
color: var(--muted);
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.episodes-head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 12px;
|
|
margin-bottom: 10px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.episodes-title {
|
|
margin: 0;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.18em;
|
|
text-transform: uppercase;
|
|
color: var(--faint);
|
|
}
|
|
|
|
.episodes-count {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
color: var(--faint);
|
|
}
|
|
|
|
/* How much of the series the stats history already has a watch mark for. */
|
|
.episodes-watched {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
color: var(--ok);
|
|
}
|
|
|
|
/* Everything lined up behind the episode playing now, across every anime. */
|
|
.episodes-queued {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.episodes-queue-clear {
|
|
padding: 3px 9px;
|
|
font-size: 11px;
|
|
border-radius: 7px;
|
|
}
|
|
|
|
/* Pushed to the far end so the counters stay next to the heading. */
|
|
.episodes-filter {
|
|
margin-left: auto;
|
|
width: clamp(160px, 24vw, 260px);
|
|
padding: 5px 10px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/*
|
|
* The cue rail: episodes read as subtitle cues on a timeline, because that is
|
|
* what they are about to become. The rail is the spine, the index is the cue
|
|
* number, and the title is the cue text.
|
|
*/
|
|
|
|
.cue-rail {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0 0 0 2px;
|
|
position: relative;
|
|
}
|
|
|
|
.cue-rail::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 48px;
|
|
top: 6px;
|
|
bottom: 6px;
|
|
width: 1px;
|
|
background: var(--line);
|
|
}
|
|
|
|
/*
|
|
* The row is the cue plus the two things you can do with it. The cue still
|
|
* fills the row and still plays on click; the actions sit over its trailing
|
|
* edge, so a row with nothing hovered looks exactly as it did before them.
|
|
*/
|
|
.cue-row {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.cue {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: 34px 1fr;
|
|
gap: 26px;
|
|
align-items: baseline;
|
|
width: 100%;
|
|
/* Room at the trailing edge for the actions that sit over it, kept whether or
|
|
* not they are showing, so a hover never slides them across the title. */
|
|
padding: 9px 132px 9px 0;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: none;
|
|
color: inherit;
|
|
font: inherit;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
transition: background 0.14s ease;
|
|
}
|
|
|
|
.cue::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 43px;
|
|
top: 15px;
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
background: var(--ctp-surface0);
|
|
border: 1px solid var(--line);
|
|
transition:
|
|
background 0.14s ease,
|
|
border-color 0.14s ease;
|
|
}
|
|
|
|
.cue:hover,
|
|
.cue:focus-visible {
|
|
background: var(--panel-elevated);
|
|
}
|
|
|
|
.cue:hover::after,
|
|
.cue:focus-visible::after {
|
|
background: var(--mine);
|
|
border-color: var(--mine);
|
|
}
|
|
|
|
.cue-index {
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
color: var(--faint);
|
|
text-align: right;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.cue-name {
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.cue-sub {
|
|
display: block;
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
color: var(--faint);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/*
|
|
* Watched episodes stay readable rather than being hidden: the mark says "you
|
|
* have been here", it does not remove the episode from the list.
|
|
*/
|
|
.cue[data-watched='true'] .cue-name,
|
|
.cue[data-watched='true'] .cue-index {
|
|
color: var(--faint);
|
|
}
|
|
|
|
.cue[data-watched='true']::after {
|
|
background: var(--ok);
|
|
border-color: var(--ok);
|
|
}
|
|
|
|
.cue-watched {
|
|
margin-left: 10px;
|
|
font-family: var(--mono);
|
|
font-size: 10px;
|
|
letter-spacing: 0.06em;
|
|
color: var(--ok);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/*
|
|
* Hidden until the row is hovered or something inside it has focus, so the
|
|
* rail stays a list of episodes rather than a grid of buttons. Queued rows keep
|
|
* theirs on: that button is the only way back out of the queue.
|
|
*/
|
|
.cue-actions {
|
|
position: absolute;
|
|
right: 8px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
display: flex;
|
|
gap: 6px;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.14s ease;
|
|
}
|
|
|
|
.cue-row:hover .cue-actions,
|
|
.cue-row:focus-within .cue-actions,
|
|
.cue-row[data-queued='true'] .cue-actions {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.cue-action {
|
|
padding: 4px 10px;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--line);
|
|
background: var(--panel-elevated);
|
|
color: var(--faint);
|
|
font: inherit;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.02em;
|
|
cursor: pointer;
|
|
backdrop-filter: blur(6px);
|
|
transition:
|
|
color 0.14s ease,
|
|
border-color 0.14s ease;
|
|
}
|
|
|
|
.cue-action:hover {
|
|
color: var(--text);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.cue-action[data-active='true'] {
|
|
color: var(--accent);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
/* The badge says where in line it is; the button beside it says it can leave. */
|
|
.cue-queued {
|
|
margin-left: 10px;
|
|
font-family: var(--mono);
|
|
font-size: 10px;
|
|
letter-spacing: 0.06em;
|
|
color: var(--accent);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.cue[data-queued='true']::after {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.cue[data-state='loading'] {
|
|
background: var(--panel-elevated);
|
|
}
|
|
|
|
.cue[data-state='loading']::after {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
animation: pulse 1.1s ease-in-out infinite;
|
|
}
|
|
|
|
.cue[data-state='playing']::after {
|
|
background: var(--mine);
|
|
border-color: var(--mine);
|
|
}
|
|
|
|
/* ---------- right-click menu ---------- */
|
|
|
|
.context-menu {
|
|
position: fixed;
|
|
z-index: 40;
|
|
min-width: 210px;
|
|
padding: 4px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 10px;
|
|
/* Opaque on purpose: the panel colours are translucent, and episode rows
|
|
showing through a menu makes both unreadable. */
|
|
background: var(--ctp-mantle);
|
|
box-shadow: 0 20px 44px -22px var(--shadow);
|
|
animation: detail-in 0.12s ease both;
|
|
}
|
|
|
|
.context-menu-item {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 7px 10px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
background: none;
|
|
color: inherit;
|
|
font: inherit;
|
|
font-size: 12px;
|
|
text-align: left;
|
|
white-space: nowrap;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.context-menu-item[data-separated='true'] {
|
|
margin-top: 4px;
|
|
border-top: 1px solid var(--line);
|
|
padding-top: 9px;
|
|
border-radius: 0 0 6px 6px;
|
|
}
|
|
|
|
.context-menu-item:hover:not(:disabled),
|
|
.context-menu-item:focus-visible:not(:disabled) {
|
|
background: var(--ctp-surface0);
|
|
outline: none;
|
|
}
|
|
|
|
.context-menu-item:disabled {
|
|
color: var(--faint);
|
|
cursor: default;
|
|
}
|
|
|
|
/* ---------- status bar ---------- */
|
|
|
|
.statusbar {
|
|
flex: none;
|
|
padding: 7px 20px;
|
|
border-top: 1px solid var(--line);
|
|
background: var(--ctp-mantle);
|
|
font-size: 12px;
|
|
color: var(--faint);
|
|
min-height: 30px;
|
|
}
|
|
|
|
.statusbar[data-tone='error'] {
|
|
color: var(--danger);
|
|
}
|
|
|
|
.statusbar[data-tone='ok'] {
|
|
color: var(--ok);
|
|
}
|
|
|
|
/* ---------- scrollbars ---------- */
|
|
|
|
::-webkit-scrollbar {
|
|
width: 10px;
|
|
height: 10px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: var(--ctp-surface0);
|
|
border-radius: 6px;
|
|
border: 2px solid var(--bg);
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: var(--faint);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
}
|
|
}
|