mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-29 19:21:33 -07:00
fix(stats): track deleting sessions by ID set, not count
- Replace deletingSessionId/deletingCount with a Set<number> so concurrent deletes disable the correct rows - Pass session IDs (not count) to onStart callback for accurate per-row disable state - Skip cover image fetch when sessions list is empty - Nudge mining-frame toast down to avoid overlap with other OSD elements
This commit is contained in:
@@ -148,7 +148,7 @@ body:focus-visible,
|
||||
|
||||
.mining-image-toast {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
top: 80px;
|
||||
right: 16px;
|
||||
padding: 6px;
|
||||
border-radius: 10px;
|
||||
|
||||
@@ -71,8 +71,9 @@ test('buildBucketDeleteHandler deletes every session in the bucket when confirm
|
||||
assert.equal(onErrorCalled, false);
|
||||
});
|
||||
|
||||
test('buildBucketDeleteHandler signals onStart after confirm, before deleting', async () => {
|
||||
test('buildBucketDeleteHandler signals deleted session IDs after confirm, before deleting', async () => {
|
||||
const events: string[] = [];
|
||||
let startedIds: number[] | null = null;
|
||||
|
||||
const bucket = makeBucket([
|
||||
makeSession({ sessionId: 11 }),
|
||||
@@ -91,8 +92,9 @@ test('buildBucketDeleteHandler signals onStart after confirm, before deleting',
|
||||
events.push('confirm');
|
||||
return true;
|
||||
},
|
||||
onStart: (count) => {
|
||||
events.push(`start:${count}`);
|
||||
onStart: (ids) => {
|
||||
startedIds = ids;
|
||||
events.push('start');
|
||||
},
|
||||
onSuccess: () => {
|
||||
events.push('success');
|
||||
@@ -104,7 +106,8 @@ test('buildBucketDeleteHandler signals onStart after confirm, before deleting',
|
||||
|
||||
await handler();
|
||||
|
||||
assert.deepEqual(events, ['confirm', 'start:3', 'delete', 'success']);
|
||||
assert.deepEqual(events, ['confirm', 'start', 'delete', 'success']);
|
||||
assert.deepEqual(startedIds, [11, 22, 33]);
|
||||
});
|
||||
|
||||
test('buildBucketDeleteHandler does not call onStart when confirm returns false', async () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface BucketDeleteDeps {
|
||||
apiClient: { deleteSessions: (ids: number[]) => Promise<void> };
|
||||
confirm: (title: string, count: number) => boolean | Promise<boolean>;
|
||||
/** Called once confirmation passes, just before the delete request begins. */
|
||||
onStart?: (count: number) => void;
|
||||
onStart?: (sessionIds: number[]) => void;
|
||||
onSuccess: (deletedIds: number[]) => void;
|
||||
onError: (message: string) => void;
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export function buildBucketDeleteHandler(deps: BucketDeleteDeps): () => Promise<
|
||||
const ids = bucket.sessions.map((s) => s.sessionId);
|
||||
try {
|
||||
if (!(await confirm(title, ids.length))) return;
|
||||
onStart?.(ids.length);
|
||||
onStart?.(ids);
|
||||
await client.deleteSessions(ids);
|
||||
onSuccess(ids);
|
||||
} catch (err) {
|
||||
@@ -74,9 +74,8 @@ export function SessionsTab({
|
||||
const [search, setSearch] = useState('');
|
||||
const [visibleSessions, setVisibleSessions] = useState<SessionSummary[]>([]);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
const [deletingSessionId, setDeletingSessionId] = useState<number | null>(null);
|
||||
const [deletingSessionIds, setDeletingSessionIds] = useState<Set<number>>(() => new Set());
|
||||
const [deletingBucketKey, setDeletingBucketKey] = useState<string | null>(null);
|
||||
const [deletingCount, setDeletingCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleSessions(sessions);
|
||||
@@ -135,8 +134,11 @@ export function SessionsTab({
|
||||
if (!confirmed) return;
|
||||
|
||||
setDeleteError(null);
|
||||
setDeletingSessionId(session.sessionId);
|
||||
setDeletingCount(1);
|
||||
setDeletingSessionIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(session.sessionId);
|
||||
return next;
|
||||
});
|
||||
try {
|
||||
await apiClient.deleteSession(session.sessionId);
|
||||
setVisibleSessions((prev) => prev.filter((item) => item.sessionId !== session.sessionId));
|
||||
@@ -144,8 +146,11 @@ export function SessionsTab({
|
||||
} catch (err) {
|
||||
setDeleteError(err instanceof Error ? err.message : 'Failed to delete session.');
|
||||
} finally {
|
||||
setDeletingSessionId(null);
|
||||
setDeletingCount(0);
|
||||
setDeletingSessionIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(session.sessionId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,7 +161,12 @@ export function SessionsTab({
|
||||
bucket,
|
||||
apiClient,
|
||||
confirm: confirmBucketDelete,
|
||||
onStart: (count) => setDeletingCount(count),
|
||||
onStart: (ids) =>
|
||||
setDeletingSessionIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
for (const id of ids) next.add(id);
|
||||
return next;
|
||||
}),
|
||||
onSuccess: (ids) => {
|
||||
const deleted = new Set(ids);
|
||||
setVisibleSessions((prev) => prev.filter((s) => !deleted.has(s.sessionId)));
|
||||
@@ -174,7 +184,11 @@ export function SessionsTab({
|
||||
await handler();
|
||||
} finally {
|
||||
setDeletingBucketKey(null);
|
||||
setDeletingCount(0);
|
||||
setDeletingSessionIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
for (const session of bucket.sessions) next.delete(session.sessionId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -219,7 +233,7 @@ export function SessionsTab({
|
||||
setExpandedId(expandedId === s.sessionId ? null : s.sessionId)
|
||||
}
|
||||
onDelete={() => void handleDeleteSession(s)}
|
||||
deleteDisabled={deletingSessionId === s.sessionId}
|
||||
deleteDisabled={deletingSessionIds.has(s.sessionId)}
|
||||
onNavigateToMediaDetail={onNavigateToMediaDetail}
|
||||
/>
|
||||
{expandedId === s.sessionId && (
|
||||
@@ -288,7 +302,7 @@ export function SessionsTab({
|
||||
setExpandedId(expandedId === s.sessionId ? null : s.sessionId)
|
||||
}
|
||||
onDelete={() => void handleDeleteSession(s)}
|
||||
deleteDisabled={deletingSessionId === s.sessionId}
|
||||
deleteDisabled={deletingSessionIds.has(s.sessionId)}
|
||||
onNavigateToMediaDetail={onNavigateToMediaDetail}
|
||||
/>
|
||||
{expandedId === s.sessionId && (
|
||||
@@ -315,7 +329,7 @@ export function SessionsTab({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DeleteProgressToast count={deletingCount} />
|
||||
<DeleteProgressToast count={deletingSessionIds.size} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,13 @@ export function useCoverImages(sessions: SessionSummary[]): CoverImageMap {
|
||||
}, getCoverRetryDelayMs(attempt));
|
||||
}
|
||||
|
||||
setImages({});
|
||||
if (requests.animeIds.length === 0 && requests.videoIds.length === 0) {
|
||||
setImages({});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
|
||||
void load(requests.animeIds, requests.videoIds, 0);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ export function appendCoverRetryToken(src: string, retryToken = 0): string {
|
||||
|
||||
const normalizedToken = String(Math.trunc(retryToken));
|
||||
try {
|
||||
// Dummy base lets URL parse relative API paths; it is never returned as a real host.
|
||||
const url = new URL(src, 'http://subminer.local');
|
||||
url.searchParams.set(COVER_RETRY_PARAM, normalizedToken);
|
||||
if (src.startsWith('/')) {
|
||||
|
||||
Reference in New Issue
Block a user