mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-30 07:21:32 -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 {
|
.mining-image-toast {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 16px;
|
top: 80px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|||||||
@@ -71,8 +71,9 @@ test('buildBucketDeleteHandler deletes every session in the bucket when confirm
|
|||||||
assert.equal(onErrorCalled, false);
|
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[] = [];
|
const events: string[] = [];
|
||||||
|
let startedIds: number[] | null = null;
|
||||||
|
|
||||||
const bucket = makeBucket([
|
const bucket = makeBucket([
|
||||||
makeSession({ sessionId: 11 }),
|
makeSession({ sessionId: 11 }),
|
||||||
@@ -91,8 +92,9 @@ test('buildBucketDeleteHandler signals onStart after confirm, before deleting',
|
|||||||
events.push('confirm');
|
events.push('confirm');
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
onStart: (count) => {
|
onStart: (ids) => {
|
||||||
events.push(`start:${count}`);
|
startedIds = ids;
|
||||||
|
events.push('start');
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
events.push('success');
|
events.push('success');
|
||||||
@@ -104,7 +106,8 @@ test('buildBucketDeleteHandler signals onStart after confirm, before deleting',
|
|||||||
|
|
||||||
await handler();
|
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 () => {
|
test('buildBucketDeleteHandler does not call onStart when confirm returns false', async () => {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export interface BucketDeleteDeps {
|
|||||||
apiClient: { deleteSessions: (ids: number[]) => Promise<void> };
|
apiClient: { deleteSessions: (ids: number[]) => Promise<void> };
|
||||||
confirm: (title: string, count: number) => boolean | Promise<boolean>;
|
confirm: (title: string, count: number) => boolean | Promise<boolean>;
|
||||||
/** Called once confirmation passes, just before the delete request begins. */
|
/** Called once confirmation passes, just before the delete request begins. */
|
||||||
onStart?: (count: number) => void;
|
onStart?: (sessionIds: number[]) => void;
|
||||||
onSuccess: (deletedIds: number[]) => void;
|
onSuccess: (deletedIds: number[]) => void;
|
||||||
onError: (message: string) => void;
|
onError: (message: string) => void;
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ export function buildBucketDeleteHandler(deps: BucketDeleteDeps): () => Promise<
|
|||||||
const ids = bucket.sessions.map((s) => s.sessionId);
|
const ids = bucket.sessions.map((s) => s.sessionId);
|
||||||
try {
|
try {
|
||||||
if (!(await confirm(title, ids.length))) return;
|
if (!(await confirm(title, ids.length))) return;
|
||||||
onStart?.(ids.length);
|
onStart?.(ids);
|
||||||
await client.deleteSessions(ids);
|
await client.deleteSessions(ids);
|
||||||
onSuccess(ids);
|
onSuccess(ids);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -74,9 +74,8 @@ export function SessionsTab({
|
|||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [visibleSessions, setVisibleSessions] = useState<SessionSummary[]>([]);
|
const [visibleSessions, setVisibleSessions] = useState<SessionSummary[]>([]);
|
||||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
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 [deletingBucketKey, setDeletingBucketKey] = useState<string | null>(null);
|
||||||
const [deletingCount, setDeletingCount] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVisibleSessions(sessions);
|
setVisibleSessions(sessions);
|
||||||
@@ -135,8 +134,11 @@ export function SessionsTab({
|
|||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
setDeleteError(null);
|
setDeleteError(null);
|
||||||
setDeletingSessionId(session.sessionId);
|
setDeletingSessionIds((prev) => {
|
||||||
setDeletingCount(1);
|
const next = new Set(prev);
|
||||||
|
next.add(session.sessionId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await apiClient.deleteSession(session.sessionId);
|
await apiClient.deleteSession(session.sessionId);
|
||||||
setVisibleSessions((prev) => prev.filter((item) => item.sessionId !== session.sessionId));
|
setVisibleSessions((prev) => prev.filter((item) => item.sessionId !== session.sessionId));
|
||||||
@@ -144,8 +146,11 @@ export function SessionsTab({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
setDeleteError(err instanceof Error ? err.message : 'Failed to delete session.');
|
setDeleteError(err instanceof Error ? err.message : 'Failed to delete session.');
|
||||||
} finally {
|
} finally {
|
||||||
setDeletingSessionId(null);
|
setDeletingSessionIds((prev) => {
|
||||||
setDeletingCount(0);
|
const next = new Set(prev);
|
||||||
|
next.delete(session.sessionId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,7 +161,12 @@ export function SessionsTab({
|
|||||||
bucket,
|
bucket,
|
||||||
apiClient,
|
apiClient,
|
||||||
confirm: confirmBucketDelete,
|
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) => {
|
onSuccess: (ids) => {
|
||||||
const deleted = new Set(ids);
|
const deleted = new Set(ids);
|
||||||
setVisibleSessions((prev) => prev.filter((s) => !deleted.has(s.sessionId)));
|
setVisibleSessions((prev) => prev.filter((s) => !deleted.has(s.sessionId)));
|
||||||
@@ -174,7 +184,11 @@ export function SessionsTab({
|
|||||||
await handler();
|
await handler();
|
||||||
} finally {
|
} finally {
|
||||||
setDeletingBucketKey(null);
|
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)
|
setExpandedId(expandedId === s.sessionId ? null : s.sessionId)
|
||||||
}
|
}
|
||||||
onDelete={() => void handleDeleteSession(s)}
|
onDelete={() => void handleDeleteSession(s)}
|
||||||
deleteDisabled={deletingSessionId === s.sessionId}
|
deleteDisabled={deletingSessionIds.has(s.sessionId)}
|
||||||
onNavigateToMediaDetail={onNavigateToMediaDetail}
|
onNavigateToMediaDetail={onNavigateToMediaDetail}
|
||||||
/>
|
/>
|
||||||
{expandedId === s.sessionId && (
|
{expandedId === s.sessionId && (
|
||||||
@@ -288,7 +302,7 @@ export function SessionsTab({
|
|||||||
setExpandedId(expandedId === s.sessionId ? null : s.sessionId)
|
setExpandedId(expandedId === s.sessionId ? null : s.sessionId)
|
||||||
}
|
}
|
||||||
onDelete={() => void handleDeleteSession(s)}
|
onDelete={() => void handleDeleteSession(s)}
|
||||||
deleteDisabled={deletingSessionId === s.sessionId}
|
deleteDisabled={deletingSessionIds.has(s.sessionId)}
|
||||||
onNavigateToMediaDetail={onNavigateToMediaDetail}
|
onNavigateToMediaDetail={onNavigateToMediaDetail}
|
||||||
/>
|
/>
|
||||||
{expandedId === s.sessionId && (
|
{expandedId === s.sessionId && (
|
||||||
@@ -315,7 +329,7 @@ export function SessionsTab({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DeleteProgressToast count={deletingCount} />
|
<DeleteProgressToast count={deletingSessionIds.size} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,13 @@ export function useCoverImages(sessions: SessionSummary[]): CoverImageMap {
|
|||||||
}, getCoverRetryDelayMs(attempt));
|
}, getCoverRetryDelayMs(attempt));
|
||||||
}
|
}
|
||||||
|
|
||||||
setImages({});
|
if (requests.animeIds.length === 0 && requests.videoIds.length === 0) {
|
||||||
|
setImages({});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
void load(requests.animeIds, requests.videoIds, 0);
|
void load(requests.animeIds, requests.videoIds, 0);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export function appendCoverRetryToken(src: string, retryToken = 0): string {
|
|||||||
|
|
||||||
const normalizedToken = String(Math.trunc(retryToken));
|
const normalizedToken = String(Math.trunc(retryToken));
|
||||||
try {
|
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');
|
const url = new URL(src, 'http://subminer.local');
|
||||||
url.searchParams.set(COVER_RETRY_PARAM, normalizedToken);
|
url.searchParams.set(COVER_RETRY_PARAM, normalizedToken);
|
||||||
if (src.startsWith('/')) {
|
if (src.startsWith('/')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user