mirror of
https://github.com/markuryy/shark.git
synced 2025-12-12 11:41:02 +00:00
204 lines
4.9 KiB
TypeScript
204 lines
4.9 KiB
TypeScript
/**
|
|
* Status notification service
|
|
* Provides a reactive status bar that shows download progress, notifications, and app state
|
|
*/
|
|
|
|
import { writable, derived, get } from 'svelte/store';
|
|
import { downloadQueue } from './downloadQueue';
|
|
|
|
export type StatusLevel = 'idle' | 'info' | 'success' | 'warning' | 'error';
|
|
|
|
export interface StatusMessage {
|
|
id: number;
|
|
message: string;
|
|
level: StatusLevel;
|
|
timestamp: number;
|
|
expiresAt: number;
|
|
}
|
|
|
|
interface StatusState {
|
|
messages: StatusMessage[];
|
|
nextId: number;
|
|
}
|
|
|
|
// Default expiration times (in ms)
|
|
const EXPIRATION_TIMES: Record<StatusLevel, number> = {
|
|
idle: 0,
|
|
info: 3000,
|
|
success: 4000,
|
|
warning: 6000,
|
|
error: 8000
|
|
};
|
|
|
|
// Priority order (higher = more important)
|
|
const PRIORITY: Record<StatusLevel, number> = {
|
|
error: 4,
|
|
warning: 3,
|
|
success: 2,
|
|
info: 1,
|
|
idle: 0
|
|
};
|
|
|
|
// Internal store
|
|
const statusState = writable<StatusState>({
|
|
messages: [],
|
|
nextId: 0
|
|
});
|
|
|
|
/**
|
|
* Push a new status message
|
|
*/
|
|
export function pushStatus(
|
|
message: string,
|
|
level: StatusLevel = 'info',
|
|
duration?: number
|
|
): number {
|
|
const state = get(statusState);
|
|
const id = state.nextId;
|
|
const timestamp = Date.now();
|
|
const expiresAt = timestamp + (duration ?? EXPIRATION_TIMES[level]);
|
|
|
|
const newMessage: StatusMessage = {
|
|
id,
|
|
message,
|
|
level,
|
|
timestamp,
|
|
expiresAt
|
|
};
|
|
|
|
statusState.update(s => ({
|
|
messages: [...s.messages, newMessage],
|
|
nextId: s.nextId + 1
|
|
}));
|
|
|
|
// Auto-remove after expiration
|
|
if (duration !== 0 && (duration ?? EXPIRATION_TIMES[level]) > 0) {
|
|
setTimeout(() => {
|
|
removeStatus(id);
|
|
}, duration ?? EXPIRATION_TIMES[level]);
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Remove a status message by ID
|
|
*/
|
|
export function removeStatus(id: number): void {
|
|
statusState.update(s => ({
|
|
...s,
|
|
messages: s.messages.filter(m => m.id !== id)
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Clear all messages of a specific level (or all if no level specified)
|
|
*/
|
|
export function clearStatus(level?: StatusLevel): void {
|
|
statusState.update(s => ({
|
|
...s,
|
|
messages: level ? s.messages.filter(m => m.level !== level) : []
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Convenience methods
|
|
*/
|
|
export function setInfo(message: string, duration?: number): number {
|
|
return pushStatus(message, 'info', duration);
|
|
}
|
|
|
|
export function setSuccess(message: string, duration?: number): number {
|
|
return pushStatus(message, 'success', duration);
|
|
}
|
|
|
|
export function setWarning(message: string, duration?: number): number {
|
|
return pushStatus(message, 'warning', duration);
|
|
}
|
|
|
|
export function setError(message: string, duration?: number): number {
|
|
return pushStatus(message, 'error', duration);
|
|
}
|
|
|
|
/**
|
|
* Derive download status from queue state
|
|
*/
|
|
function deriveDownloadStatus(queueState: any): string | null {
|
|
const activeDownloads = queueState.queueOrder.filter((id: string) => {
|
|
const item = queueState.queue[id];
|
|
return item && (item.status === 'queued' || item.status === 'downloading');
|
|
});
|
|
|
|
const currentItem = queueState.currentJob
|
|
? queueState.queue[queueState.currentJob]
|
|
: null;
|
|
|
|
if (currentItem && currentItem.status === 'downloading') {
|
|
if (currentItem.type === 'track') {
|
|
return `Downloading: ${currentItem.title}`;
|
|
} else {
|
|
const total = currentItem.totalTracks;
|
|
const completed = currentItem.completedTracks;
|
|
const failed = currentItem.failedTracks;
|
|
const remaining = total - completed - failed;
|
|
|
|
if (remaining > 0) {
|
|
return `Downloading ${currentItem.title} (${completed}/${total} tracks)`;
|
|
} else {
|
|
return `Finishing ${currentItem.title}...`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (activeDownloads.length > 0) {
|
|
return `${activeDownloads.length} download${activeDownloads.length !== 1 ? 's' : ''} queued`;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Main status store - shows the highest priority active message or derived download status
|
|
*/
|
|
export const status = derived(
|
|
[statusState, downloadQueue],
|
|
([$statusState, $downloadQueue]) => {
|
|
const now = Date.now();
|
|
|
|
// Filter out expired messages
|
|
const activeMessages = $statusState.messages.filter(m => m.expiresAt > now);
|
|
|
|
// Get highest priority message
|
|
if (activeMessages.length > 0) {
|
|
const sorted = activeMessages.sort((a, b) => {
|
|
// Sort by priority first
|
|
const priorityDiff = PRIORITY[b.level] - PRIORITY[a.level];
|
|
if (priorityDiff !== 0) return priorityDiff;
|
|
|
|
// Then by timestamp (newer first)
|
|
return b.timestamp - a.timestamp;
|
|
});
|
|
|
|
return sorted[0].message;
|
|
}
|
|
|
|
// No explicit messages - check download queue
|
|
const downloadStatus = deriveDownloadStatus($downloadQueue);
|
|
if (downloadStatus) {
|
|
return downloadStatus;
|
|
}
|
|
|
|
// Default to idle
|
|
return 'Idle';
|
|
}
|
|
);
|
|
|
|
// Clean up expired messages periodically
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
statusState.update(s => ({
|
|
...s,
|
|
messages: s.messages.filter(m => m.expiresAt > now)
|
|
}));
|
|
}, 1000);
|