refactor: seperate history from queue
This commit is contained in:
parent
210712cee2
commit
e7dfee66b1
12 changed files with 4279 additions and 3418 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,9 @@ const props = defineProps<{
|
||||||
unrenderDelay?: number;
|
unrenderDelay?: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const ROOT_MARGIN = 600;
|
||||||
|
const nuxtApp = useNuxtApp();
|
||||||
|
|
||||||
const shouldRender = ref(false);
|
const shouldRender = ref(false);
|
||||||
const targetEl = ref<HTMLElement | null>(null);
|
const targetEl = ref<HTMLElement | null>(null);
|
||||||
const fixedMinHeight = ref(0);
|
const fixedMinHeight = ref(0);
|
||||||
|
|
@ -21,12 +24,35 @@ const fixedMinHeight = ref(0);
|
||||||
let unrenderTimer: ReturnType<typeof setTimeout> | null = null;
|
let unrenderTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let renderTimer: ReturnType<typeof setTimeout> | null = null;
|
let renderTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
function onIdle(cb: () => void): void {
|
function ensureRenderedIfNearViewport(): void {
|
||||||
if ('requestIdleCallback' in window) {
|
if (!targetEl.value) {
|
||||||
(window as any).requestIdleCallback(cb);
|
return;
|
||||||
} else {
|
|
||||||
setTimeout(() => nextTick(cb), 300);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rect = targetEl.value.getBoundingClientRect();
|
||||||
|
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||||
|
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
||||||
|
|
||||||
|
if (
|
||||||
|
rect.bottom < -ROOT_MARGIN ||
|
||||||
|
rect.top > viewportHeight + ROOT_MARGIN ||
|
||||||
|
rect.right < 0 ||
|
||||||
|
rect.left > viewportWidth
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unrenderTimer) {
|
||||||
|
clearTimeout(unrenderTimer);
|
||||||
|
unrenderTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderTimer) {
|
||||||
|
clearTimeout(renderTimer);
|
||||||
|
renderTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRender.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { stop } = useIntersectionObserver(
|
const { stop } = useIntersectionObserver(
|
||||||
|
|
@ -43,7 +69,7 @@ const { stop } = useIntersectionObserver(
|
||||||
props.unrender ? 200 : 0,
|
props.unrender ? 200 : 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
shouldRender.value = true;
|
ensureRenderedIfNearViewport();
|
||||||
|
|
||||||
if (!props.unrender) {
|
if (!props.unrender) {
|
||||||
stop();
|
stop();
|
||||||
|
|
@ -61,19 +87,55 @@ const { stop } = useIntersectionObserver(
|
||||||
}, props.unrenderDelay ?? 6000);
|
}, props.unrenderDelay ?? 6000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ rootMargin: '600px' },
|
{ rootMargin: `${ROOT_MARGIN}px` },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (props.renderOnIdle) {
|
const removePageHooks = [
|
||||||
onIdle(() => {
|
nuxtApp.hook('page:finish', () => {
|
||||||
shouldRender.value = true;
|
requestAnimationFrame(() => {
|
||||||
if (!props.unrender) {
|
ensureRenderedIfNearViewport();
|
||||||
stop();
|
});
|
||||||
}
|
}),
|
||||||
|
nuxtApp.hook('page:transition:finish', () => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
ensureRenderedIfNearViewport();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', ensureRenderedIfNearViewport, { passive: true });
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
ensureRenderedIfNearViewport();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (props.renderOnIdle) {
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
(window as any).requestIdleCallback(() => {
|
||||||
|
shouldRender.value = true;
|
||||||
|
if (!props.unrender) {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
nextTick(() => {
|
||||||
|
shouldRender.value = true;
|
||||||
|
if (!props.unrender) {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
300,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', ensureRenderedIfNearViewport);
|
||||||
|
removePageHooks.forEach((removeHook) => removeHook());
|
||||||
|
|
||||||
if (renderTimer) {
|
if (renderTimer) {
|
||||||
clearTimeout(renderTimer);
|
clearTimeout(renderTimer);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<UButton
|
|
||||||
v-if="page !== 1"
|
|
||||||
rel="first"
|
|
||||||
aria-label="Go to first page"
|
|
||||||
color="neutral"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
icon="i-lucide-chevrons-left"
|
|
||||||
:disabled="isLoading"
|
|
||||||
:loading="isLoading"
|
|
||||||
square
|
|
||||||
@click="changePage(1)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
v-if="page > 1 && page - 1 !== 1"
|
|
||||||
rel="prev"
|
|
||||||
aria-label="Go to previous page"
|
|
||||||
color="neutral"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
icon="i-lucide-chevron-left"
|
|
||||||
:disabled="isLoading"
|
|
||||||
:loading="isLoading"
|
|
||||||
square
|
|
||||||
@click="changePage(page - 1)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<USelect
|
|
||||||
id="pager_list"
|
|
||||||
v-model="currentPage"
|
|
||||||
:items="paginationItems"
|
|
||||||
value-key="page"
|
|
||||||
label-key="text"
|
|
||||||
color="neutral"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
class="min-w-52"
|
|
||||||
:disabled="isLoading"
|
|
||||||
:ui="{ base: 'w-full' }"
|
|
||||||
@update:model-value="changePage"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
v-if="page !== last_page && page + 1 !== last_page"
|
|
||||||
rel="next"
|
|
||||||
aria-label="Go to next page"
|
|
||||||
color="neutral"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
icon="i-lucide-chevron-right"
|
|
||||||
:disabled="isLoading"
|
|
||||||
:loading="isLoading"
|
|
||||||
square
|
|
||||||
@click="changePage(page + 1)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
v-if="page !== last_page"
|
|
||||||
rel="last"
|
|
||||||
aria-label="Go to last page"
|
|
||||||
color="neutral"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
icon="i-lucide-chevrons-right"
|
|
||||||
:disabled="isLoading"
|
|
||||||
:loading="isLoading"
|
|
||||||
square
|
|
||||||
@click="changePage(last_page)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { makePagination } from '~/utils/index';
|
|
||||||
|
|
||||||
const emitter = defineEmits<{
|
|
||||||
(e: 'navigate', page: number): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
page: number;
|
|
||||||
last_page: number;
|
|
||||||
isLoading?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const currentPage = ref(props.page);
|
|
||||||
|
|
||||||
const paginationItems = computed(() =>
|
|
||||||
makePagination(props.page, props.last_page).map((item) => ({
|
|
||||||
...item,
|
|
||||||
disabled: item.page === 0,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.page,
|
|
||||||
(value) => {
|
|
||||||
currentPage.value = value;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const changePage = (page: number | string): void => {
|
|
||||||
const nextPage = Number(page);
|
|
||||||
if (Number.isNaN(nextPage) || nextPage < 1 || nextPage > props.last_page) {
|
|
||||||
currentPage.value = props.page;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emitter('navigate', nextPage);
|
|
||||||
currentPage.value = nextPage;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
184
ui/app/composables/useHistoryState.ts
Normal file
184
ui/app/composables/useHistoryState.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useNotification } from '~/composables/useNotification';
|
||||||
|
import { useYtpConfig } from '~/composables/useYtpConfig';
|
||||||
|
import { parse_api_error, parse_list_response, request } from '~/utils';
|
||||||
|
import type { Pagination } from '~/types/responses';
|
||||||
|
import type { StoreItem } from '~/types/store';
|
||||||
|
|
||||||
|
type HistoryLoadOptions = {
|
||||||
|
order?: 'ASC' | 'DESC';
|
||||||
|
status?: string;
|
||||||
|
perPage?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const items = ref<StoreItem[]>([]);
|
||||||
|
const pagination = ref<Pagination>({
|
||||||
|
page: 1,
|
||||||
|
per_page: 50,
|
||||||
|
total: 0,
|
||||||
|
total_pages: 0,
|
||||||
|
has_next: false,
|
||||||
|
has_prev: false,
|
||||||
|
});
|
||||||
|
const isLoading = ref<boolean>(false);
|
||||||
|
const isLoaded = ref<boolean>(false);
|
||||||
|
const lastError = ref<string | null>(null);
|
||||||
|
const throwInstead = ref(false);
|
||||||
|
|
||||||
|
const readJson = async (response: Response): Promise<unknown> => {
|
||||||
|
try {
|
||||||
|
const clone = response.clone();
|
||||||
|
return await clone.json();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureSuccess = async (response: Response): Promise<void> => {
|
||||||
|
if (response.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await readJson(response);
|
||||||
|
const message = await parse_api_error(payload);
|
||||||
|
throw new Error(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = (error: unknown): void => {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unexpected error occurred.';
|
||||||
|
lastError.value = message;
|
||||||
|
useNotification().error(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageSize = computed<number>(() => {
|
||||||
|
const config = useYtpConfig();
|
||||||
|
return Number(config.app.default_pagination || 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildQuery = (
|
||||||
|
page: number,
|
||||||
|
perPage: number,
|
||||||
|
order: 'ASC' | 'DESC' = 'DESC',
|
||||||
|
status?: string,
|
||||||
|
): string => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
type: 'done',
|
||||||
|
page: String(page),
|
||||||
|
per_page: String(perPage),
|
||||||
|
order,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
params.set('status', status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadHistory = async (page: number = 1, options: HistoryLoadOptions = {}): Promise<void> => {
|
||||||
|
const { order = 'DESC', status, perPage = pageSize.value } = options;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request(`/api/history?${buildQuery(page, perPage, order, status)}`);
|
||||||
|
await ensureSuccess(response);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const { items: loadedItems, pagination: paginationData } =
|
||||||
|
await parse_list_response<StoreItem>(data);
|
||||||
|
|
||||||
|
items.value = loadedItems;
|
||||||
|
pagination.value = paginationData;
|
||||||
|
isLoaded.value = true;
|
||||||
|
lastError.value = null;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
if (throwInstead.value) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadHistory = async (options: HistoryLoadOptions = {}): Promise<void> => {
|
||||||
|
const targetPage = isLoaded.value ? pagination.value.page : 1;
|
||||||
|
await loadHistory(targetPage, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteHistoryItems = async (
|
||||||
|
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 response = await request('/api/history', {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: 'done',
|
||||||
|
ids,
|
||||||
|
status,
|
||||||
|
remove_file: removeFile,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await ensureSuccess(response);
|
||||||
|
|
||||||
|
const result = (await response.json()) as {
|
||||||
|
deleted?: number;
|
||||||
|
error?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.error || result.message) {
|
||||||
|
throw new Error(result.error || result.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(result.deleted ?? 0);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
if (throwInstead.value) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetHistory = (): void => {
|
||||||
|
items.value = [];
|
||||||
|
pagination.value = {
|
||||||
|
page: 1,
|
||||||
|
per_page: pageSize.value,
|
||||||
|
total: 0,
|
||||||
|
total_pages: 0,
|
||||||
|
has_next: false,
|
||||||
|
has_prev: false,
|
||||||
|
};
|
||||||
|
isLoaded.value = false;
|
||||||
|
lastError.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHistoryState = () => {
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
pagination,
|
||||||
|
isLoading,
|
||||||
|
isLoaded,
|
||||||
|
lastError,
|
||||||
|
pageSize,
|
||||||
|
loadHistory,
|
||||||
|
reloadHistory,
|
||||||
|
deleteHistoryItems,
|
||||||
|
resetHistory,
|
||||||
|
};
|
||||||
|
};
|
||||||
243
ui/app/composables/useQueueState.ts
Normal file
243
ui/app/composables/useQueueState.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
import { proxyRefs, reactive, toRefs } from 'vue';
|
||||||
|
import type { item_request } from '~/types/item';
|
||||||
|
import type { StoreItem } from '~/types/store';
|
||||||
|
import { request } from '~/utils';
|
||||||
|
|
||||||
|
type KeyType = string;
|
||||||
|
|
||||||
|
interface QueueState {
|
||||||
|
queue: Record<KeyType, StoreItem>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = reactive<QueueState>({
|
||||||
|
queue: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const add = (key: KeyType, value: StoreItem): void => {
|
||||||
|
state.queue[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = (key: KeyType, value: StoreItem): void => {
|
||||||
|
state.queue[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = (key: KeyType): void => {
|
||||||
|
if (!state.queue[key]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { [key]: _, ...rest } = state.queue;
|
||||||
|
state.queue = rest;
|
||||||
|
};
|
||||||
|
|
||||||
|
const get = (key: KeyType, defaultValue: StoreItem | null = null): StoreItem | null => {
|
||||||
|
return state.queue[key] || defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const has = (key: KeyType): boolean => {
|
||||||
|
return !!state.queue[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAll = (): void => {
|
||||||
|
state.queue = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAll = (data: Record<KeyType, StoreItem>): void => {
|
||||||
|
state.queue = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const count = (): number => {
|
||||||
|
return Object.keys(state.queue).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadQueue = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const response = await request('/api/history/live');
|
||||||
|
const data = (await response.json()) as {
|
||||||
|
queue: Record<KeyType, StoreItem>;
|
||||||
|
};
|
||||||
|
|
||||||
|
state.queue = data.queue || {};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load queue:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDownload = async (data: item_request): Promise<void> => {
|
||||||
|
const socket = useAppSocket();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startItems = async (ids: string[]): Promise<void> => {
|
||||||
|
const socket = useAppSocket();
|
||||||
|
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(id);
|
||||||
|
if (item) {
|
||||||
|
update(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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pauseItems = async (ids: string[]): Promise<void> => {
|
||||||
|
const socket = useAppSocket();
|
||||||
|
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(id);
|
||||||
|
if (item) {
|
||||||
|
update(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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelItems = async (ids: string[]): Promise<void> => {
|
||||||
|
const socket = useAppSocket();
|
||||||
|
const toast = useNotification();
|
||||||
|
|
||||||
|
if (socket.isConnected) {
|
||||||
|
ids.forEach((id) => socket.emit('item_cancel', id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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]) {
|
||||||
|
remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const queueStateApi = proxyRefs({
|
||||||
|
...toRefs(state),
|
||||||
|
add,
|
||||||
|
update,
|
||||||
|
remove,
|
||||||
|
get,
|
||||||
|
has,
|
||||||
|
clearAll,
|
||||||
|
addAll,
|
||||||
|
count,
|
||||||
|
loadQueue,
|
||||||
|
addDownload,
|
||||||
|
startItems,
|
||||||
|
pauseItems,
|
||||||
|
cancelItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useQueueState = () => queueStateApi;
|
||||||
|
|
@ -29,14 +29,14 @@
|
||||||
variant="link"
|
variant="link"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="px-0"
|
class="px-0"
|
||||||
@click="() => socket.reconnect()"
|
@click="socket.reconnect()"
|
||||||
>
|
>
|
||||||
Reconnect
|
Reconnect
|
||||||
</UButton>
|
</UButton>
|
||||||
</template>
|
</template>
|
||||||
</UAlert>
|
</UAlert>
|
||||||
|
|
||||||
<Simple @show_settings="() => (show_settings = true)" />
|
<Simple @show_settings="show_settings = true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else key="regular" class="shell-stage flex flex-col">
|
<div v-else key="regular" class="shell-stage flex flex-col">
|
||||||
|
|
@ -174,7 +174,7 @@
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
icon="i-lucide-refresh-cw"
|
icon="i-lucide-refresh-cw"
|
||||||
@click="reloadPage"
|
@click="$router.go(0)"
|
||||||
>
|
>
|
||||||
<span class="hidden xl:inline">Reload</span>
|
<span class="hidden xl:inline">Reload</span>
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -186,10 +186,12 @@
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
:icon="colorModeButtonIcon"
|
:icon="colorModeButtonIcon"
|
||||||
:aria-label="colorModeButtonAriaLabel"
|
:aria-label="colorModeButtonTitle"
|
||||||
:title="colorModeButtonTitle"
|
:title="colorModeButtonTitle"
|
||||||
@click="cycleColorMode"
|
@click="colorMode.preference = nextColorModePreference"
|
||||||
/>
|
>
|
||||||
|
<span class="hidden xl:inline">{{ colorModeButtonTitle }}</span>
|
||||||
|
</UButton>
|
||||||
|
|
||||||
<UButton
|
<UButton
|
||||||
color="neutral"
|
color="neutral"
|
||||||
|
|
@ -256,7 +258,7 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="font-semibold text-highlighted underline-offset-2 hover:underline"
|
class="font-semibold text-highlighted underline-offset-2 hover:underline"
|
||||||
@click="reloadPage"
|
@click="$router.go(0)"
|
||||||
>
|
>
|
||||||
click here
|
click here
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -269,7 +271,7 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="font-semibold text-highlighted underline-offset-2 hover:underline"
|
class="font-semibold text-highlighted underline-offset-2 hover:underline"
|
||||||
@click="() => config.loadConfig(true)"
|
@click="config.loadConfig(true)"
|
||||||
>
|
>
|
||||||
reload configuration
|
reload configuration
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -277,7 +279,7 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="font-semibold text-highlighted underline-offset-2 hover:underline"
|
class="font-semibold text-highlighted underline-offset-2 hover:underline"
|
||||||
@click="reloadPage"
|
@click="$router.go(0)"
|
||||||
>
|
>
|
||||||
reload the page
|
reload the page
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -326,7 +328,7 @@
|
||||||
variant="link"
|
variant="link"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="px-0"
|
class="px-0"
|
||||||
@click="() => socket.reconnect()"
|
@click="socket.reconnect()"
|
||||||
>
|
>
|
||||||
Reconnect
|
Reconnect
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -337,7 +339,7 @@
|
||||||
v-if="config.is_loaded"
|
v-if="config.is_loaded"
|
||||||
class="flex min-h-0 min-w-0 max-w-full flex-1 flex-col"
|
class="flex min-h-0 min-w-0 max-w-full flex-1 flex-col"
|
||||||
>
|
>
|
||||||
<NuxtPage :isLoading="loadingImage" @reload_bg="() => loadImage(true)" />
|
<NuxtPage :isLoading="loadingImage" @reload_bg="loadImage(true)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -481,8 +483,8 @@
|
||||||
<SettingsPanel
|
<SettingsPanel
|
||||||
:isOpen="show_settings"
|
:isOpen="show_settings"
|
||||||
:isLoading="loadingImage"
|
:isLoading="loadingImage"
|
||||||
@close="closeSettings()"
|
@close="show_settings = false"
|
||||||
@reload_bg="() => loadImage(true)"
|
@reload_bg="loadImage(true)"
|
||||||
direction="right"
|
direction="right"
|
||||||
/>
|
/>
|
||||||
</UApp>
|
</UApp>
|
||||||
|
|
@ -576,29 +578,14 @@ const nextColorModePreference = computed<ColorModePreference>(() => {
|
||||||
const colorModeButtonTitle = computed(() => {
|
const colorModeButtonTitle = computed(() => {
|
||||||
switch (colorModePreference.value) {
|
switch (colorModePreference.value) {
|
||||||
case 'light':
|
case 'light':
|
||||||
return 'Theme: Light';
|
return 'Light';
|
||||||
case 'dark':
|
case 'dark':
|
||||||
return 'Theme: Dark';
|
return 'Dark';
|
||||||
default:
|
default:
|
||||||
return 'Theme: System';
|
return 'System';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const colorModeButtonAriaLabel = computed(() => {
|
|
||||||
switch (nextColorModePreference.value) {
|
|
||||||
case 'light':
|
|
||||||
return 'Switch theme to light';
|
|
||||||
case 'dark':
|
|
||||||
return 'Switch theme to dark';
|
|
||||||
default:
|
|
||||||
return 'Switch theme to system';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cycleColorMode = (): void => {
|
|
||||||
colorMode.preference = nextColorModePreference.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetSwipe = (): void => {
|
const resetSwipe = (): void => {
|
||||||
SwipeState.mode = null;
|
SwipeState.mode = null;
|
||||||
SwipeState.tracking = false;
|
SwipeState.tracking = false;
|
||||||
|
|
@ -1091,8 +1078,6 @@ const useVersionUpdate = () => {
|
||||||
|
|
||||||
const { newVersionIsAvailable } = useVersionUpdate();
|
const { newVersionIsAvailable } = useVersionUpdate();
|
||||||
|
|
||||||
const closeSettings = () => (show_settings.value = false);
|
|
||||||
|
|
||||||
const shutdownApp = async () => {
|
const shutdownApp = async () => {
|
||||||
if (false === config.app.is_native) {
|
if (false === config.app.is_native) {
|
||||||
await alertDialog({
|
await alertDialog({
|
||||||
|
|
@ -1131,8 +1116,6 @@ const shutdownApp = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const reloadPage = () => window.location.reload();
|
|
||||||
|
|
||||||
const connectionStatusColor = computed(() => {
|
const connectionStatusColor = computed(() => {
|
||||||
switch (socket.connectionStatus) {
|
switch (socket.connectionStatus) {
|
||||||
case 'connected':
|
case 'connected':
|
||||||
|
|
@ -1177,6 +1160,8 @@ const toasterConfig = computed(() => ({
|
||||||
expand: true,
|
expand: true,
|
||||||
progress: true,
|
progress: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const reloadPage = () => window.location.reload();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
1561
ui/app/pages/history.vue
Normal file
1561
ui/app/pages/history.vue
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,138 +1,206 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-n-head-ssr="" data-n-head-ssr-body="">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>YTPTube Loading...</title>
|
<meta name="theme-color" content="#0f172a" />
|
||||||
|
<title>Loading YTPTube</title>
|
||||||
<style>
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
--bg: #edf2f9;
|
||||||
|
--bg-deep: #dbe5f1;
|
||||||
|
--panel: rgb(255 255 255 / 0.84);
|
||||||
|
--border: rgb(15 23 42 / 0.1);
|
||||||
|
--shadow: rgb(15 23 42 / 0.14);
|
||||||
|
--text: #0f172a;
|
||||||
|
--muted: #5b6b80;
|
||||||
|
--accent: #dc2626;
|
||||||
|
--accent-soft: rgb(220 38 38 / 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #08111d;
|
||||||
|
--bg-deep: #0f172a;
|
||||||
|
--panel: rgb(15 23 42 / 0.84);
|
||||||
|
--border: rgb(148 163 184 / 0.16);
|
||||||
|
--shadow: rgb(2 6 23 / 0.45);
|
||||||
|
--text: #e7eef9;
|
||||||
|
--muted: #9fb0c7;
|
||||||
|
--accent: #f87171;
|
||||||
|
--accent-soft: rgb(248 113 113 / 0.18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
min-height: 100vh;
|
||||||
height: 100%;
|
min-height: 100dvh;
|
||||||
display: flex;
|
padding: 1rem;
|
||||||
align-items: center;
|
display: grid;
|
||||||
justify-content: center;
|
place-items: center;
|
||||||
background-color: #fff;
|
|
||||||
color: #111;
|
|
||||||
user-select: none;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
color: var(--text);
|
||||||
transition:
|
user-select: none;
|
||||||
background-color 0.3s ease,
|
font-family:
|
||||||
color 0.3s ease;
|
system-ui,
|
||||||
}
|
-apple-system,
|
||||||
@media (prefers-color-scheme: dark) {
|
BlinkMacSystemFont,
|
||||||
html,
|
'Segoe UI',
|
||||||
body {
|
sans-serif;
|
||||||
background-color: #121212;
|
background:
|
||||||
color: #eee;
|
radial-gradient(circle at top left, var(--accent-soft), transparent 24%),
|
||||||
}
|
linear-gradient(180deg, var(--bg-deep), var(--bg));
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader-container {
|
body::before {
|
||||||
text-align: center;
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background:
|
||||||
|
linear-gradient(rgb(255 255 255 / 0.035) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgb(255 255 255 / 0.035) 1px, transparent 1px);
|
||||||
|
background-size: 2rem 2rem;
|
||||||
|
opacity: 0.32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-card {
|
||||||
|
width: min(100%, 29rem);
|
||||||
|
position: relative;
|
||||||
|
padding: 1.65rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 1.1rem;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 1rem 2.5rem var(--shadow);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.ytptube-loading {
|
.brand img {
|
||||||
width: 420px;
|
width: 2.8rem;
|
||||||
height: 105px;
|
height: 2.8rem;
|
||||||
stroke: #ff0000;
|
flex: none;
|
||||||
stroke-width: 4.5px;
|
display: block;
|
||||||
stroke-linecap: round;
|
object-fit: contain;
|
||||||
stroke-linejoin: round;
|
|
||||||
fill: none;
|
|
||||||
stroke-dasharray: 700;
|
|
||||||
stroke-dashoffset: 700;
|
|
||||||
animation: stroke-move 3s linear infinite;
|
|
||||||
user-select: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
.brand-copy {
|
||||||
svg.ytptube-loading {
|
min-width: 0;
|
||||||
stroke: #ff4444;
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
font-size: 1.55rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.96rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
margin-top: 1.2rem;
|
||||||
|
height: 0.32rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgb(148 163 184 / 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 36%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
||||||
|
animation: slide 1.35s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin: 0.95rem 0 0;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-130%);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes stroke-move {
|
|
||||||
100% {
|
100% {
|
||||||
stroke-dashoffset: -700;
|
transform: translateX(330%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
@media (max-width: 560px) {
|
||||||
margin: 20px auto 0;
|
.loading-card {
|
||||||
width: 64px;
|
padding: 1.3rem;
|
||||||
height: 64px;
|
}
|
||||||
border: 8px solid transparent;
|
|
||||||
border-top-color: #ff0000;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1.2s linear infinite;
|
|
||||||
filter: drop-shadow(0 0 6px rgba(255, 0, 0, 0.8));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
.brand {
|
||||||
.spinner {
|
align-items: flex-start;
|
||||||
border-top-color: #ff4444;
|
}
|
||||||
filter: drop-shadow(0 0 8px #ff4444);
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.35rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
to {
|
*,
|
||||||
transform: rotate(360deg);
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation: none !important;
|
||||||
|
transition: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
margin-top: 14px;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: inherit;
|
|
||||||
opacity: 0.8;
|
|
||||||
user-select: none;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="loader-container" role="img" aria-label="Loading YTPTube">
|
<main class="loading-card" role="status" aria-live="polite" aria-busy="true">
|
||||||
<svg
|
<div class="brand">
|
||||||
class="ytptube-loading"
|
<img src="/images/favicon.png" alt="" aria-hidden="true" />
|
||||||
viewBox="0 0 420 105"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<div class="brand-copy">
|
||||||
aria-hidden="true"
|
<p class="label">YTPTube</p>
|
||||||
focusable="false"
|
<h1>Loading your dashboard</h1>
|
||||||
>
|
</div>
|
||||||
<!-- Y -->
|
</div>
|
||||||
<path d="M10 10 L25 37.5 L10 65" />
|
|
||||||
<path d="M25 37.5 L40 10" />
|
<p class="description">Getting the interface ready. Please wait a moment...</p>
|
||||||
<!-- T -->
|
|
||||||
<path d="M50 10 L90 10" />
|
<div class="loader" aria-hidden="true"></div>
|
||||||
<path d="M70 10 L70 65" />
|
<p class="hint">Please wait...</p>
|
||||||
<!-- P -->
|
</main>
|
||||||
<path d="M100 65 L100 10 L130 10 Q145 10 145 27.5 Q145 45 130 45 L100 45" />
|
|
||||||
<!-- T -->
|
|
||||||
<path d="M160 10 L200 10" />
|
|
||||||
<path d="M180 10 L180 65" />
|
|
||||||
<!-- U -->
|
|
||||||
<path d="M210 10 L210 50 Q210 65 230 65 Q250 65 250 50 L250 10" />
|
|
||||||
<!-- B -->
|
|
||||||
<path d="M270 10 L270 65" />
|
|
||||||
<path d="M270 10 Q300 10 300 25 Q300 45 270 45" />
|
|
||||||
<path d="M270 45 Q300 45 300 60 Q300 65 270 65" />
|
|
||||||
<!-- E -->
|
|
||||||
<path d="M320 10 L320 65" />
|
|
||||||
<path d="M320 10 L360 10" />
|
|
||||||
<path d="M320 37.5 L350 37.5" />
|
|
||||||
<path d="M320 65 L360 65" />
|
|
||||||
</svg>
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div class="subtitle">Loading, please wait...</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ type NavDefinition = {
|
||||||
matchPath?: string;
|
matchPath?: string;
|
||||||
sidebarVisible?: boolean;
|
sidebarVisible?: boolean;
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
activeMode?: 'path' | 'downloads' | 'history';
|
|
||||||
navbarTitle?: string;
|
navbarTitle?: string;
|
||||||
requires?: 'file_logging' | 'console_enabled';
|
requires?: 'file_logging' | 'console_enabled';
|
||||||
};
|
};
|
||||||
|
|
@ -70,7 +69,6 @@ const NavItems: Array<NavDefinition> = [
|
||||||
icon: 'i-lucide-download',
|
icon: 'i-lucide-download',
|
||||||
to: '/',
|
to: '/',
|
||||||
matchPath: '/',
|
matchPath: '/',
|
||||||
activeMode: 'downloads',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'history',
|
id: 'history',
|
||||||
|
|
@ -81,9 +79,8 @@ const NavItems: Array<NavDefinition> = [
|
||||||
breadcrumbSectionLabel: 'Workspace',
|
breadcrumbSectionLabel: 'Workspace',
|
||||||
description: 'Completed, skipped, and failed downloads.',
|
description: 'Completed, skipped, and failed downloads.',
|
||||||
icon: 'i-lucide-history',
|
icon: 'i-lucide-history',
|
||||||
to: '/#history',
|
to: '/history',
|
||||||
matchPath: '/',
|
matchPath: '/history',
|
||||||
activeMode: 'history',
|
|
||||||
navbarTitle: 'Downloads',
|
navbarTitle: 'Downloads',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -269,24 +266,14 @@ export const getNavItemById = (id: string): NavItem | undefined => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isNavItemActive = (entry: NavItem, route: LocationPath): boolean => {
|
export const isNavItemActive = (entry: NavItem, route: LocationPath): boolean => {
|
||||||
switch (entry.activeMode) {
|
const current = normalizePath(route.path);
|
||||||
case 'downloads':
|
const target = normalizePath(entry.matchPath);
|
||||||
return route.path === '/' && route.hash !== '#history';
|
|
||||||
|
|
||||||
case 'history':
|
if (target === '/') {
|
||||||
return route.path === '/' && route.hash === '#history';
|
return current === '/';
|
||||||
|
|
||||||
default: {
|
|
||||||
const current = normalizePath(route.path);
|
|
||||||
const target = normalizePath(entry.matchPath);
|
|
||||||
|
|
||||||
if (target === '/') {
|
|
||||||
return current === '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
return current === target || current.startsWith(`${target}/`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return current === target || current.startsWith(`${target}/`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getActiveNavItem = (
|
export const getActiveNavItem = (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue