refactor: drop pinia stores
This commit is contained in:
parent
fc5cf84dd2
commit
210712cee2
35 changed files with 871 additions and 1421 deletions
|
|
@ -538,7 +538,7 @@ const props = defineProps<{
|
||||||
const toast = useNotification();
|
const toast = useNotification();
|
||||||
const showImport = useStorage('showImport', false);
|
const showImport = useStorage('showImport', false);
|
||||||
const box = useConfirm();
|
const box = useConfirm();
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
|
|
||||||
const form = reactive<Condition>(normalizeCondition(props.item));
|
const form = reactive<Condition>(normalizeCondition(props.item));
|
||||||
const importString = ref('');
|
const importString = ref('');
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
const toast = useNotification();
|
const toast = useNotification();
|
||||||
const box = useConfirm();
|
const box = useConfirm();
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
|
|
||||||
const fieldTypes = ['string', 'text', 'bool'] as const;
|
const fieldTypes = ['string', 'text', 'bool'] as const;
|
||||||
const fieldTypeItems = [...fieldTypes];
|
const fieldTypeItems = [...fieldTypes];
|
||||||
|
|
|
||||||
|
|
@ -520,7 +520,7 @@ const emitter = defineEmits<{
|
||||||
(e: 'getInfo', url: string, preset: string | undefined, cli: string | undefined): void;
|
(e: 'getInfo', url: string, preset: string | undefined, cli: string | undefined): void;
|
||||||
(e: 'clear_form'): void;
|
(e: 'clear_form'): void;
|
||||||
}>();
|
}>();
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const toast = useNotification();
|
const toast = useNotification();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const { findPreset, hasPreset, selectItems, getPresetDefault } = usePresetOptions();
|
const { findPreset, hasPreset, selectItems, getPresetDefault } = usePresetOptions();
|
||||||
|
|
|
||||||
|
|
@ -138,9 +138,10 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { useNotificationCenter } from '~/composables/useNotificationCenter';
|
||||||
import type { notificationType } from '~/composables/useNotification';
|
import type { notificationType } from '~/composables/useNotification';
|
||||||
|
|
||||||
const store = useNotificationStore();
|
const store = useNotificationCenter();
|
||||||
|
|
||||||
const copiedId = ref<string | null>(null);
|
const copiedId = ref<string | null>(null);
|
||||||
const expandedId = ref<string | null>(null);
|
const expandedId = ref<string | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,7 @@ const props = defineProps<{
|
||||||
presets?: Preset[];
|
presets?: Preset[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const toast = useNotification();
|
const toast = useNotification();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const { presets, findPreset, selectItems } = usePresetOptions(() => props.presets);
|
const { presets, findPreset, selectItems } = usePresetOptions(() => props.presets);
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watch, onMounted, onBeforeUnmount, ref, computed } from 'vue';
|
import { watch, onMounted, onBeforeUnmount, ref, computed } from 'vue';
|
||||||
import { useStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
import { useConfigStore } from '~/stores/ConfigStore';
|
|
||||||
import { useNotification } from '~/composables/useNotification';
|
import { useNotification } from '~/composables/useNotification';
|
||||||
import type { notificationTarget, toastPosition } from '~/composables/useNotification';
|
import type { notificationTarget, toastPosition } from '~/composables/useNotification';
|
||||||
|
|
||||||
|
|
@ -294,7 +293,7 @@ const props = withDefaults(
|
||||||
|
|
||||||
const emitter = defineEmits<{ (e: 'close' | 'reload_bg'): void }>();
|
const emitter = defineEmits<{ (e: 'close' | 'reload_bg'): void }>();
|
||||||
|
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const bg_enable = useStorage<boolean>('random_bg', true);
|
const bg_enable = useStorage<boolean>('random_bg', true);
|
||||||
|
|
|
||||||
|
|
@ -521,7 +521,7 @@ const emitter = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const toast = useNotification();
|
const toast = useNotification();
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const { findPreset, getPresetDefault, selectItems } = usePresetOptions();
|
const { findPreset, getPresetDefault, selectItems } = usePresetOptions();
|
||||||
const showImport = useStorage('showTaskImport', false);
|
const showImport = useStorage('showTaskImport', false);
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { request } from '~/utils';
|
import { request } from '~/utils';
|
||||||
import { useConfigStore } from '~/stores/ConfigStore';
|
|
||||||
import type { TaskInspectRequest, TaskInspectResponse } from '~/types/task_inspect';
|
import type { TaskInspectRequest, TaskInspectResponse } from '~/types/task_inspect';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -138,7 +137,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
const { selectItems } = usePresetOptions();
|
const { selectItems } = usePresetOptions();
|
||||||
|
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const url = ref(props.url ?? '');
|
const url = ref(props.url ?? '');
|
||||||
const preset = ref(props.preset || config.app.default_preset || '');
|
const preset = ref(props.preset || config.app.default_preset || '');
|
||||||
const handler = ref(props.handler ?? '');
|
const handler = ref(props.handler ?? '');
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@ import { useKeyboardShortcuts } from '~/composables/useKeyboardShortcuts';
|
||||||
import type { StoreItem } from '~/types/store';
|
import type { StoreItem } from '~/types/store';
|
||||||
import type { file_info, video_source_element, video_track_element } from '~/types/video';
|
import type { file_info, video_source_element, video_track_element } from '~/types/video';
|
||||||
|
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
|
|
||||||
const props = defineProps<{ item: StoreItem }>();
|
const props = defineProps<{ item: StoreItem }>();
|
||||||
const emitter = defineEmits<{
|
const emitter = defineEmits<{
|
||||||
|
|
|
||||||
403
ui/app/composables/useAppSocket.ts
Normal file
403
ui/app/composables/useAppSocket.ts
Normal file
|
|
@ -0,0 +1,403 @@
|
||||||
|
import { proxyRefs, readonly, ref } from 'vue';
|
||||||
|
import { useYtpConfig } from '~/composables/useYtpConfig';
|
||||||
|
import { useQueueState } from '~/composables/useQueueState';
|
||||||
|
import type { StoreItem } from '~/types/store';
|
||||||
|
import type {
|
||||||
|
ConfigUpdatePayload,
|
||||||
|
WebSocketClientEmits,
|
||||||
|
WebSocketEnvelope,
|
||||||
|
WSEP as WSEP,
|
||||||
|
} from '~/types/sockets';
|
||||||
|
|
||||||
|
export type connectionStatus = 'connected' | 'disconnected' | 'connecting';
|
||||||
|
|
||||||
|
type SocketHandler = (...args: unknown[]) => void;
|
||||||
|
type HandlerRegistry = Map<SocketHandler, SocketHandler>;
|
||||||
|
type KnownEvent = keyof WSEP;
|
||||||
|
|
||||||
|
const getRuntimeConfig = () => useRuntimeConfig();
|
||||||
|
const getConfig = () => useYtpConfig();
|
||||||
|
const getQueueState = () => useQueueState();
|
||||||
|
const getToast = () => useNotification();
|
||||||
|
|
||||||
|
const socket = ref<WebSocket | null>(null);
|
||||||
|
const isConnected = ref<boolean>(false);
|
||||||
|
const connectionStatus = ref<connectionStatus>('disconnected');
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
const error_count = ref<number>(0);
|
||||||
|
const wasHidden = ref<boolean>(false);
|
||||||
|
const reconnectTimeout = ref<NodeJS.Timeout | null>(null);
|
||||||
|
const manualDisconnect = ref<boolean>(false);
|
||||||
|
const reconnectAttempts = ref<number>(0);
|
||||||
|
|
||||||
|
const handlers = new Map<string, HandlerRegistry>();
|
||||||
|
|
||||||
|
const emit = <K extends keyof WebSocketClientEmits>(
|
||||||
|
event: K,
|
||||||
|
data: WebSocketClientEmits[K],
|
||||||
|
): void => {
|
||||||
|
if (!socket.value || WebSocket.OPEN !== socket.value.readyState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.value.send(JSON.stringify({ event, data }));
|
||||||
|
};
|
||||||
|
|
||||||
|
function on<K extends KnownEvent>(event: K | K[], callback: (payload: WSEP[K]) => void): void;
|
||||||
|
function on<K extends KnownEvent>(
|
||||||
|
event: K | K[],
|
||||||
|
callback: (event: K, payload: WSEP[K]) => void,
|
||||||
|
withEvent: true,
|
||||||
|
): void;
|
||||||
|
function on(event: string | string[], callback: SocketHandler, withEvent?: boolean): void;
|
||||||
|
function on(event: string | string[], callback: SocketHandler, withEvent: boolean = false): void {
|
||||||
|
const events = Array.isArray(event) ? event : [event];
|
||||||
|
events.forEach((eventName) => {
|
||||||
|
if (!handlers.has(eventName)) {
|
||||||
|
handlers.set(eventName, new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
const registry = handlers.get(eventName) as HandlerRegistry;
|
||||||
|
const handler =
|
||||||
|
true === withEvent
|
||||||
|
? (payload: unknown) => callback(eventName, payload)
|
||||||
|
: (payload: unknown) => callback(payload);
|
||||||
|
|
||||||
|
registry.set(callback, handler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function off<K extends KnownEvent>(event: K | K[], callback?: (payload: WSEP[K]) => void): void;
|
||||||
|
function off(event: string | string[], callback?: SocketHandler): void;
|
||||||
|
function off(event: string | string[], callback?: SocketHandler): void {
|
||||||
|
const events = Array.isArray(event) ? event : [event];
|
||||||
|
events.forEach((eventName) => {
|
||||||
|
const registry = handlers.get(eventName);
|
||||||
|
if (!registry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!callback) {
|
||||||
|
registry.clear();
|
||||||
|
handlers.delete(eventName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.delete(callback);
|
||||||
|
if (0 === registry.size) {
|
||||||
|
handlers.delete(eventName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSessionId = (): string | null => null;
|
||||||
|
|
||||||
|
const dispatch = (eventName: string, payload: unknown): void => {
|
||||||
|
const registry = handlers.get(eventName);
|
||||||
|
if (!registry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.forEach((handler) => handler(payload));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
wasHidden.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true === wasHidden.value && false === isConnected.value) {
|
||||||
|
if (null !== reconnectTimeout.value) {
|
||||||
|
clearTimeout(reconnectTimeout.value);
|
||||||
|
reconnectTimeout.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectTimeout.value = setTimeout(() => {
|
||||||
|
if (false === isConnected.value) {
|
||||||
|
console.debug('[SocketStore] Page visible after background, reconnecting...');
|
||||||
|
reconnect();
|
||||||
|
}
|
||||||
|
reconnectTimeout.value = null;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
wasHidden.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupVisibilityListener = () => {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanupVisibilityListener = () => {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
}
|
||||||
|
if (null !== reconnectTimeout.value) {
|
||||||
|
clearTimeout(reconnectTimeout.value);
|
||||||
|
reconnectTimeout.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleReconnect = () => {
|
||||||
|
if (true === manualDisconnect.value || true === isConnected.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reconnectAttempts.value >= 50) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== reconnectTimeout.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectTimeout.value = setTimeout(() => {
|
||||||
|
reconnectAttempts.value += 1;
|
||||||
|
reconnectTimeout.value = null;
|
||||||
|
connect();
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reconnect = () => {
|
||||||
|
if (true === isConnected.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
connect();
|
||||||
|
connectionStatus.value = 'connecting';
|
||||||
|
};
|
||||||
|
|
||||||
|
const disconnect = () => {
|
||||||
|
manualDisconnect.value = true;
|
||||||
|
if (null === socket.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.value.close();
|
||||||
|
socket.value = null;
|
||||||
|
isConnected.value = false;
|
||||||
|
connectionStatus.value = 'disconnected';
|
||||||
|
cleanupVisibilityListener();
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildWsUrl = (): string => {
|
||||||
|
const runtimeConfig = getRuntimeConfig();
|
||||||
|
const basePath = runtimeConfig.app.baseURL.replace(/\/$/, '');
|
||||||
|
const wsPath = `${basePath}/ws?_=${Date.now()}`;
|
||||||
|
const configuredBase = runtimeConfig.public.wss?.trim();
|
||||||
|
|
||||||
|
if (configuredBase) {
|
||||||
|
return new URL(wsPath, configuredBase).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheme = 'https:' === window.location.protocol ? 'wss' : 'ws';
|
||||||
|
return new URL(wsPath, `${scheme}://${window.location.host}`).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const connect = () => {
|
||||||
|
const runtimeConfig = getRuntimeConfig();
|
||||||
|
|
||||||
|
if (socket.value && WebSocket.OPEN === socket.value.readyState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socket.value && WebSocket.CONNECTING === socket.value.readyState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
manualDisconnect.value = false;
|
||||||
|
connectionStatus.value = 'connecting';
|
||||||
|
|
||||||
|
socket.value = new WebSocket(buildWsUrl());
|
||||||
|
|
||||||
|
if ('development' === runtimeConfig.public?.APP_ENV) {
|
||||||
|
window.ws = socket.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.value.addEventListener('open', () => {
|
||||||
|
isConnected.value = true;
|
||||||
|
connectionStatus.value = 'connected';
|
||||||
|
error.value = null;
|
||||||
|
error_count.value = 0;
|
||||||
|
reconnectAttempts.value = 0;
|
||||||
|
dispatch('connect', null);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.addEventListener('close', () => {
|
||||||
|
isConnected.value = false;
|
||||||
|
connectionStatus.value = 'disconnected';
|
||||||
|
error.value = 'Disconnected from server.';
|
||||||
|
dispatch('disconnect', null);
|
||||||
|
scheduleReconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.addEventListener('error', () => {
|
||||||
|
isConnected.value = false;
|
||||||
|
connectionStatus.value = 'disconnected';
|
||||||
|
error.value = 'Connection error: Unknown error';
|
||||||
|
error_count.value += 1;
|
||||||
|
dispatch('connect_error', { message: 'Unknown error' });
|
||||||
|
scheduleReconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.addEventListener('message', (event: MessageEvent<string>) => {
|
||||||
|
let payload: WebSocketEnvelope | null = null;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(event.data);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload?.event || 'string' != typeof payload.event) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = payload.data;
|
||||||
|
if ('string' === typeof data) {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
} catch {
|
||||||
|
data = payload.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(payload.event, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
setupVisibilityListener();
|
||||||
|
};
|
||||||
|
|
||||||
|
on('connect', () => getConfig().loadConfig(false));
|
||||||
|
|
||||||
|
on('connected', () => {
|
||||||
|
error.value = null;
|
||||||
|
getConfig().loadConfig(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
on('item_added', (data: WSEP['item_added']) => {
|
||||||
|
const queueState = getQueueState();
|
||||||
|
const toast = getToast();
|
||||||
|
|
||||||
|
queueState.add(data.data._id, data.data);
|
||||||
|
toast.success(`Item queued: ${ag(queueState.get(data.data._id, {} as StoreItem), 'title')}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
on(
|
||||||
|
['log_info', 'log_success', 'log_warning', 'log_error'],
|
||||||
|
(event, data: WSEP['log_info']) => {
|
||||||
|
const toast = getToast();
|
||||||
|
const message =
|
||||||
|
'string' === typeof data?.message
|
||||||
|
? data.message
|
||||||
|
: String((data?.data as Record<string, unknown>)?.message ?? '');
|
||||||
|
const extra = (data?.data as Record<string, unknown>)?.data || data?.data || {};
|
||||||
|
switch (event) {
|
||||||
|
case 'log_info':
|
||||||
|
toast.info(message, extra);
|
||||||
|
break;
|
||||||
|
case 'log_success':
|
||||||
|
toast.success(message, extra);
|
||||||
|
break;
|
||||||
|
case 'log_warning':
|
||||||
|
toast.warning(message, extra);
|
||||||
|
break;
|
||||||
|
case 'log_error':
|
||||||
|
toast.error(message, extra);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
on('item_cancelled', (data: WSEP['item_cancelled']) => {
|
||||||
|
const queueState = getQueueState();
|
||||||
|
const toast = getToast();
|
||||||
|
const id = data.data._id;
|
||||||
|
|
||||||
|
if (true !== queueState.has(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.warning(`Download cancelled: ${ag(queueState.get(id, {} as StoreItem), 'title')}`);
|
||||||
|
|
||||||
|
if (true === queueState.has(id)) {
|
||||||
|
queueState.remove(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
on('item_deleted', (data: WSEP['item_deleted']) => {
|
||||||
|
const queueState = getQueueState();
|
||||||
|
const id = data.data._id;
|
||||||
|
|
||||||
|
if (true === queueState.has(id)) {
|
||||||
|
queueState.remove(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
on('item_updated', (data: WSEP['item_updated']) => {
|
||||||
|
const queueState = getQueueState();
|
||||||
|
const id = data.data._id;
|
||||||
|
|
||||||
|
if (true === queueState.has(id)) {
|
||||||
|
queueState.update(id, data.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
on('item_moved', (data: WSEP['item_moved']) => {
|
||||||
|
const queueState = getQueueState();
|
||||||
|
const to = data.data.to;
|
||||||
|
const id = data.data.item._id;
|
||||||
|
|
||||||
|
if ('queue' === to) {
|
||||||
|
queueState.add(id, data.data.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('history' === to) {
|
||||||
|
if (true === queueState.has(id)) {
|
||||||
|
queueState.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
on(
|
||||||
|
['paused', 'resumed'],
|
||||||
|
(event, data: WSEP['paused']) => {
|
||||||
|
const config = getConfig();
|
||||||
|
const toast = getToast();
|
||||||
|
const pausedState = Boolean(data.data.paused);
|
||||||
|
config.update('paused', pausedState);
|
||||||
|
|
||||||
|
if ('resumed' === event) {
|
||||||
|
toast.success('Download queue resumed.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.warning('Download queue paused.', { timeout: 10000 });
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
on('config_update', (data: WSEP['config_update']) => {
|
||||||
|
const config = getConfig();
|
||||||
|
const configUpdate = data.data as ConfigUpdatePayload;
|
||||||
|
if (!configUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.patch(configUpdate.feature, configUpdate.action, configUpdate.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
const appSocketApi = proxyRefs({
|
||||||
|
connect,
|
||||||
|
reconnect,
|
||||||
|
disconnect,
|
||||||
|
on,
|
||||||
|
off,
|
||||||
|
emit,
|
||||||
|
isConnected,
|
||||||
|
getSessionId,
|
||||||
|
connectionStatus: readonly(connectionStatus),
|
||||||
|
error: readonly(error),
|
||||||
|
error_count: readonly(error_count),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useAppSocket = () => appSocketApi;
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { ref, readonly, computed, toRaw } from 'vue';
|
import { ref, readonly, computed, toRaw } from 'vue';
|
||||||
|
|
||||||
import { useNotification } from '~/composables/useNotification';
|
import { useNotification } from '~/composables/useNotification';
|
||||||
import { useConfigStore } from '~/stores/ConfigStore';
|
|
||||||
import { request, parse_api_error, sTrim, encodePath } from '~/utils';
|
import { request, parse_api_error, sTrim, encodePath } from '~/utils';
|
||||||
import type { FileItem, Pagination } from '~/types/filebrowser';
|
import type { FileItem, Pagination } from '~/types/filebrowser';
|
||||||
|
|
||||||
|
|
@ -50,7 +49,7 @@ const handleError = (error: unknown): void => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildQueryParams = (page?: number): string => {
|
const buildQueryParams = (page?: number): string => {
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('page', String(page ?? pagination.value.page));
|
params.set('page', String(page ?? pagination.value.page));
|
||||||
params.set('per_page', String(config.app.default_pagination || 50));
|
params.set('per_page', String(config.app.default_pagination || 50));
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
|
import { useNotificationCenter } from '~/composables/useNotificationCenter';
|
||||||
import { getNuxtToastManager } from '~/utils/nuxtToastManager';
|
import { getNuxtToastManager } from '~/utils/nuxtToastManager';
|
||||||
|
|
||||||
export type notificationType = 'info' | 'success' | 'warning' | 'error';
|
export type notificationType = 'info' | 'success' | 'warning' | 'error';
|
||||||
|
|
@ -70,7 +71,7 @@ const sendMessage = (
|
||||||
message: string,
|
message: string,
|
||||||
opts?: notificationOptions,
|
opts?: notificationOptions,
|
||||||
): void => {
|
): void => {
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationCenter();
|
||||||
|
|
||||||
const useToastNotification =
|
const useToastNotification =
|
||||||
!window.isSecureContext ||
|
!window.isSecureContext ||
|
||||||
|
|
@ -115,7 +116,7 @@ const sendMessage = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const notify = (type: notificationType, message: string, opts?: notificationOptions): void => {
|
const notify = (type: notificationType, message: string, opts?: notificationOptions): void => {
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationCenter();
|
||||||
|
|
||||||
if (!opts) {
|
if (!opts) {
|
||||||
opts = {};
|
opts = {};
|
||||||
|
|
|
||||||
115
ui/app/composables/useNotificationCenter.ts
Normal file
115
ui/app/composables/useNotificationCenter.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { computed, proxyRefs } from 'vue';
|
||||||
|
import { useStorage } from '@vueuse/core';
|
||||||
|
import type { notification, notificationType } from '~/composables/useNotification';
|
||||||
|
|
||||||
|
const NOTIFICATION_META: Record<notificationType, { level: number; color: string; icon: string }> =
|
||||||
|
{
|
||||||
|
error: { level: 3, color: 'error', icon: 'i-lucide-triangle-alert' },
|
||||||
|
warning: { level: 2, color: 'warning', icon: 'i-lucide-circle-alert' },
|
||||||
|
success: { level: 1, color: 'success', icon: 'i-lucide-badge-check' },
|
||||||
|
info: { level: 0, color: 'info', icon: 'i-lucide-info' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const notifications = useStorage<notification[]>('notifications', []);
|
||||||
|
|
||||||
|
const unreadCount = computed<number>(() => notifications.value.filter((n) => !n.seen).length);
|
||||||
|
|
||||||
|
const severityLevel = computed<notificationType | null>(() => {
|
||||||
|
const unread = notifications.value.filter((n) => !n.seen);
|
||||||
|
|
||||||
|
if (0 === unread.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unread.reduce((highest, current) =>
|
||||||
|
NOTIFICATION_META[current.level].level > NOTIFICATION_META[highest.level].level
|
||||||
|
? current
|
||||||
|
: highest,
|
||||||
|
).level;
|
||||||
|
});
|
||||||
|
|
||||||
|
const severityColor = computed<string>(() => {
|
||||||
|
const level = severityLevel.value;
|
||||||
|
return level ? NOTIFICATION_META[level].color : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const severityIcon = computed<string>(() => {
|
||||||
|
const level = severityLevel.value;
|
||||||
|
return level ? NOTIFICATION_META[level].icon : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedNotifications = computed<notification[]>(() => {
|
||||||
|
return [...notifications.value].sort((left, right) => {
|
||||||
|
const severityDiff = NOTIFICATION_META[right.level].level - NOTIFICATION_META[left.level].level;
|
||||||
|
|
||||||
|
if (0 !== severityDiff) {
|
||||||
|
return severityDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(right.created).getTime() - new Date(left.created).getTime();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const add = (level: notificationType, message: string, seen: boolean = false): string => {
|
||||||
|
const id = Array.from(window.crypto.getRandomValues(new Uint8Array(14 / 2)), (dec: number) =>
|
||||||
|
dec.toString(16).padStart(2, '0'),
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
notifications.value.unshift({
|
||||||
|
id,
|
||||||
|
message,
|
||||||
|
level,
|
||||||
|
seen,
|
||||||
|
created: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (notifications.value.length > 99) {
|
||||||
|
notifications.value.length = 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clear = (): void => {
|
||||||
|
notifications.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const markAllRead = (): void => {
|
||||||
|
notifications.value.forEach((item) => {
|
||||||
|
item.seen = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const markRead = (id: string): void => {
|
||||||
|
const entry = notifications.value.find((item) => item.id === id);
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.seen = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const get = (id: string): notification | undefined =>
|
||||||
|
notifications.value.find((item) => item.id === id);
|
||||||
|
|
||||||
|
const remove = (id: string): void => {
|
||||||
|
notifications.value = notifications.value.filter((item) => item.id !== id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationCenterApi = proxyRefs({
|
||||||
|
notifications,
|
||||||
|
unreadCount,
|
||||||
|
severityLevel,
|
||||||
|
severityColor,
|
||||||
|
severityIcon,
|
||||||
|
sortedNotifications,
|
||||||
|
add,
|
||||||
|
get,
|
||||||
|
markAllRead,
|
||||||
|
clear,
|
||||||
|
markRead,
|
||||||
|
remove,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useNotificationCenter = () => notificationCenterApi;
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { computed, toValue, type MaybeRefOrGetter } from 'vue';
|
import { computed, toValue, type MaybeRefOrGetter } from 'vue';
|
||||||
|
|
||||||
|
import { useYtpConfig } from '~/composables/useYtpConfig';
|
||||||
import type { Preset } from '~/types/presets';
|
import type { Preset } from '~/types/presets';
|
||||||
import { prettyName } from '~/utils';
|
import { prettyName } from '~/utils';
|
||||||
|
|
||||||
|
|
@ -19,7 +20,7 @@ export const usePresetOptions = (
|
||||||
source?: MaybeRefOrGetter<Preset[] | readonly Preset[] | undefined>,
|
source?: MaybeRefOrGetter<Preset[] | readonly Preset[] | undefined>,
|
||||||
options: UsePresetOptionsOptions = {},
|
options: UsePresetOptionsOptions = {},
|
||||||
) => {
|
) => {
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
|
|
||||||
const presets = computed<Preset[]>(() => {
|
const presets = computed<Preset[]>(() => {
|
||||||
const items = source ? toValue(source) : config.presets;
|
const items = source ? toValue(source) : config.presets;
|
||||||
|
|
|
||||||
219
ui/app/composables/useYtpConfig.ts
Normal file
219
ui/app/composables/useYtpConfig.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
import { proxyRefs, reactive, toRefs } from 'vue';
|
||||||
|
import { useStorage } from '@vueuse/core';
|
||||||
|
import type { ConfigState } from '~/types/config';
|
||||||
|
import type { DLField } from '~/types/dl_fields';
|
||||||
|
import type { Preset } from '~/types/presets';
|
||||||
|
import type { ConfigFeature, ConfigUpdateAction } from '~/types/sockets';
|
||||||
|
import { useQueueState } from '~/composables/useQueueState';
|
||||||
|
import { request } from '~/utils';
|
||||||
|
|
||||||
|
let last_reload = 0;
|
||||||
|
const CONFIG_TTL = 10;
|
||||||
|
|
||||||
|
const state = reactive<ConfigState>({
|
||||||
|
showForm: useStorage('showForm', true),
|
||||||
|
app: {
|
||||||
|
download_path: '/downloads',
|
||||||
|
remove_files: false,
|
||||||
|
ui_update_title: true,
|
||||||
|
output_template: '',
|
||||||
|
ytdlp_version: '',
|
||||||
|
max_workers: 20,
|
||||||
|
max_workers_per_extractor: 2,
|
||||||
|
default_preset: 'default',
|
||||||
|
instance_title: null,
|
||||||
|
console_enabled: false,
|
||||||
|
browser_control_enabled: false,
|
||||||
|
file_logging: false,
|
||||||
|
is_native: false,
|
||||||
|
app_version: '',
|
||||||
|
app_commit_sha: '',
|
||||||
|
app_build_date: '',
|
||||||
|
app_branch: '',
|
||||||
|
started: 0,
|
||||||
|
app_env: 'production',
|
||||||
|
simple_mode: false,
|
||||||
|
default_pagination: 50,
|
||||||
|
check_for_updates: true,
|
||||||
|
new_version: '',
|
||||||
|
yt_new_version: '',
|
||||||
|
},
|
||||||
|
presets: [
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
description: 'Default preset',
|
||||||
|
folder: '',
|
||||||
|
template: '',
|
||||||
|
cookies: '',
|
||||||
|
cli: '',
|
||||||
|
default: true,
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dl_fields: [],
|
||||||
|
folders: [],
|
||||||
|
ytdlp_options: [],
|
||||||
|
paused: false,
|
||||||
|
is_loaded: false,
|
||||||
|
is_loading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadConfig = async (force: boolean = false) => {
|
||||||
|
if (state.is_loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (state.is_loaded && !force && last_reload > 0) {
|
||||||
|
const age = (now - last_reload) / 1000;
|
||||||
|
if (age < CONFIG_TTL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.is_loaded = false;
|
||||||
|
state.is_loading = true;
|
||||||
|
try {
|
||||||
|
const resp = await request('/api/system/configuration', { timeout: 10 });
|
||||||
|
if (!resp.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await resp.json();
|
||||||
|
const queueState = useQueueState();
|
||||||
|
|
||||||
|
if ('number' === typeof data.history_count) {
|
||||||
|
delete data.history_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.queue) {
|
||||||
|
queueState.addAll(data.queue);
|
||||||
|
delete data.queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAll(data);
|
||||||
|
state.is_loaded = true;
|
||||||
|
last_reload = now;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`Failed to load configuration: ${e}`);
|
||||||
|
} finally {
|
||||||
|
state.is_loading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const add = (key: string, value: any) => {
|
||||||
|
if (key.includes('.')) {
|
||||||
|
const [parentKey, subKey] = key.split('.') as [keyof ConfigState, string];
|
||||||
|
state[parentKey][subKey] = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(state as any)[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const get = (key: string, defaultValue: any = null): any => {
|
||||||
|
if (key.includes('.')) {
|
||||||
|
const [parentKey, subKey] = key.split('.') as [keyof ConfigState, string];
|
||||||
|
const parent = state[parentKey] as any;
|
||||||
|
return parent?.[subKey] ?? defaultValue;
|
||||||
|
}
|
||||||
|
return (state as any)[key] ?? defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoaded = () => state.is_loaded;
|
||||||
|
|
||||||
|
const update = add;
|
||||||
|
|
||||||
|
const getAll = (): ConfigState => state;
|
||||||
|
|
||||||
|
const setAll = (data: Record<string, any>) => {
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (key.includes('.')) {
|
||||||
|
const [parentKey, subKey] = key.split('.') as [keyof ConfigState, string];
|
||||||
|
const parent = state[parentKey] as any;
|
||||||
|
parent[subKey] = data[key];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(state as any)[key] = data[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
state.is_loaded = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const patch = (feature: ConfigFeature, action: ConfigUpdateAction, data: unknown): void => {
|
||||||
|
const supportedFeatures: ConfigFeature[] = ['dl_fields', 'presets'];
|
||||||
|
|
||||||
|
if (!supportedFeatures.includes(feature)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('presets' === feature) {
|
||||||
|
const item = data as Preset;
|
||||||
|
const current = get(feature, []) as Array<Preset>;
|
||||||
|
|
||||||
|
if ('create' === action) {
|
||||||
|
current.push(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('delete' === action) {
|
||||||
|
const index = current.findIndex((i) => i.id === item.id);
|
||||||
|
if (-1 !== index) {
|
||||||
|
current.splice(index, 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('update' === action) {
|
||||||
|
const target = current.find((i) => i.id === item.id);
|
||||||
|
if (target) {
|
||||||
|
Object.assign(target, item);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('dl_fields' === feature) {
|
||||||
|
const item = data as DLField;
|
||||||
|
const current = get(feature, []) as Array<DLField>;
|
||||||
|
|
||||||
|
if ('create' === action) {
|
||||||
|
current.push(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('delete' === action) {
|
||||||
|
const index = current.findIndex((i) => i.id === item.id);
|
||||||
|
if (-1 !== index) {
|
||||||
|
current.splice(index, 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('update' === action) {
|
||||||
|
const target = current.find((i) => i.id === item.id);
|
||||||
|
if (target) {
|
||||||
|
Object.assign(target, item);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('replace' === action) {
|
||||||
|
state.dl_fields = data as Array<DLField>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ytpConfigApi = proxyRefs({
|
||||||
|
...toRefs(state),
|
||||||
|
add,
|
||||||
|
get,
|
||||||
|
update,
|
||||||
|
getAll,
|
||||||
|
setAll,
|
||||||
|
isLoaded,
|
||||||
|
patch,
|
||||||
|
loadConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useYtpConfig = () => ytpConfigApi;
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<UApp :toaster="toasterConfig">
|
<UApp :toaster="toasterConfig">
|
||||||
<Transition name="shell-mode" mode="out-in">
|
<Transition name="shell-mode" mode="out-in">
|
||||||
<div v-if="simpleMode" key="simple" class="shell-stage flex flex-col">
|
<div
|
||||||
|
v-if="simpleMode"
|
||||||
|
key="simple"
|
||||||
|
class="shell-stage flex flex-col bg-default/95 backdrop-blur-sm"
|
||||||
|
>
|
||||||
<UAlert
|
<UAlert
|
||||||
v-if="showConnectionBanner"
|
v-if="showConnectionBanner"
|
||||||
color="warning"
|
color="warning"
|
||||||
|
|
@ -517,8 +521,8 @@ type SwipeMode = 'open' | 'close';
|
||||||
const MOBILE_SIDEBAR_EDGE_WIDTH = 32;
|
const MOBILE_SIDEBAR_EDGE_WIDTH = 32;
|
||||||
const MOBILE_SIDEBAR_MIN_SWIPE_DISTANCE = 64;
|
const MOBILE_SIDEBAR_MIN_SWIPE_DISTANCE = 64;
|
||||||
|
|
||||||
const socket = useSocketStore();
|
const socket = useAppSocket();
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const colorMode = useColorMode();
|
const colorMode = useColorMode();
|
||||||
const loadedImage = ref();
|
const loadedImage = ref();
|
||||||
|
|
|
||||||
|
|
@ -153,12 +153,15 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pager
|
<UPagination
|
||||||
v-if="pagination.total_pages > 1"
|
v-if="pagination.total_pages > 1"
|
||||||
:page="pagination.page"
|
:page="pagination.page"
|
||||||
:last_page="pagination.total_pages"
|
:total="pagination.total"
|
||||||
:isLoading="isLoading"
|
:items-per-page="pagination.per_page"
|
||||||
@navigate="handlePageChange"
|
:disabled="isLoading"
|
||||||
|
show-edges
|
||||||
|
:sibling-count="0"
|
||||||
|
@update:page="handlePageChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -481,12 +484,15 @@
|
||||||
description="You can enable rename, delete, move, and create directory controls by setting YTP_BROWSER_CONTROL_ENABLED=true and restarting the application."
|
description="You can enable rename, delete, move, and create directory controls by setting YTP_BROWSER_CONTROL_ENABLED=true and restarting the application."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Pager
|
<UPagination
|
||||||
v-if="pagination.total_pages > 1"
|
v-if="pagination.total_pages > 1"
|
||||||
:page="pagination.page"
|
:page="pagination.page"
|
||||||
:last_page="pagination.total_pages"
|
:total="pagination.total"
|
||||||
:isLoading="isLoading"
|
:items-per-page="pagination.per_page"
|
||||||
@navigate="handlePageChange"
|
:disabled="isLoading"
|
||||||
|
show-edges
|
||||||
|
:sibling-count="0"
|
||||||
|
@update:page="handlePageChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UModal
|
<UModal
|
||||||
|
|
@ -536,7 +542,7 @@ import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const toast = useNotification();
|
const toast = useNotification();
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const browser = useBrowser();
|
const browser = useBrowser();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ import { request, ucFirst, uri } from '~/utils';
|
||||||
import { requirePageShell } from '~/utils/topLevelNavigation';
|
import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||||
|
|
||||||
const toast = useNotification();
|
const toast = useNotification();
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const pageShell = requirePageShell('changelog');
|
const pageShell = requirePageShell('changelog');
|
||||||
|
|
||||||
useHead({ title: 'CHANGELOG' });
|
useHead({ title: 'CHANGELOG' });
|
||||||
|
|
|
||||||
|
|
@ -82,12 +82,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pager
|
<UPagination
|
||||||
v-if="paging?.total_pages > 1"
|
v-if="paging?.total_pages > 1"
|
||||||
:page="paging.page"
|
:page="paging.page"
|
||||||
:last_page="paging.total_pages"
|
:total="paging.total"
|
||||||
:isLoading="isLoading"
|
:items-per-page="paging.per_page"
|
||||||
@navigate="navigatePage"
|
:disabled="isLoading"
|
||||||
|
show-edges
|
||||||
|
:sibling-count="0"
|
||||||
|
@update:page="navigatePage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -338,7 +338,7 @@ let terminalResizeObserver: ResizeObserver | null = null;
|
||||||
let didInitialRender = false;
|
let didInitialRender = false;
|
||||||
let renderedChunkCount = 0;
|
let renderedChunkCount = 0;
|
||||||
|
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const toast = useNotification();
|
const toast = useNotification();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const consoleSession = useConsoleSession();
|
const consoleSession = useConsoleSession();
|
||||||
|
|
|
||||||
|
|
@ -82,12 +82,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pager
|
<UPagination
|
||||||
v-if="paging?.total_pages > 1"
|
v-if="paging?.total_pages > 1"
|
||||||
:page="paging.page"
|
:page="paging.page"
|
||||||
:last_page="paging.total_pages"
|
:total="paging.total"
|
||||||
:isLoading="isLoading"
|
:items-per-page="paging.per_page"
|
||||||
@navigate="navigatePage"
|
:disabled="isLoading"
|
||||||
|
show-edges
|
||||||
|
:sibling-count="0"
|
||||||
|
@update:page="navigatePage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ const FILTER_CONTEXT_REGEX = /context:(\d+)/;
|
||||||
let scrollTimeout: NodeJS.Timeout | null = null;
|
let scrollTimeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
const toast = useNotification();
|
const toast = useNotification();
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const pageShell = requirePageShell('logs');
|
const pageShell = requirePageShell('logs');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,12 +95,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pager
|
<UPagination
|
||||||
v-if="paging?.total_pages > 1"
|
v-if="paging?.total_pages > 1"
|
||||||
:page="paging.page"
|
:page="paging.page"
|
||||||
:last_page="paging.total_pages"
|
:total="paging.total"
|
||||||
:isLoading="isLoading"
|
:items-per-page="paging.per_page"
|
||||||
@navigate="navigatePage"
|
:disabled="isLoading"
|
||||||
|
show-edges
|
||||||
|
:sibling-count="0"
|
||||||
|
@update:page="navigatePage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -468,7 +468,7 @@ import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||||
type PresetWithUI = Preset & { raw?: boolean; toggle_description?: boolean };
|
type PresetWithUI = Preset & { raw?: boolean; toggle_description?: boolean };
|
||||||
|
|
||||||
const presetsStore = usePresets();
|
const presetsStore = usePresets();
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const box = useConfirm();
|
const box = useConfirm();
|
||||||
const editor = usePresetEditor();
|
const editor = usePresetEditor();
|
||||||
const pageShell = requirePageShell('presets');
|
const pageShell = requirePageShell('presets');
|
||||||
|
|
|
||||||
|
|
@ -718,9 +718,9 @@ import { requirePageShell } from '~/utils/topLevelNavigation';
|
||||||
|
|
||||||
const box = useConfirm();
|
const box = useConfirm();
|
||||||
const toast = useNotification();
|
const toast = useNotification();
|
||||||
const config = useConfigStore();
|
const config = useYtpConfig();
|
||||||
const socket = useSocketStore();
|
const socket = useAppSocket();
|
||||||
const stateStore = useStateStore();
|
const stateStore = useQueueState();
|
||||||
const pageShell = requirePageShell('tasks');
|
const pageShell = requirePageShell('tasks');
|
||||||
const { confirmDialog } = useDialog();
|
const { confirmDialog } = useDialog();
|
||||||
const sessionCache = useSessionCache();
|
const sessionCache = useSessionCache();
|
||||||
|
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
import { useStorage } from '@vueuse/core';
|
|
||||||
import type { ConfigState } from '~/types/config';
|
|
||||||
import type { DLField } from '~/types/dl_fields';
|
|
||||||
import type { Preset } from '~/types/presets';
|
|
||||||
import type { ConfigFeature, ConfigUpdateAction } from '~/types/sockets';
|
|
||||||
import { request } from '~/utils';
|
|
||||||
|
|
||||||
let last_reload = 0;
|
|
||||||
const CONFIG_TTL = 10;
|
|
||||||
|
|
||||||
export const useConfigStore = defineStore('config', () => {
|
|
||||||
const state = reactive<ConfigState>({
|
|
||||||
showForm: useStorage('showForm', true),
|
|
||||||
app: {
|
|
||||||
download_path: '/downloads',
|
|
||||||
remove_files: false,
|
|
||||||
ui_update_title: true,
|
|
||||||
output_template: '',
|
|
||||||
ytdlp_version: '',
|
|
||||||
max_workers: 20,
|
|
||||||
max_workers_per_extractor: 2,
|
|
||||||
default_preset: 'default',
|
|
||||||
instance_title: null,
|
|
||||||
console_enabled: false,
|
|
||||||
browser_control_enabled: false,
|
|
||||||
file_logging: false,
|
|
||||||
is_native: false,
|
|
||||||
app_version: '',
|
|
||||||
app_commit_sha: '',
|
|
||||||
app_build_date: '',
|
|
||||||
app_branch: '',
|
|
||||||
started: 0,
|
|
||||||
app_env: 'production',
|
|
||||||
simple_mode: false,
|
|
||||||
default_pagination: 50,
|
|
||||||
check_for_updates: true,
|
|
||||||
new_version: '',
|
|
||||||
yt_new_version: '',
|
|
||||||
},
|
|
||||||
presets: [
|
|
||||||
{
|
|
||||||
name: 'default',
|
|
||||||
description: 'Default preset',
|
|
||||||
folder: '',
|
|
||||||
template: '',
|
|
||||||
cookies: '',
|
|
||||||
cli: '',
|
|
||||||
default: true,
|
|
||||||
priority: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
dl_fields: [],
|
|
||||||
folders: [],
|
|
||||||
ytdlp_options: [],
|
|
||||||
paused: false,
|
|
||||||
is_loaded: false,
|
|
||||||
is_loading: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadConfig = async (force: boolean = false) => {
|
|
||||||
if (state.is_loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
if (state.is_loaded && !force && last_reload > 0) {
|
|
||||||
const age = (now - last_reload) / 1000;
|
|
||||||
if (age < CONFIG_TTL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.is_loaded = false;
|
|
||||||
state.is_loading = true;
|
|
||||||
try {
|
|
||||||
const resp = await request('/api/system/configuration', { timeout: 10 });
|
|
||||||
if (!resp.ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await resp.json();
|
|
||||||
const stateStore = useStateStore();
|
|
||||||
|
|
||||||
if ('number' === typeof data.history_count) {
|
|
||||||
stateStore.setHistoryCount(data.history_count);
|
|
||||||
delete data.history_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.queue) {
|
|
||||||
stateStore.addAll('queue', data.queue);
|
|
||||||
delete data.queue;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAll(data);
|
|
||||||
state.is_loaded = true;
|
|
||||||
last_reload = now;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`Failed to load configuration: ${e}`);
|
|
||||||
} finally {
|
|
||||||
state.is_loading = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const add = (key: string, value: any) => {
|
|
||||||
if (key.includes('.')) {
|
|
||||||
const [parentKey, subKey] = key.split('.') as [keyof ConfigState, string];
|
|
||||||
state[parentKey][subKey] = value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
(state as any)[key] = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const get = (key: string, defaultValue: any = null): any => {
|
|
||||||
if (key.includes('.')) {
|
|
||||||
const [parentKey, subKey] = key.split('.') as [keyof ConfigState, string];
|
|
||||||
const parent = state[parentKey] as any;
|
|
||||||
return parent?.[subKey] ?? defaultValue;
|
|
||||||
}
|
|
||||||
return (state as any)[key] ?? defaultValue;
|
|
||||||
};
|
|
||||||
const isLoaded = () => state.is_loaded;
|
|
||||||
|
|
||||||
const update = add;
|
|
||||||
|
|
||||||
const getAll = (): ConfigState => state;
|
|
||||||
|
|
||||||
const setAll = (data: Record<string, any>) => {
|
|
||||||
Object.keys(data).forEach((key) => {
|
|
||||||
if (key.includes('.')) {
|
|
||||||
const [parentKey, subKey] = key.split('.') as [keyof ConfigState, string];
|
|
||||||
const parent = state[parentKey] as any;
|
|
||||||
parent[subKey] = data[key];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
(state as any)[key] = data[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
state.is_loaded = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const patch = (feature: ConfigFeature, action: ConfigUpdateAction, data: unknown): void => {
|
|
||||||
const supportedFeatures: ConfigFeature[] = ['dl_fields', 'presets'];
|
|
||||||
|
|
||||||
if (!supportedFeatures.includes(feature)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('presets' === feature) {
|
|
||||||
const item = data as Preset;
|
|
||||||
const current = get(feature, []) as Array<Preset>;
|
|
||||||
|
|
||||||
if ('create' === action) {
|
|
||||||
current.push(item);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('delete' === action) {
|
|
||||||
const index = current.findIndex((i) => i.id === item.id);
|
|
||||||
if (-1 !== index) {
|
|
||||||
current.splice(index, 1);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('update' === action) {
|
|
||||||
const target = current.find((i) => i.id === item.id);
|
|
||||||
if (target) {
|
|
||||||
Object.assign(target, item);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('dl_fields' === feature) {
|
|
||||||
const item = data as DLField;
|
|
||||||
const current = get(feature, []) as Array<DLField>;
|
|
||||||
|
|
||||||
if ('create' === action) {
|
|
||||||
current.push(item);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('delete' === action) {
|
|
||||||
const index = current.findIndex((i) => i.id === item.id);
|
|
||||||
if (-1 !== index) {
|
|
||||||
current.splice(index, 1);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('update' === action) {
|
|
||||||
const target = current.find((i) => i.id === item.id);
|
|
||||||
if (target) {
|
|
||||||
Object.assign(target, item);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ('replace' === action) {
|
|
||||||
state.dl_fields = data as Array<DLField>;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
add,
|
|
||||||
get,
|
|
||||||
update,
|
|
||||||
getAll,
|
|
||||||
setAll,
|
|
||||||
isLoaded,
|
|
||||||
patch,
|
|
||||||
loadConfig,
|
|
||||||
} as { [K in keyof ConfigState]: Ref<ConfigState[K]> } & {
|
|
||||||
add: typeof add;
|
|
||||||
get: typeof get;
|
|
||||||
update: typeof update;
|
|
||||||
getAll: typeof getAll;
|
|
||||||
setAll: typeof setAll;
|
|
||||||
patch: typeof patch;
|
|
||||||
isLoaded: typeof isLoaded;
|
|
||||||
loadConfig: typeof loadConfig;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
import { useStorage } from '@vueuse/core';
|
|
||||||
import type { notification, notificationType } from '~/composables/useNotification';
|
|
||||||
|
|
||||||
const _map: Record<notificationType, { level: number; color: string; icon: string }> = {
|
|
||||||
error: { level: 3, color: 'error', icon: 'i-lucide-triangle-alert' },
|
|
||||||
warning: { level: 2, color: 'warning', icon: 'i-lucide-circle-alert' },
|
|
||||||
success: { level: 1, color: 'success', icon: 'i-lucide-badge-check' },
|
|
||||||
info: { level: 0, color: 'info', icon: 'i-lucide-info' },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useNotificationStore = defineStore('notifications', () => {
|
|
||||||
const notifications = useStorage<notification[]>('notifications', []);
|
|
||||||
const unreadCount = computed<number>(() => notifications.value.filter((n) => !n.seen).length);
|
|
||||||
|
|
||||||
const severityLevel = computed<notificationType | null>(() => {
|
|
||||||
const unread = notifications.value.filter((n) => !n.seen);
|
|
||||||
if (0 === unread.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return unread.reduce((h, n) => (_map[n.level].level > _map[h.level].level ? n : h)).level;
|
|
||||||
});
|
|
||||||
|
|
||||||
const severityColor = computed<string>(() => {
|
|
||||||
const level = severityLevel.value;
|
|
||||||
return level ? _map[level].color : '';
|
|
||||||
});
|
|
||||||
|
|
||||||
const severityIcon = computed<string>(() => {
|
|
||||||
const level = severityLevel.value;
|
|
||||||
return level ? _map[level].icon : '';
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortedNotifications = computed<notification[]>(() => {
|
|
||||||
return [...notifications.value].sort((a, b) => {
|
|
||||||
const severityDiff = _map[b.level].level - _map[a.level].level;
|
|
||||||
if (0 !== severityDiff) {
|
|
||||||
return severityDiff;
|
|
||||||
}
|
|
||||||
return new Date(b.created).getTime() - new Date(a.created).getTime();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const add = (level: notificationType, message: string, seen: boolean = false): string => {
|
|
||||||
const id = Array.from(window.crypto.getRandomValues(new Uint8Array(14 / 2)), (dec: any) =>
|
|
||||||
dec.toString(16).padStart(2, '0'),
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
notifications.value.unshift({
|
|
||||||
id: id,
|
|
||||||
message,
|
|
||||||
level,
|
|
||||||
seen: seen,
|
|
||||||
created: new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (notifications.value.length > 99) {
|
|
||||||
notifications.value.length = 99;
|
|
||||||
}
|
|
||||||
|
|
||||||
return id;
|
|
||||||
};
|
|
||||||
|
|
||||||
const clear = () => (notifications.value = []);
|
|
||||||
const markAllRead = () => notifications.value.forEach((n) => (n.seen = true));
|
|
||||||
|
|
||||||
const markRead = (id: string) => {
|
|
||||||
const n = notifications.value.find((n) => n.id === id);
|
|
||||||
if (!n) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
n.seen = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const get = (id: string): notification | undefined =>
|
|
||||||
notifications.value.find((n) => n.id === id);
|
|
||||||
|
|
||||||
const remove = (id: string) =>
|
|
||||||
(notifications.value = notifications.value.filter((n) => n.id !== id));
|
|
||||||
|
|
||||||
return {
|
|
||||||
notifications,
|
|
||||||
unreadCount,
|
|
||||||
severityLevel,
|
|
||||||
severityColor,
|
|
||||||
severityIcon,
|
|
||||||
sortedNotifications,
|
|
||||||
add,
|
|
||||||
get,
|
|
||||||
markAllRead,
|
|
||||||
clear,
|
|
||||||
markRead,
|
|
||||||
remove,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
@ -1,404 +0,0 @@
|
||||||
import { ref, readonly } from 'vue';
|
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
import type { StoreItem } from '~/types/store';
|
|
||||||
import type {
|
|
||||||
ConfigUpdatePayload,
|
|
||||||
WebSocketClientEmits,
|
|
||||||
WebSocketEnvelope,
|
|
||||||
WSEP as WSEP,
|
|
||||||
} from '~/types/sockets';
|
|
||||||
|
|
||||||
export type connectionStatus = 'connected' | 'disconnected' | 'connecting';
|
|
||||||
|
|
||||||
type SocketHandler = (...args: unknown[]) => void;
|
|
||||||
type HandlerRegistry = Map<SocketHandler, SocketHandler>;
|
|
||||||
type KnownEvent = keyof WSEP;
|
|
||||||
|
|
||||||
export const useSocketStore = defineStore('socket', () => {
|
|
||||||
const runtimeConfig = useRuntimeConfig();
|
|
||||||
const config = useConfigStore();
|
|
||||||
const stateStore = useStateStore();
|
|
||||||
const toast = useNotification();
|
|
||||||
|
|
||||||
const socket = ref<WebSocket | null>(null);
|
|
||||||
const isConnected = ref<boolean>(false);
|
|
||||||
const connectionStatus = ref<connectionStatus>('disconnected');
|
|
||||||
const error = ref<string | null>(null);
|
|
||||||
const error_count = ref<number>(0);
|
|
||||||
const wasHidden = ref<boolean>(false);
|
|
||||||
const reconnectTimeout = ref<NodeJS.Timeout | null>(null);
|
|
||||||
const manualDisconnect = ref<boolean>(false);
|
|
||||||
const reconnectAttempts = ref<number>(0);
|
|
||||||
|
|
||||||
const handlers = new Map<string, HandlerRegistry>();
|
|
||||||
|
|
||||||
const emit = <K extends keyof WebSocketClientEmits>(
|
|
||||||
event: K,
|
|
||||||
data: WebSocketClientEmits[K],
|
|
||||||
): void => {
|
|
||||||
if (!socket.value || WebSocket.OPEN !== socket.value.readyState) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
socket.value.send(JSON.stringify({ event, data }));
|
|
||||||
};
|
|
||||||
|
|
||||||
function on<K extends KnownEvent>(event: K, callback: (payload: WSEP[K]) => void): void;
|
|
||||||
function on<K extends KnownEvent>(event: K[], callback: (payload: WSEP[K]) => void): void;
|
|
||||||
function on<K extends KnownEvent>(
|
|
||||||
event: K | K[],
|
|
||||||
callback: (event: K, payload: WSEP[K]) => void,
|
|
||||||
withEvent: true,
|
|
||||||
): void;
|
|
||||||
function on(event: string | string[], callback: SocketHandler, withEvent?: boolean): void;
|
|
||||||
function on(event: string | string[], callback: SocketHandler, withEvent: boolean = false): void {
|
|
||||||
const events = Array.isArray(event) ? event : [event];
|
|
||||||
events.forEach((eventName) => {
|
|
||||||
if (!handlers.has(eventName)) {
|
|
||||||
handlers.set(eventName, new Map());
|
|
||||||
}
|
|
||||||
|
|
||||||
const registry = handlers.get(eventName) as HandlerRegistry;
|
|
||||||
const handler =
|
|
||||||
true === withEvent
|
|
||||||
? (payload: unknown) => callback(eventName, payload)
|
|
||||||
: (payload: unknown) => callback(payload);
|
|
||||||
|
|
||||||
registry.set(callback, handler);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function off<K extends KnownEvent>(event: K, callback?: (payload: WSEP[K]) => void): void;
|
|
||||||
function off<K extends KnownEvent>(event: K[], callback?: (payload: WSEP[K]) => void): void;
|
|
||||||
function off(event: string | string[], callback?: SocketHandler): void;
|
|
||||||
function off(event: string | string[], callback?: SocketHandler): void {
|
|
||||||
const events = Array.isArray(event) ? event : [event];
|
|
||||||
events.forEach((eventName) => {
|
|
||||||
const registry = handlers.get(eventName);
|
|
||||||
if (!registry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!callback) {
|
|
||||||
registry.clear();
|
|
||||||
handlers.delete(eventName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.delete(callback);
|
|
||||||
if (0 === registry.size) {
|
|
||||||
handlers.delete(eventName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSessionId = (): string | null => null;
|
|
||||||
|
|
||||||
const dispatch = (eventName: string, payload: unknown): void => {
|
|
||||||
const registry = handlers.get(eventName);
|
|
||||||
if (!registry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.forEach((handler) => handler(payload));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVisibilityChange = () => {
|
|
||||||
if (document.hidden) {
|
|
||||||
wasHidden.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (true === wasHidden.value && false === isConnected.value) {
|
|
||||||
if (null !== reconnectTimeout.value) {
|
|
||||||
clearTimeout(reconnectTimeout.value);
|
|
||||||
reconnectTimeout.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
reconnectTimeout.value = setTimeout(() => {
|
|
||||||
if (false === isConnected.value) {
|
|
||||||
console.debug('[SocketStore] Page visible after background, reconnecting...');
|
|
||||||
reconnect();
|
|
||||||
}
|
|
||||||
reconnectTimeout.value = null;
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
wasHidden.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setupVisibilityListener = () => {
|
|
||||||
if (typeof document !== 'undefined') {
|
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanupVisibilityListener = () => {
|
|
||||||
if (typeof document !== 'undefined') {
|
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
||||||
}
|
|
||||||
if (null !== reconnectTimeout.value) {
|
|
||||||
clearTimeout(reconnectTimeout.value);
|
|
||||||
reconnectTimeout.value = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleReconnect = () => {
|
|
||||||
if (true === manualDisconnect.value || true === isConnected.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reconnectAttempts.value >= 50) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== reconnectTimeout.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reconnectTimeout.value = setTimeout(() => {
|
|
||||||
reconnectAttempts.value += 1;
|
|
||||||
reconnectTimeout.value = null;
|
|
||||||
connect();
|
|
||||||
}, 5000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const reconnect = () => {
|
|
||||||
if (true === isConnected.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
connect();
|
|
||||||
connectionStatus.value = 'connecting';
|
|
||||||
};
|
|
||||||
|
|
||||||
const disconnect = () => {
|
|
||||||
manualDisconnect.value = true;
|
|
||||||
if (null === socket.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
socket.value.close();
|
|
||||||
socket.value = null;
|
|
||||||
isConnected.value = false;
|
|
||||||
connectionStatus.value = 'disconnected';
|
|
||||||
cleanupVisibilityListener();
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildWsUrl = (): string => {
|
|
||||||
const basePath = runtimeConfig.app.baseURL.replace(/\/$/, '');
|
|
||||||
const wsPath = `${basePath}/ws?_=${Date.now()}`;
|
|
||||||
const configuredBase = runtimeConfig.public.wss?.trim();
|
|
||||||
|
|
||||||
if (configuredBase) {
|
|
||||||
return new URL(wsPath, configuredBase).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheme = 'https:' === window.location.protocol ? 'wss' : 'ws';
|
|
||||||
return new URL(wsPath, `${scheme}://${window.location.host}`).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const connect = () => {
|
|
||||||
if (socket.value && WebSocket.OPEN === socket.value.readyState) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socket.value && WebSocket.CONNECTING === socket.value.readyState) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
manualDisconnect.value = false;
|
|
||||||
connectionStatus.value = 'connecting';
|
|
||||||
|
|
||||||
socket.value = new WebSocket(buildWsUrl());
|
|
||||||
|
|
||||||
if ('development' === runtimeConfig.public?.APP_ENV) {
|
|
||||||
window.ws = socket.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.value.addEventListener('open', () => {
|
|
||||||
isConnected.value = true;
|
|
||||||
connectionStatus.value = 'connected';
|
|
||||||
error.value = null;
|
|
||||||
error_count.value = 0;
|
|
||||||
reconnectAttempts.value = 0;
|
|
||||||
dispatch('connect', null);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.value.addEventListener('close', () => {
|
|
||||||
isConnected.value = false;
|
|
||||||
connectionStatus.value = 'disconnected';
|
|
||||||
error.value = 'Disconnected from server.';
|
|
||||||
dispatch('disconnect', null);
|
|
||||||
scheduleReconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.value.addEventListener('error', () => {
|
|
||||||
isConnected.value = false;
|
|
||||||
connectionStatus.value = 'disconnected';
|
|
||||||
error.value = 'Connection error: Unknown error';
|
|
||||||
error_count.value += 1;
|
|
||||||
dispatch('connect_error', { message: 'Unknown error' });
|
|
||||||
scheduleReconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.value.addEventListener('message', (event: MessageEvent<string>) => {
|
|
||||||
let payload: WebSocketEnvelope | null = null;
|
|
||||||
try {
|
|
||||||
payload = JSON.parse(event.data);
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!payload?.event || 'string' != typeof payload.event) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = payload.data;
|
|
||||||
if ('string' === typeof data) {
|
|
||||||
try {
|
|
||||||
data = JSON.parse(data);
|
|
||||||
} catch {
|
|
||||||
data = payload.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(payload.event, data);
|
|
||||||
});
|
|
||||||
|
|
||||||
setupVisibilityListener();
|
|
||||||
};
|
|
||||||
|
|
||||||
on('connect', () => config.loadConfig(false));
|
|
||||||
|
|
||||||
on('connected', () => {
|
|
||||||
error.value = null;
|
|
||||||
config.loadConfig(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
on('item_added', (data: WSEP['item_added']) => {
|
|
||||||
stateStore.add('queue', data.data._id, data.data);
|
|
||||||
toast.success(
|
|
||||||
`Item queued: ${ag(stateStore.get('queue', data.data._id, {} as StoreItem), 'title')}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
on(
|
|
||||||
['log_info', 'log_success', 'log_warning', 'log_error'],
|
|
||||||
(event, data: WSEP['log_info']) => {
|
|
||||||
const message =
|
|
||||||
'string' === typeof data?.message
|
|
||||||
? data.message
|
|
||||||
: String((data?.data as Record<string, unknown>)?.message ?? '');
|
|
||||||
const extra = (data?.data as Record<string, unknown>)?.data || data?.data || {};
|
|
||||||
switch (event) {
|
|
||||||
case 'log_info':
|
|
||||||
toast.info(message, extra);
|
|
||||||
break;
|
|
||||||
case 'log_success':
|
|
||||||
toast.success(message, extra);
|
|
||||||
break;
|
|
||||||
case 'log_warning':
|
|
||||||
toast.warning(message, extra);
|
|
||||||
break;
|
|
||||||
case 'log_error':
|
|
||||||
toast.error(message, extra);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
on('item_cancelled', (data: WSEP['item_cancelled']) => {
|
|
||||||
const id = data.data._id;
|
|
||||||
|
|
||||||
if (true !== stateStore.has('queue', id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.warning(
|
|
||||||
`Download cancelled: ${ag(stateStore.get('queue', id, {} as StoreItem), 'title')}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (true === stateStore.has('queue', id)) {
|
|
||||||
stateStore.remove('queue', id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
on('item_deleted', (data: WSEP['item_deleted']) => {
|
|
||||||
const id = data.data._id;
|
|
||||||
|
|
||||||
if (true !== stateStore.has('history', id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stateStore.remove('history', id);
|
|
||||||
});
|
|
||||||
|
|
||||||
on('item_updated', (data: WSEP['item_updated']) => {
|
|
||||||
const id = data.data._id;
|
|
||||||
|
|
||||||
if (true === stateStore.has('history', id)) {
|
|
||||||
stateStore.update('history', id, data.data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (true === stateStore.has('queue', id)) {
|
|
||||||
stateStore.update('queue', id, data.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
on('item_moved', (data: WSEP['item_moved']) => {
|
|
||||||
const to = data.data.to;
|
|
||||||
const id = data.data.item._id;
|
|
||||||
|
|
||||||
if ('queue' === to) {
|
|
||||||
if (true === stateStore.has('history', id)) {
|
|
||||||
stateStore.remove('history', id);
|
|
||||||
}
|
|
||||||
stateStore.add('queue', id, data.data.item);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('history' === to) {
|
|
||||||
if (true === stateStore.has('queue', id)) {
|
|
||||||
stateStore.remove('queue', id);
|
|
||||||
}
|
|
||||||
stateStore.add('history', id, data.data.item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
on(
|
|
||||||
['paused', 'resumed'],
|
|
||||||
(event, data: WSEP['paused']) => {
|
|
||||||
const pausedState = Boolean(data.data.paused);
|
|
||||||
config.update('paused', pausedState);
|
|
||||||
|
|
||||||
if ('resumed' === event) {
|
|
||||||
toast.success('Download queue resumed.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.warning('Download queue paused.', { timeout: 10000 });
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
on('config_update', (data: WSEP['config_update']) => {
|
|
||||||
const configUpdate = data.data as ConfigUpdatePayload;
|
|
||||||
if (!configUpdate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
config.patch(configUpdate.feature, configUpdate.action, configUpdate.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
connect,
|
|
||||||
reconnect,
|
|
||||||
disconnect,
|
|
||||||
on,
|
|
||||||
off,
|
|
||||||
emit,
|
|
||||||
isConnected,
|
|
||||||
getSessionId,
|
|
||||||
connectionStatus: readonly(connectionStatus) as Readonly<Ref<connectionStatus>>,
|
|
||||||
error: readonly(error) as Readonly<Ref<string | null>>,
|
|
||||||
error_count: readonly(error_count) as Readonly<Ref<number>>,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
@ -1,531 +0,0 @@
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
import type { item_request } from '~/types/item';
|
|
||||||
import type { StoreItem } from '~/types/store';
|
|
||||||
import { request } from '~/utils';
|
|
||||||
|
|
||||||
type StateType = 'queue' | 'history';
|
|
||||||
type KeyType = string;
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
queue: Record<KeyType, StoreItem>;
|
|
||||||
history: Record<KeyType, StoreItem>;
|
|
||||||
pagination: {
|
|
||||||
page: number;
|
|
||||||
per_page: number;
|
|
||||||
total: number;
|
|
||||||
total_pages: number;
|
|
||||||
has_next: boolean;
|
|
||||||
has_prev: boolean;
|
|
||||||
isLoaded: boolean;
|
|
||||||
isLoading: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useStateStore = defineStore('state', () => {
|
|
||||||
const state = reactive<State>({
|
|
||||||
queue: {},
|
|
||||||
history: {},
|
|
||||||
pagination: {
|
|
||||||
page: 1,
|
|
||||||
per_page: 50,
|
|
||||||
total: 0,
|
|
||||||
total_pages: 0,
|
|
||||||
has_next: false,
|
|
||||||
has_prev: false,
|
|
||||||
isLoaded: false,
|
|
||||||
isLoading: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const add = (type: StateType, key: KeyType, value: StoreItem): void => {
|
|
||||||
if ('history' === type && state.pagination.total > 0) {
|
|
||||||
state.pagination.total += 1;
|
|
||||||
}
|
|
||||||
state[type][key] = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const update = (type: StateType, key: KeyType, value: StoreItem): void => {
|
|
||||||
state[type][key] = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const remove = (type: StateType, key: KeyType): void => {
|
|
||||||
if (!state[type][key]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('history' === type && state.pagination.total > 0) {
|
|
||||||
state.pagination.total -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { [key]: _, ...rest } = state[type];
|
|
||||||
state[type] = rest;
|
|
||||||
};
|
|
||||||
|
|
||||||
const get = (
|
|
||||||
type: StateType,
|
|
||||||
key: KeyType,
|
|
||||||
defaultValue: StoreItem | null = null,
|
|
||||||
): StoreItem | null => {
|
|
||||||
return state[type][key] || defaultValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
const has = (type: StateType, key: KeyType): boolean => {
|
|
||||||
return !!state[type][key];
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearAll = (type: StateType): void => {
|
|
||||||
state[type] = {};
|
|
||||||
if ('queue' === type) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.pagination.total = 0;
|
|
||||||
state.pagination.page = 1;
|
|
||||||
state.pagination.total_pages = 0;
|
|
||||||
state.pagination.has_next = false;
|
|
||||||
state.pagination.has_prev = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAll = (type: StateType, data: Record<KeyType, StoreItem>): void => {
|
|
||||||
state[type] = data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const move = (fromType: StateType, toType: StateType, key: KeyType): void => {
|
|
||||||
if (true === has(fromType, key)) {
|
|
||||||
remove(fromType, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
add(toType, key, get(fromType, key, {} as StoreItem) as StoreItem);
|
|
||||||
};
|
|
||||||
|
|
||||||
const count = (type: StateType): number => {
|
|
||||||
if ('history' === type && state.pagination.total > 0) {
|
|
||||||
return state.pagination.total;
|
|
||||||
}
|
|
||||||
return Object.keys(state[type]).length;
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadPaginated = async (
|
|
||||||
type: StateType,
|
|
||||||
page: number = 1,
|
|
||||||
per_page: number = 50,
|
|
||||||
order: 'ASC' | 'DESC' = 'DESC',
|
|
||||||
append: boolean = false,
|
|
||||||
status?: string,
|
|
||||||
): Promise<void> => {
|
|
||||||
if ('history' !== type) {
|
|
||||||
throw new Error('Pagination is only supported for history type');
|
|
||||||
}
|
|
||||||
|
|
||||||
state.pagination.isLoading = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const params: Record<string, string> = {
|
|
||||||
type: 'done',
|
|
||||||
page: page.toString(),
|
|
||||||
per_page: per_page.toString(),
|
|
||||||
order,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
params.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = new URLSearchParams(params);
|
|
||||||
|
|
||||||
const response = await request(`/api/history?${search}`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.pagination) {
|
|
||||||
state.pagination = { ...data.pagination, isLoaded: true, isLoading: false };
|
|
||||||
const items: Record<KeyType, StoreItem> = {};
|
|
||||||
for (const item of data.items || []) {
|
|
||||||
items[item._id] = item;
|
|
||||||
}
|
|
||||||
|
|
||||||
state[type] = append ? { ...state[type], ...items } : items;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load ${type} page ${page}:`, error);
|
|
||||||
state.pagination.isLoading = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadNextPage = async (type: StateType, append: boolean = false): Promise<void> => {
|
|
||||||
if ('history' !== type) {
|
|
||||||
throw new Error('Pagination is only supported for history type');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.pagination.has_next || state.pagination.isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadPaginated(type, state.pagination.page + 1, state.pagination.per_page, 'DESC', append);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadPreviousPage = async (type: StateType): Promise<void> => {
|
|
||||||
if ('history' !== type) {
|
|
||||||
throw new Error('Pagination is only supported for history type');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.pagination.has_prev || state.pagination.isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadPaginated(type, state.pagination.page - 1, state.pagination.per_page);
|
|
||||||
};
|
|
||||||
|
|
||||||
const reloadCurrentPage = async (type: StateType): Promise<void> => {
|
|
||||||
if ('history' !== type) {
|
|
||||||
throw new Error('Pagination is only supported for history type');
|
|
||||||
}
|
|
||||||
if (!state.pagination.isLoaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadPaginated(type, state.pagination.page, state.pagination.per_page);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPagination = () => state.pagination;
|
|
||||||
|
|
||||||
const setHistoryCount = (count: number) => {
|
|
||||||
state.pagination.total = count;
|
|
||||||
if (count > 0 && !state.pagination.isLoaded) {
|
|
||||||
state.pagination.isLoaded = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load queue data from REST API.
|
|
||||||
* Uses the /live endpoint to get real-time in-memory data with live progress.
|
|
||||||
*
|
|
||||||
* @returns Promise that resolves when queue is loaded
|
|
||||||
*/
|
|
||||||
const loadQueue = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const response = await request('/api/history/live');
|
|
||||||
const data = (await response.json()) as {
|
|
||||||
queue: Record<KeyType, StoreItem>;
|
|
||||||
history_count: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
state.queue = data.queue || {};
|
|
||||||
setHistoryCount(data.history_count);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load queue:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a download using WebSocket if connected, fallback to REST API.
|
|
||||||
*
|
|
||||||
* @param data - Download data (url, preset, folder, etc.)
|
|
||||||
* @returns Promise that resolves when download is added
|
|
||||||
*/
|
|
||||||
const addDownload = async (data: item_request): Promise<void> => {
|
|
||||||
const socket = useSocketStore();
|
|
||||||
const toast = useNotification();
|
|
||||||
|
|
||||||
if (socket.isConnected) {
|
|
||||||
socket.emit('add_url', data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await request('/api/history/', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json();
|
|
||||||
toast.error(error.error || 'Failed to add download');
|
|
||||||
throw new Error(error.error || 'Failed to add download');
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success('Download added successfully');
|
|
||||||
await loadQueue();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to add download:', error);
|
|
||||||
if (error instanceof Error && !error.message.includes('Failed to add download')) {
|
|
||||||
toast.error('Failed to add download');
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start one or more downloads using WebSocket if connected, fallback to REST API.
|
|
||||||
*
|
|
||||||
* @param ids - Array of item IDs to start
|
|
||||||
* @returns Promise that resolves when items are started
|
|
||||||
*/
|
|
||||||
const startItems = async (ids: string[]): Promise<void> => {
|
|
||||||
const socket = useSocketStore();
|
|
||||||
const toast = useNotification();
|
|
||||||
|
|
||||||
if (socket.isConnected) {
|
|
||||||
ids.forEach((id) => socket.emit('item_start', id));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await request('/api/history/start', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ ids }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json();
|
|
||||||
toast.error(error.error || 'Failed to start items');
|
|
||||||
throw new Error(error.error || 'Failed to start items');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
for (const id of ids) {
|
|
||||||
if ('started' === result[id]) {
|
|
||||||
const item = get('queue', id);
|
|
||||||
if (item) {
|
|
||||||
update('queue', id, { ...item, auto_start: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success(`Started ${ids.length} item${1 === ids.length ? '' : 's'}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to start items:', error);
|
|
||||||
if (error instanceof Error && !error.message.includes('Failed to start items')) {
|
|
||||||
toast.error('Failed to start items');
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pause one or more downloads using WebSocket if connected, fallback to REST API.
|
|
||||||
*
|
|
||||||
* @param ids - Array of item IDs to pause
|
|
||||||
* @returns Promise that resolves when items are paused
|
|
||||||
*/
|
|
||||||
const pauseItems = async (ids: string[]): Promise<void> => {
|
|
||||||
const socket = useSocketStore();
|
|
||||||
const toast = useNotification();
|
|
||||||
|
|
||||||
if (socket.isConnected) {
|
|
||||||
ids.forEach((id) => socket.emit('item_pause', id));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await request('/api/history/pause', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ ids }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json();
|
|
||||||
toast.error(error.error || 'Failed to pause items');
|
|
||||||
throw new Error(error.error || 'Failed to pause items');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
for (const id of ids) {
|
|
||||||
if ('paused' === result[id]) {
|
|
||||||
const item = get('queue', id);
|
|
||||||
if (item) {
|
|
||||||
update('queue', id, { ...item, auto_start: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success(`Paused ${ids.length} item${1 === ids.length ? '' : 's'}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to pause items:', error);
|
|
||||||
if (error instanceof Error && !error.message.includes('Failed to pause items')) {
|
|
||||||
toast.error('Failed to pause items');
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel one or more downloads using WebSocket if connected, fallback to REST API.
|
|
||||||
*
|
|
||||||
* @param ids - Array of item IDs to cancel
|
|
||||||
* @returns Promise that resolves when items are cancelled
|
|
||||||
*/
|
|
||||||
const cancelItems = async (ids: string[]): Promise<void> => {
|
|
||||||
const socket = useSocketStore();
|
|
||||||
const toast = useNotification();
|
|
||||||
|
|
||||||
if (socket.isConnected) {
|
|
||||||
ids.forEach((id) => socket.emit('item_cancel', id));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsToMove: Record<string, StoreItem> = {};
|
|
||||||
for (const id of ids) {
|
|
||||||
const item = get('queue', id);
|
|
||||||
if (item) {
|
|
||||||
itemsToMove[id] = { ...item };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await request('/api/history/cancel', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ ids }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json();
|
|
||||||
toast.error(error.error || 'Failed to cancel items');
|
|
||||||
throw new Error(error.error || 'Failed to cancel items');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
for (const id of ids) {
|
|
||||||
if ('ok' === result[id] && itemsToMove[id]) {
|
|
||||||
remove('queue', id);
|
|
||||||
const cancelledItem = { ...itemsToMove[id], status: 'cancelled' } as StoreItem;
|
|
||||||
add('history', id, cancelledItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success(`Cancelled ${ids.length} item${1 === ids.length ? '' : 's'}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to cancel items:', error);
|
|
||||||
if (error instanceof Error && !error.message.includes('Failed to cancel items')) {
|
|
||||||
toast.error('Failed to cancel items');
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove items using WebSocket if connected, fallback to REST API.
|
|
||||||
*
|
|
||||||
* @param type - The store type ('queue' or 'history')
|
|
||||||
* @param ids - Array of item IDs to remove
|
|
||||||
* @param removeFile - Whether to remove files from disk (default: false)
|
|
||||||
* @returns Promise that resolves when items are removed
|
|
||||||
*/
|
|
||||||
const removeItems = async (
|
|
||||||
type: StateType,
|
|
||||||
ids: string[],
|
|
||||||
removeFile: boolean = false,
|
|
||||||
): Promise<void> => {
|
|
||||||
const socket = useSocketStore();
|
|
||||||
const toast = useNotification();
|
|
||||||
|
|
||||||
if (socket.isConnected) {
|
|
||||||
ids.forEach((id) => socket.emit('item_delete', { id, remove_file: removeFile }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteItems(type, { ids, removeFile });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to remove items:', error);
|
|
||||||
toast.error('Failed to remove items');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete items by specific IDs or status filter.
|
|
||||||
*
|
|
||||||
* @param type - The store type ('queue' or 'history')
|
|
||||||
* @param options - Delete options
|
|
||||||
* @param options.ids - Array of item IDs to delete (if provided, status is ignored)
|
|
||||||
* @param options.status - Status filter (e.g., 'finished' or '!finished')
|
|
||||||
* @param options.removeFile - Whether to remove files from disk (default: true)
|
|
||||||
*
|
|
||||||
* @returns Number of items deleted
|
|
||||||
*/
|
|
||||||
const deleteItems = async (
|
|
||||||
type: StateType,
|
|
||||||
options: { ids?: string[]; status?: string; removeFile?: boolean } = {},
|
|
||||||
): Promise<number> => {
|
|
||||||
const { ids, status, removeFile = true } = options;
|
|
||||||
|
|
||||||
if (!ids && !status) {
|
|
||||||
throw new Error('Either ids or status filter must be provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body: Record<string, unknown> = {
|
|
||||||
type: type === 'queue' ? 'queue' : 'done',
|
|
||||||
remove_file: removeFile,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ids) {
|
|
||||||
body.ids = ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
body.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await request('/api/history', {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = (await response.json()) as {
|
|
||||||
items: Record<string, string>;
|
|
||||||
deleted: number;
|
|
||||||
error?: string;
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (result.error || result.message || !response.ok) {
|
|
||||||
throw new Error(result.error || result.message || 'Failed to delete items.');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const id of Object.keys(result.items)) {
|
|
||||||
remove(type, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.deleted;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to delete items:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
add,
|
|
||||||
update,
|
|
||||||
remove,
|
|
||||||
get,
|
|
||||||
has,
|
|
||||||
clearAll,
|
|
||||||
addAll,
|
|
||||||
move,
|
|
||||||
count,
|
|
||||||
loadPaginated,
|
|
||||||
loadNextPage,
|
|
||||||
loadPreviousPage,
|
|
||||||
reloadCurrentPage,
|
|
||||||
getPagination,
|
|
||||||
setHistoryCount,
|
|
||||||
loadQueue,
|
|
||||||
addDownload,
|
|
||||||
startItems,
|
|
||||||
pauseItems,
|
|
||||||
cancelItems,
|
|
||||||
removeItems,
|
|
||||||
deleteItems,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
@ -142,45 +142,6 @@ const dEvent = (eventName: string, detail: Record<string, any> = {}): boolean =>
|
||||||
return window.dispatchEvent(new CustomEvent(eventName, { detail }));
|
return window.dispatchEvent(new CustomEvent(eventName, { detail }));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a pagination list based on current page, total pages, and delta range.
|
|
||||||
*
|
|
||||||
* @param current - The current active page number.
|
|
||||||
* @param last - The last page number.
|
|
||||||
* @param delta - How many pages to show before/after the current page.
|
|
||||||
* @returns An array of pagination entries including optional gaps.
|
|
||||||
*/
|
|
||||||
const makePagination = (
|
|
||||||
current: number,
|
|
||||||
last: number,
|
|
||||||
delta: number = 5,
|
|
||||||
): Array<{ page: number; text: string; selected: boolean }> => {
|
|
||||||
const pagination: Array<{ page: number; text: string; selected: boolean }> = [];
|
|
||||||
if (last < 2) {
|
|
||||||
return pagination;
|
|
||||||
}
|
|
||||||
|
|
||||||
const strR = '-'.repeat(9 + `${last}`.length);
|
|
||||||
const left = current - delta;
|
|
||||||
const right = current + delta + 1;
|
|
||||||
|
|
||||||
for (let i = 1; i <= last; i++) {
|
|
||||||
if (1 === i || last === i || (i >= left && i < right)) {
|
|
||||||
if (i === left && i > 2) {
|
|
||||||
pagination.push({ page: 0, text: strR, selected: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
pagination.push({ page: i, text: `Page #${i}`, selected: i === current });
|
|
||||||
|
|
||||||
if (i === right - 1 && i < last - 1) {
|
|
||||||
pagination.push({ page: 0, text: strR, selected: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pagination;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely encode a path string for use in a URL.
|
* Safely encode a path string for use in a URL.
|
||||||
*
|
*
|
||||||
|
|
@ -1027,7 +988,6 @@ export {
|
||||||
r,
|
r,
|
||||||
copyText,
|
copyText,
|
||||||
dEvent,
|
dEvent,
|
||||||
makePagination,
|
|
||||||
encodePath,
|
encodePath,
|
||||||
request,
|
request,
|
||||||
removeANSIColors,
|
removeANSIColors,
|
||||||
|
|
|
||||||
106
ui/bun.lock
106
ui/bun.lock
|
|
@ -5,41 +5,39 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "nuxt-app",
|
"name": "nuxt-app",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify-json/lucide": "latest",
|
"@iconify-json/lucide": "^1.2.102",
|
||||||
"@microsoft/fetch-event-source": "latest",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
"@nuxt/eslint": "latest",
|
"@nuxt/eslint": "^1.15.2",
|
||||||
"@nuxt/eslint-config": "latest",
|
"@nuxt/eslint-config": "^1.15.2",
|
||||||
"@nuxt/ui": "latest",
|
"@nuxt/ui": "^4.6.1",
|
||||||
"@pinia/nuxt": "latest",
|
"@vueuse/core": "^14.2.1",
|
||||||
"@vueuse/core": "latest",
|
"@vueuse/nuxt": "^14.2.1",
|
||||||
"@vueuse/nuxt": "latest",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-fit": "latest",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"@xterm/xterm": "latest",
|
"cron-parser": "^5.5.0",
|
||||||
"cron-parser": "latest",
|
"cronstrue": "^3.14.0",
|
||||||
"cronstrue": "latest",
|
"hls.js": "^1.6.16",
|
||||||
"hls.js": "latest",
|
"marked": "^18.0.0",
|
||||||
"marked": "latest",
|
"marked-alert": "^2.1.2",
|
||||||
"marked-alert": "latest",
|
"marked-base-url": "^1.1.9",
|
||||||
"marked-base-url": "latest",
|
"marked-gfm-heading-id": "^4.1.4",
|
||||||
"marked-gfm-heading-id": "latest",
|
"moment": "^2.30.1",
|
||||||
"moment": "latest",
|
"nuxt": "^4.4.2",
|
||||||
"nuxt": "latest",
|
"tailwindcss": "^4.2.2",
|
||||||
"pinia": "latest",
|
"vue": "^3.5.32",
|
||||||
"tailwindcss": "latest",
|
"vue-router": "^5.0.4",
|
||||||
"vue": "latest",
|
|
||||||
"vue-router": "latest",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "^1.3.12",
|
||||||
"@types/jsdom": "latest",
|
"@types/jsdom": "^28.0.1",
|
||||||
"@types/node": "latest",
|
"@types/node": "25.6.0",
|
||||||
"@typescript-eslint/parser": "latest",
|
"@typescript-eslint/parser": "^8.58.2",
|
||||||
"eslint": "latest",
|
"eslint": "^10.2.0",
|
||||||
"jsdom": "latest",
|
"jsdom": "^29.0.2",
|
||||||
"oxfmt": "latest",
|
"oxfmt": "^0.45.0",
|
||||||
"typescript": "latest",
|
"typescript": "^6.0.2",
|
||||||
"vue-eslint-parser": "latest",
|
"vue-eslint-parser": "^10.4.0",
|
||||||
"vue-tsc": "latest",
|
"vue-tsc": "^3.2.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -512,8 +510,6 @@
|
||||||
|
|
||||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
|
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
|
||||||
|
|
||||||
"@pinia/nuxt": ["@pinia/nuxt@0.11.3", "", { "dependencies": { "@nuxt/kit": "^4.2.0" }, "peerDependencies": { "pinia": "^3.0.4" } }, "sha512-7WVNHpWx4qAEzOlnyrRC88kYrwnlR/PrThWT0XI1dSNyUAXu/KBv9oR37uCgYkZroqP5jn8DfzbkNF3BtKvE9w=="],
|
|
||||||
|
|
||||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||||
|
|
@ -838,7 +834,7 @@
|
||||||
|
|
||||||
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.32", "", { "dependencies": { "@vue/compiler-dom": "3.5.32", "@vue/shared": "3.5.32" } }, "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw=="],
|
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.32", "", { "dependencies": { "@vue/compiler-dom": "3.5.32", "@vue/shared": "3.5.32" } }, "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw=="],
|
||||||
|
|
||||||
"@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
|
"@vue/devtools-api": ["@vue/devtools-api@8.0.6", "", { "dependencies": { "@vue/devtools-kit": "^8.0.6" } }, "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA=="],
|
||||||
|
|
||||||
"@vue/devtools-core": ["@vue/devtools-core@8.1.0", "", { "dependencies": { "@vue/devtools-kit": "^8.1.0", "@vue/devtools-shared": "^8.1.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-LvD1VgDpoHmYL00IgKRLKktF6SsPAb0yaV8wB8q2jRwsAWvqhS8+vsMLEGKNs7uoKyymXhT92dhxgf/wir6YGQ=="],
|
"@vue/devtools-core": ["@vue/devtools-core@8.1.0", "", { "dependencies": { "@vue/devtools-kit": "^8.1.0", "@vue/devtools-shared": "^8.1.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-LvD1VgDpoHmYL00IgKRLKktF6SsPAb0yaV8wB8q2jRwsAWvqhS8+vsMLEGKNs7uoKyymXhT92dhxgf/wir6YGQ=="],
|
||||||
|
|
||||||
|
|
@ -2262,7 +2258,7 @@
|
||||||
|
|
||||||
"@vue/compiler-ssr/@vue/shared": ["@vue/shared@3.5.32", "", {}, "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg=="],
|
"@vue/compiler-ssr/@vue/shared": ["@vue/shared@3.5.32", "", {}, "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg=="],
|
||||||
|
|
||||||
"@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="],
|
"@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@8.0.6", "", { "dependencies": { "@vue/devtools-shared": "^8.0.6", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^2.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw=="],
|
||||||
|
|
||||||
"@vue/devtools-kit/birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
"@vue/devtools-kit/birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
||||||
|
|
||||||
|
|
@ -2386,6 +2382,8 @@
|
||||||
|
|
||||||
"path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
"path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||||
|
|
||||||
|
"pinia/@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
|
||||||
|
|
||||||
"readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
"readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
||||||
|
|
||||||
"rollup-plugin-visualizer/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
"rollup-plugin-visualizer/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
||||||
|
|
@ -2448,8 +2446,6 @@
|
||||||
|
|
||||||
"vue-eslint-parser/espree": ["espree@11.1.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw=="],
|
"vue-eslint-parser/espree": ["espree@11.1.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw=="],
|
||||||
|
|
||||||
"vue-router/@vue/devtools-api": ["@vue/devtools-api@8.0.6", "", { "dependencies": { "@vue/devtools-kit": "^8.0.6" } }, "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA=="],
|
|
||||||
|
|
||||||
"vue-router/mlly": ["mlly@1.8.1", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ=="],
|
"vue-router/mlly": ["mlly@1.8.1", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ=="],
|
||||||
|
|
||||||
"whatwg-url/@exodus/bytes": ["@exodus/bytes@1.14.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ=="],
|
"whatwg-url/@exodus/bytes": ["@exodus/bytes@1.14.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ=="],
|
||||||
|
|
@ -2580,14 +2576,12 @@
|
||||||
|
|
||||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
"@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
|
"@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@8.0.6", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg=="],
|
||||||
|
|
||||||
"@vue/devtools-api/@vue/devtools-kit/birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
"@vue/devtools-api/@vue/devtools-kit/birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
||||||
|
|
||||||
"@vue/devtools-api/@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
"@vue/devtools-api/@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||||
|
|
||||||
"@vue/devtools-api/@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
|
||||||
|
|
||||||
"@vue/language-core/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.30", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw=="],
|
"@vue/language-core/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.30", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw=="],
|
||||||
|
|
||||||
"archiver-utils/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
"archiver-utils/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||||
|
|
@ -2628,10 +2622,10 @@
|
||||||
|
|
||||||
"nuxt/vue/@vue/server-renderer": ["@vue/server-renderer@3.5.30", "", { "dependencies": { "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30" }, "peerDependencies": { "vue": "3.5.30" } }, "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ=="],
|
"nuxt/vue/@vue/server-renderer": ["@vue/server-renderer@3.5.30", "", { "dependencies": { "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30" }, "peerDependencies": { "vue": "3.5.30" } }, "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ=="],
|
||||||
|
|
||||||
"nuxt/vue-router/@vue/devtools-api": ["@vue/devtools-api@8.0.6", "", { "dependencies": { "@vue/devtools-kit": "^8.0.6" } }, "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA=="],
|
|
||||||
|
|
||||||
"nuxt/vue-router/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
|
"nuxt/vue-router/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
|
||||||
|
|
||||||
|
"pinia/@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="],
|
||||||
|
|
||||||
"readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
"readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
"rollup-plugin-visualizer/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
"rollup-plugin-visualizer/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
||||||
|
|
@ -2666,8 +2660,6 @@
|
||||||
|
|
||||||
"vue-eslint-parser/espree/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"vue-eslint-parser/espree/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
"vue-router/@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@8.0.6", "", { "dependencies": { "@vue/devtools-shared": "^8.0.6", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^2.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw=="],
|
|
||||||
|
|
||||||
"vue-router/mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
"vue-router/mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|
||||||
"@dxup/nuxt/@nuxt/kit/mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
"@dxup/nuxt/@nuxt/kit/mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|
@ -2742,8 +2734,6 @@
|
||||||
|
|
||||||
"nuxt/mlly/pkg-types/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
|
"nuxt/mlly/pkg-types/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
|
||||||
|
|
||||||
"nuxt/vue-router/@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@8.0.6", "", { "dependencies": { "@vue/devtools-shared": "^8.0.6", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^2.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw=="],
|
|
||||||
|
|
||||||
"nuxt/vue-router/mlly/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"nuxt/vue-router/mlly/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
"nuxt/vue-router/mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
"nuxt/vue-router/mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|
@ -2764,6 +2754,14 @@
|
||||||
|
|
||||||
"nuxt/vue/@vue/server-renderer/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA=="],
|
"nuxt/vue/@vue/server-renderer/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA=="],
|
||||||
|
|
||||||
|
"pinia/@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
|
||||||
|
|
||||||
|
"pinia/@vue/devtools-api/@vue/devtools-kit/birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
||||||
|
|
||||||
|
"pinia/@vue/devtools-api/@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||||
|
|
||||||
|
"pinia/@vue/devtools-api/@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||||
|
|
||||||
"readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
"unplugin-vue-components/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
"unplugin-vue-components/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||||
|
|
@ -2786,12 +2784,6 @@
|
||||||
|
|
||||||
"vaul-vue/vue/@vue/server-renderer/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA=="],
|
"vaul-vue/vue/@vue/server-renderer/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA=="],
|
||||||
|
|
||||||
"vue-router/@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@8.0.6", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg=="],
|
|
||||||
|
|
||||||
"vue-router/@vue/devtools-api/@vue/devtools-kit/birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
|
||||||
|
|
||||||
"vue-router/@vue/devtools-api/@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
|
||||||
|
|
||||||
"vue-router/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
"vue-router/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||||
|
|
||||||
"vue-router/mlly/pkg-types/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
|
"vue-router/mlly/pkg-types/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
|
||||||
|
|
@ -2848,12 +2840,6 @@
|
||||||
|
|
||||||
"nuxt/mlly/pkg-types/mlly/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"nuxt/mlly/pkg-types/mlly/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
"nuxt/vue-router/@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@8.0.6", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg=="],
|
|
||||||
|
|
||||||
"nuxt/vue-router/@vue/devtools-api/@vue/devtools-kit/birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
|
||||||
|
|
||||||
"nuxt/vue-router/@vue/devtools-api/@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
|
||||||
|
|
||||||
"nuxt/vue-router/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
"nuxt/vue-router/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||||
|
|
||||||
"nuxt/vue/@vue/compiler-dom/@vue/compiler-core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
"nuxt/vue/@vue/compiler-dom/@vue/compiler-core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,12 @@ export default defineNuxtConfig({
|
||||||
},
|
},
|
||||||
pageTransition: { name: 'page' },
|
pageTransition: { name: 'page' },
|
||||||
},
|
},
|
||||||
modules: ['@nuxt/ui', '@pinia/nuxt', '@vueuse/nuxt', '@nuxt/eslint'],
|
modules: ['@nuxt/ui', '@vueuse/nuxt', '@nuxt/eslint'],
|
||||||
icon: {
|
icon: {
|
||||||
provider: 'none',
|
provider: 'none',
|
||||||
fallbackToApi: false,
|
fallbackToApi: false,
|
||||||
clientBundle: {
|
clientBundle: {
|
||||||
|
icons: isProd ? [] : ['lucide:book'],
|
||||||
scan: {
|
scan: {
|
||||||
globInclude: ['app/**/*.{vue,ts,js}', 'node_modules/@nuxt/ui/dist/shared/ui*.mjs'],
|
globInclude: ['app/**/*.{vue,ts,js}', 'node_modules/@nuxt/ui/dist/shared/ui*.mjs'],
|
||||||
globExclude: ['dist', 'build', 'coverage', 'test', 'tests', '.*'],
|
globExclude: ['dist', 'build', 'coverage', 'test', 'tests', '.*'],
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
"@nuxt/eslint": "^1.15.2",
|
"@nuxt/eslint": "^1.15.2",
|
||||||
"@nuxt/eslint-config": "^1.15.2",
|
"@nuxt/eslint-config": "^1.15.2",
|
||||||
"@nuxt/ui": "^4.6.1",
|
"@nuxt/ui": "^4.6.1",
|
||||||
"@pinia/nuxt": "^0.11.3",
|
|
||||||
"@vueuse/core": "^14.2.1",
|
"@vueuse/core": "^14.2.1",
|
||||||
"@vueuse/nuxt": "^14.2.1",
|
"@vueuse/nuxt": "^14.2.1",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
|
@ -40,7 +39,6 @@
|
||||||
"marked-gfm-heading-id": "^4.1.4",
|
"marked-gfm-heading-id": "^4.1.4",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nuxt": "^4.4.2",
|
"nuxt": "^4.4.2",
|
||||||
"pinia": "^3.0.4",
|
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"vue": "^3.5.32",
|
"vue": "^3.5.32",
|
||||||
"vue-router": "^5.0.4"
|
"vue-router": "^5.0.4"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,24 @@
|
||||||
import { describe, expect, it } from 'bun:test'
|
import { beforeAll, describe, expect, it, mock } from 'bun:test'
|
||||||
|
|
||||||
import { usePresetOptions } from '~/composables/usePresetOptions'
|
|
||||||
import type { Preset } from '~/types/presets'
|
import type { Preset } from '~/types/presets'
|
||||||
|
|
||||||
|
let configState = {
|
||||||
|
presets: [] as Preset[],
|
||||||
|
app: {
|
||||||
|
download_path: '/downloads',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.module('~/composables/useYtpConfig', () => ({
|
||||||
|
useYtpConfig: () => configState,
|
||||||
|
}))
|
||||||
|
|
||||||
|
let usePresetOptions: typeof import('~/composables/usePresetOptions').usePresetOptions
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
;({ usePresetOptions } = await import('~/composables/usePresetOptions'))
|
||||||
|
})
|
||||||
|
|
||||||
const buildPreset = (name: string, isDefault: boolean): Preset => ({
|
const buildPreset = (name: string, isDefault: boolean): Preset => ({
|
||||||
name,
|
name,
|
||||||
default: isDefault,
|
default: isDefault,
|
||||||
|
|
@ -15,12 +31,12 @@ const buildPreset = (name: string, isDefault: boolean): Preset => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
const setConfigStore = (presets: Preset[]) => {
|
const setConfigStore = (presets: Preset[]) => {
|
||||||
;(globalThis as any).useConfigStore = () => ({
|
configState = {
|
||||||
presets,
|
presets,
|
||||||
app: {
|
app: {
|
||||||
download_path: '/downloads',
|
download_path: '/downloads',
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('usePresetOptions', () => {
|
describe('usePresetOptions', () => {
|
||||||
|
|
|
||||||
|
|
@ -326,15 +326,6 @@ describe('data conversion helpers', () => {
|
||||||
expect(utils.decode(encoded)).toEqual(payload);
|
expect(utils.decode(encoded)).toEqual(payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('makePagination builds a ranged pagination list', () => {
|
|
||||||
const pages = utils.makePagination(5, 10, 1);
|
|
||||||
const selected = pages.find((page: any) => page.selected);
|
|
||||||
expect(selected?.page).toBe(5);
|
|
||||||
expect(pages.length).toBeGreaterThan(0);
|
|
||||||
expect(pages[0]?.page).toBe(1);
|
|
||||||
expect(pages[pages.length - 1]?.page).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getQueryParams parses query strings', () => {
|
it('getQueryParams parses query strings', () => {
|
||||||
expect(utils.getQueryParams('?a=1&b=two')).toEqual({ a: '1', b: 'two' });
|
expect(utils.getQueryParams('?a=1&b=two')).toEqual({ a: '1', b: 'two' });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue