nuxt rewrite

This commit is contained in:
2025-09-25 22:19:06 +02:00
parent ee56a59aef
commit 7cdf77cb52
63 changed files with 395 additions and 11065 deletions

View File

@@ -0,0 +1,7 @@
@import 'tailwindcss';
@import '@nuxt/ui';
@theme {
--color-pink: #f5a9b8;
--color-blue: #5bcefa;
}

View File

@@ -0,0 +1,37 @@
<template>
<UModal v-bind="$props" title="Add Library">
<template #body>
<div class="flex flex-col gap-2">
<UFormField label="Library Name" required>
<UInput v-model="name" placeholder="Name for the library" class="w-full" :disabled="busy" />
</UFormField>
<UFormField label="Directory Path" required>
<UInput v-model="path" placeholder="Path for the library" class="w-full" :disabled="busy" />
</UFormField>
<UButton icon="i-lucide-plus" @click="onAddClick" :loading="busy">Add</UButton>
</div>
</template>
</UModal>
</template>
<script setup lang="ts">
import type { components } from '#open-fetch-schemas/api';
const name = ref('');
const path = ref('');
const model = computed((): components['schemas']['CreateLibraryRecord'] => {
return { basePath: path.value, libraryName: name.value };
});
const config = useRuntimeConfig();
const busy = ref(false);
const onAddClick = () => {
busy.value = true;
$fetch(new Request(`${config.public.openFetch.api.baseURL}v2/FileLibrary`), { method: 'PUT', body: model.value })
.then(() => emit('change'))
.finally(() => (busy.value = false));
};
const emit = defineEmits(['change']);
</script>

View File

@@ -0,0 +1,31 @@
<template>
<UPageList divide>
<UPageCard
v-for="l in fileLibraries"
variant="ghost"
icon="i-lucide-library-big"
:title="l.libraryName"
:description="l.basePath"
orientation="horizontal">
<UButton color="warning" @click="deleteLibrary(l)" :loading="busy">Delete</UButton>
</UPageCard>
</UPageList>
</template>
<script setup lang="ts">
import type { components } from '#open-fetch-schemas/api';
type FileLibrary = components['schemas']['FileLibrary'];
const { data: fileLibraries, refresh } = useApi('/v2/FileLibrary');
const config = useRuntimeConfig();
const busy = ref(false);
const deleteLibrary = (l: FileLibrary) => {
busy.value = true;
$fetch(new Request(`${config.public.openFetch.api.baseURL}v2/FileLibrary/${l.key}`), { method: 'DELETE' }).finally(
() => {
refresh();
busy.value = false;
}
);
};
</script>

View File

@@ -0,0 +1,48 @@
<template>
<UCard
v-if="!expanded"
:ui="{ body: 'p-0 sm:p-0', root: 'overflow-visible' }"
class="relative h-[350px] mt-2"
@click="$emit('click')">
<MangaCover :manga="manga" blur />
<div class="absolute -top-4 -right-4 flex flex-col bg-pink rounded-full">
<MangaconnectorIcon v-for="m in manga.mangaConnectorIds" v-bind="m" />
</div>
</UCard>
<UCard
v-else
orientation="horizontal"
reverse
class="relative max-w-[600px] w-full h-[350px] mt-2"
:ui="{ body: 'p-0 sm:p-0', root: 'overflow-visible' }"
@click="$emit('click')">
<div class="flex flex-row w-full h-full basis-auto">
<MangaCover :manga="manga" class="shrink-0" />
<div class="absolute -top-4 -right-4 flex flex-col bg-pink rounded-full">
<MangaconnectorIcon v-for="m in manga.mangaConnectorIds" v-bind="m" />
</div>
<div class="flex flex-col h-[350px] shrink mx-2">
<p class="font-semibold text-xl">{{ manga.name }}</p>
<p class="max-h-30 overflow-y-hidden grow">{{ manga.description }}</p>
</div>
</div>
<div class="absolute bottom-0 w-full p-2 flex flex-row justify-end">
<slot name="actions" v-bind="manga" />
</div>
</UCard>
</template>
<script setup lang="ts">
import type { components } from '#open-fetch-schemas/api';
import type { PageCardProps } from '#ui/components/PageCard.vue';
type Manga = components['schemas']['Manga'];
type MinimalManga = components['schemas']['MinimalManga'];
defineProps<MangaCardProps>();
defineEmits(['click']);
export interface MangaCardProps extends PageCardProps {
manga: Manga | MinimalManga;
expanded?: boolean;
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="relative w-[240px] h-[350px] rounded-lg overflow-clip">
<div
v-if="blur"
class="absolute l-0 t-0 w-full h-full rounded-lg overflow-clip"
style="
background: linear-gradient(150deg, rgba(245, 169, 184, 0.3) 50%, rgba(91, 206, 250, 0.2));
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(2px) brightness(70%);
-webkit-backdrop-filter: blur(2px) brightness(70%);
">
<p class="p-3 text-xl font-semibold max-h-full overflow-clip">{{ manga?.name }}</p>
</div>
<LazyNuxtImg
v-if="manga || mangaId"
:src="`${$config.public.openFetch.api.baseURL}v2/Manga/${manga ? manga.key : mangaId}/Cover/Medium`"
class="w-full h-full object-cover" />
<USkeleton v-else class="w-full h-full object-cover" />
</div>
</template>
<script setup lang="ts">
import type { components } from '#open-fetch-schemas/api';
type Manga = components['schemas']['Manga'];
type MinimalManga = components['schemas']['MinimalManga'];
defineProps<{ manga?: Manga | MinimalManga; mangaId?: string; blur?: boolean }>();
</script>

View File

@@ -0,0 +1,46 @@
<template>
<UPage class="p-4 h-full">
<template #left>
<div class="flex flex-col gap-2 border-r-2 pr-4">
<MangaCover :manga="manga" class="self-center" />
<p v-if="manga" class="font-semibold text-xl">
{{ manga.name }}
<MangaconnectorIcon v-for="m in manga.mangaConnectorIds" v-bind="m" />
</p>
<USkeleton v-else class="text-xl h-20 w-full" />
<div v-if="manga" class="flex flex-row gap-1 flex-wrap">
<UBadge variant="outline" v-for="author in manga.authors" color="neutral">{{ author.name }}</UBadge>
<UBadge variant="outline" v-for="tag in manga.tags">{{ tag }}</UBadge>
<NuxtLink v-for="link in manga.links" :to="link.url">
<UBadge variant="outline" color="warning">{{ link.provider }}</UBadge>
</NuxtLink>
</div>
<USkeleton v-else class="w-full h-lh" />
<p v-if="manga" class="max-h-30 overflow-y-hidden grow">
{{ manga.description }}
</p>
<USkeleton v-else class="w-full h-30" />
</div>
</template>
<UPageBody class="mt-0 relative">
<div>
<UButton variant="soft" to="/" icon="i-lucide-arrow-left">Back</UButton>
<p v-if="title" class="text-3xl">{{ title }}</p>
</div>
<slot />
</UPageBody>
</UPage>
</template>
<script setup lang="ts">
import type { components } from '#open-fetch-schemas/api';
type Manga = components['schemas']['Manga'];
export interface MangaDetailPageProps {
manga?: Manga;
title?: string;
}
defineProps<MangaDetailPageProps>();
</script>

View File

@@ -0,0 +1,25 @@
<template>
<div class="w-6 h-6 inline-block align-middle m-1">
<NuxtLink :href="$props.websiteUrl ?? ''">
<NuxtImg
v-if="mangaConnector"
:src="mangaConnector?.iconUrl"
:class="[
'w-full rounded-full outline-2 -outline-offset-1',
props.useForDownload ? 'outline-green-500' : 'outline-red-500',
]" />
<p v-else>{{ mangaConnectorName }}</p>
</NuxtLink>
</div>
</template>
<script setup lang="ts">
import type { components } from '#open-fetch-schemas/api';
type MangaConnectorId = components['schemas']['MangaConnectorId'];
const props = defineProps<MangaConnectorId>();
const { data: mangaConnector } = useApi('/v2/MangaConnector/{MangaConnectorName}', {
path: { MangaConnectorName: props.mangaConnectorName },
});
</script>

View File

@@ -0,0 +1,20 @@
<template>
<UPageBody class="p-4 flex flex-row flex-wrap gap-6 mt-0">
<MangaCard
v-for="(m, i) in manga"
:manga="m"
:expanded="i === expanded"
@click="expanded = expanded === i ? -1 : i">
<template #actions="manga">
<UButton :to="`manga/${manga.key}`">Details</UButton>
</template>
</MangaCard>
</UPageBody>
</template>
<script setup lang="ts">
const { data: manga } = useApi('/v2/Manga/Downloading');
const expanded = ref(-1);
</script>
<style scoped></style>

View File

@@ -0,0 +1,11 @@
<template>
<MangaDetailPage :manga="manga"> </MangaDetailPage>
</template>
<script setup lang="ts">
import MangaDetailPage from '~/components/MangaDetailPage.vue';
const route = useRoute();
const { data: manga } = useApi('/v2/Manga/{MangaId}', { path: { MangaId: route.params.mangaId as string } });
</script>

View File

@@ -0,0 +1,109 @@
<template>
<UPageSection :ui="{ container: 'gap-4 sm:gap-4 lg:gap-4 ' }">
<UButton variant="ghost" to="/" icon="i-lucide-arrow-left" class="w-min">Back</UButton>
<div class="flex flex-row w-full h-full justify-between gap-4">
<UStepper v-model="activeStep" orientation="vertical" :items="items" class="h-full" disabled />
<UCard class="grow">
<div class="flex flex-col justify-between gap-2">
<UInput v-model="query" class="w-full" :disabled="busy" />
<div class="flex gap-1 w-full justify-center">
<UButton
v-for="c in connectors"
@click="connectorClick(c)"
:color="connector?.key == c.key ? 'success' : 'neutral'"
:disabled="busy">
<template #leading>
<NuxtImg :src="c.iconUrl" class="h-lh" />
</template>
{{ c.name }}
</UButton>
<UButton color="secondary" :disabled="busy" @click="performSearch" :loading="busy"
>Search</UButton
>
</div>
</div>
</UCard>
</div>
<p v-if="searchResult" class="text-lg">Result for '{{ searchQuery }}'</p>
<div v-if="searchResult" class="relative flex flex-row flex-wrap gap-6 mt-0">
<MangaCard
v-for="(m, i) in searchResult"
:manga="m"
:expanded="i === expanded"
@click="expanded = expanded === i ? -1 : i">
<template #actions="manga">
<UButton :to="`manga/${manga.key}`">Download</UButton>
</template>
</MangaCard>
</div>
</UPageSection>
</template>
<script setup lang="ts">
import type { components } from '#open-fetch-schemas/api';
import type { StepperItem } from '@nuxt/ui';
type MangaConnector = components['schemas']['MangaConnector'];
type MinimalManga = components['schemas']['MinimalManga'];
const { data: connectors } = useApi('/v2/MangaConnector');
const query = ref<string>();
const connector = ref<MangaConnector>();
const activeStep = ref(0);
const busy = ref<boolean>(false);
watch(query, (v) => {
if (!v) activeStep.value = 0;
else activeStep.value = 1;
});
const isUrl = (input: string): boolean => {
try {
new URL(input);
return true;
} catch {
return false;
}
};
const connectorClick = (c: MangaConnector) => {
connector.value = c;
performSearch();
};
const searchResult = ref<MinimalManga[]>();
const expanded = ref(-1);
const searchQuery = ref<string>('');
const performSearch = () => {
if (!query.value) return;
busy.value = true;
searchQuery.value = query.value;
search(query.value)
.then((data) => {
searchResult.value = data;
activeStep.value = 2;
})
.finally(() => (busy.value = false));
};
const config = useRuntimeConfig();
const search = async (query: string): Promise<MinimalManga[]> => {
if (isUrl(query)) {
return await $fetch(new Request(`${config.public.openFetch.api.baseURL}v2/Search/Url`), {
method: 'POST',
body: query,
});
} else if (connector.value) {
return await $fetch(
new Request(`${config.public.openFetch.api.baseURL}v2/Search/${connector.value.name}/${query}`)
);
}
return Promise.reject();
};
const items = ref<StepperItem[]>([
{ title: 'Query', description: 'The name or URL', icon: 'i-lucide-search' },
{ title: 'Site', description: 'Select the site on which to search', icon: 'i-lucide-panel-top' },
{ title: 'Results', icon: 'i-lucide-logs' },
]);
</script>

View File

@@ -0,0 +1,32 @@
<template>
<UPageSection title="Settings"> </UPageSection>
<UPageSection title="Libraries" orientation="horizontal">
<FileLibraries />
<UButton icon="i-lucide-plus" @click="() => addLibraryModal.open()" class="w-fit">Add</UButton>
</UPageSection>
<UPageSection title="Maintenance" orientation="horizontal">
<div class="flex flex-col gap-1 items-end basis-1">
<UButton icon="i-lucide-database" :loading="cleanUpDatabaseBusy" @click="cleanUpDatabase" class="w-fit"
>Clean database</UButton
>
</div>
</UPageSection>
</template>
<script setup lang="ts">
import { LazyAddLibraryModal } from '#components';
const overlay = useOverlay();
const config = useRuntimeConfig();
const addLibraryModal = overlay.create(LazyAddLibraryModal);
import FileLibraries from '~/components/FileLibraries.vue';
const cleanUpDatabaseBusy = ref(false);
const cleanUpDatabase = () => {
cleanUpDatabaseBusy.value = true;
$fetch(`${config.public.openFetch.api.baseURL}v2/Maintenance/CleanupNoDownloadManga`).finally(
() => (cleanUpDatabaseBusy.value = false)
);
};
</script>