diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b282435 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +# Logs +**/logs +**/*.log +**/npm-debug.log* +**/yarn-debug.log* +**/yarn-error.log* +**/pnpm-debug.log* +**/lerna-debug.log* + +**/node_modules +**/dist +**/dist-ssr +**/*.local +**/src-tauri \ No newline at end of file diff --git a/api.Dockerfile b/api.Dockerfile index 91711ed..03a1a4f 100644 --- a/api.Dockerfile +++ b/api.Dockerfile @@ -1,6 +1,18 @@ # Build context: repository root -# Stage 1: Build .NET API +# Stage 1: Build dashbord +FROM node:24-slim AS dashboard-build +WORKDIR /build + +# Install Node dependencies with a clean install +COPY dashboard/package.json dashboard/package-lock.json ./ +RUN npm ci + +# Copy dashboard source and build it with Vite +COPY dashboard/ ./ +RUN npm run build + +# Stage 2: Build .NET API FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src @@ -12,6 +24,7 @@ COPY server/AniStream.Shared/AniStream.Shared.csproj server/AniStream.Shared/ COPY server/AniStream.Worker/AniStream.Worker.csproj server/AniStream.Worker/ COPY server/ server/ +COPY --from=dashboard-build /build/dist/ server/AniStream/wwwroot RUN dotnet restore ./AniStream.slnx RUN dotnet publish server/AniStream/AniStream.csproj \ diff --git a/app/package.json b/app/package.json index ba9d1e1..c5fb6b2 100644 --- a/app/package.json +++ b/app/package.json @@ -41,11 +41,13 @@ "@types/node": "^25.0.6", "@types/random-useragent": "^0.3.3", "@vitejs/plugin-vue": "^6.0.5", - "better-sqlite3": "^12.9.0", "daisyui": "^5.5.14", "tailwindcss": "^4.1.18", "typescript": "~5.6.2", "vite": "^8.0.5", "vue-tsc": "^2.1.10" + }, + "optionalDependencies": { + "better-sqlite3": "^12.9.0" } } diff --git a/app/src/controls/DetailControl.model.ts b/app/src/controls/DetailControl.model.ts index bb9ad5b..75637ab 100644 --- a/app/src/controls/DetailControl.model.ts +++ b/app/src/controls/DetailControl.model.ts @@ -10,6 +10,7 @@ import {StreamViewModel} from "@views/StreamView.model"; import {SeriesModel} from "@models/series.model"; import {GenreModel} from "@models/genre.model"; +import {ListModel} from "@models/list.model"; import {GenreService} from "@contracts/genre.contract"; import {I18nService} from "@contracts/i18n.contract"; @@ -19,7 +20,6 @@ import {WatchtimeService} from "@contracts/watchtime.contract"; import {ListService} from "@contracts/list.contract"; import I18n from "@utils/i18n"; -import {ListModel} from "@models/list.model"; export class DetailControlModel extends DialogControl { public static readonly component: Component = DetailControl; diff --git a/app/src/controls/DetailControl.vue b/app/src/controls/DetailControl.vue index bd713d3..7cd7e27 100644 --- a/app/src/controls/DetailControl.vue +++ b/app/src/controls/DetailControl.vue @@ -60,7 +60,8 @@ const vm: DetailControlModel = useDialogControl(DetailControlModel);
-
diff --git a/app/src/models/profile.model.ts b/app/src/models/profile.model.ts index 4f5c79a..8c708a1 100644 --- a/app/src/models/profile.model.ts +++ b/app/src/models/profile.model.ts @@ -38,25 +38,28 @@ export interface ProfileApiModel extends Omit = new Set(); for (let i: number = 0; i < genreList.children.length; i++) { const liElement: HTMLLIElement = genreList.children[i] as HTMLLIElement; const anchor: HTMLAnchorElement | null = liElement.children.item(0) as HTMLAnchorElement | null; @@ -84,6 +85,10 @@ export class AniWorldFetcher implements IInformationFetcher { } const genre: string = anchor.href.split("/").at(-1); + if (seenGenres.has(genre)) { + continue; + } + seenGenres.add(genre); genres.push({key: genre, main: genre == mainGenreKey}); } diff --git a/app/src/providers/sto/fetcher.ts b/app/src/providers/sto/fetcher.ts index e33505f..ef349a5 100644 --- a/app/src/providers/sto/fetcher.ts +++ b/app/src/providers/sto/fetcher.ts @@ -77,9 +77,14 @@ export class StoFetcher implements IInformationFetcher { if (!genreList) { throw "Failed to extract genre list"; } - + const seenGenres: Set = new Set(); for (let anchorElement of genreList.values()) { const genre: string = anchorElement.href.split("/").at(-1); + if (seenGenres.has(genre)) { + continue; + } + seenGenres.add(genre); + genres.push({key: genre, main: false}); } diff --git a/app/src/services/client/resource.service.ts b/app/src/services/client/resource.service.ts index e31a42e..8e235a1 100644 --- a/app/src/services/client/resource.service.ts +++ b/app/src/services/client/resource.service.ts @@ -4,6 +4,7 @@ import {ServiceDeclaration} from "@services/declaration"; import {ResourceService} from "@contracts/resource.contract"; import {ProviderService} from "@contracts/provider.contract"; +import {SettingsService} from "@contracts/settings.contract"; import {DefaultProvider} from "@providers/default"; @@ -11,15 +12,16 @@ import {UnsupportedPlatformError} from "@utils/error"; class ResourceServiceImpl implements ResourceService { private readonly providerService: ProviderService; + private readonly settingsService: SettingsService; public constructor(ctx: ReadableGlobalContext) { this.providerService = ctx.getService(ProviderService); + this.settingsService = ctx.getService(SettingsService); } public async getResourceLocation(): Promise { const provider: DefaultProvider = await this.providerService.getProvider(); - // TODO replace with settings url - return `http://localhost:5000/api/${provider.uniqueKey}/resources/`; + return `${this.settingsService.serverUrl.value}/api/${provider.uniqueKey}/resources/`; } public async saveResource(_name: string, _data: Uint8Array): Promise { @@ -30,4 +32,4 @@ class ResourceServiceImpl implements ResourceService { export default { key: ResourceService, ctor: ResourceServiceImpl -} satisfies ServiceDeclaration; \ No newline at end of file +} satisfies ServiceDeclaration; diff --git a/app/src/services/client/user.service.ts b/app/src/services/client/user.service.ts index a792c5c..e9032af 100644 --- a/app/src/services/client/user.service.ts +++ b/app/src/services/client/user.service.ts @@ -104,7 +104,7 @@ export class UserServiceImpl extends ApiServiceBase implements UserService { } public async getProfiles(): Promise { - const rows: ProfileApiModel[] = await this.get(["api", "profiles"]); + const rows: ProfileApiModel[] = await this.get(["api", "profiles", "public"]); return rows.map(row => ProfileModel( row.profile_id, @@ -115,8 +115,8 @@ export class UserServiceImpl extends ApiServiceBase implements UserService { row.mouth, row.theme, row.lang, - row.tos_accepted, - row.sync_catalog + false, + false )); } @@ -163,13 +163,14 @@ export class UserServiceImpl extends ApiServiceBase implements UserService { const row: ProfileApiModel = await this.post(["api", "profiles"], { name, - password: "", - password_salt: "", + password: "0000000", background_color: backgroundColor, eye, mouth, theme, - lang: local + lang: local, + client_user: true, + dashboard_user: true }); return ProfileModel( @@ -204,7 +205,9 @@ export class UserServiceImpl extends ApiServiceBase implements UserService { mouth, theme, lang: local, - tos_accepted: tosAccepted + tos_accepted: tosAccepted, + dashboard_user: null, + client_user: null }); } diff --git a/app/src/views/ProfileView.model.ts b/app/src/views/ProfileView.model.ts index 63d6418..48d326d 100644 --- a/app/src/views/ProfileView.model.ts +++ b/app/src/views/ProfileView.model.ts @@ -14,6 +14,8 @@ import type {ProfileModel} from "@models/profile.model"; import {ProfileSetupControlModel} from "@controls/ProfileSetupControl.model"; import {PinDialogModel} from "@controls/PinDialog.model"; +import {InvalidOperationError} from "@utils/error"; + import * as AppEnv from "@AppEnv"; export class ProfileViewModel extends ViewModel { @@ -63,7 +65,7 @@ export class ProfileViewModel extends ViewModel { this.profiles = await this.userService.getProfiles(); return; } - + this.isProfileSetupFormCancellable = false; await nextTick(); if (this.profileSetupControl) { @@ -77,7 +79,7 @@ export class ProfileViewModel extends ViewModel { } } - public async onProfileItem(profile: ProfileModel): Promise { + public async onProfileBtn(profile: ProfileModel): Promise { if (AppEnv.isClientMode) { using dialog: PinDialogModel = this.dialogService.initDialog(PinDialogModel, profile); await dialog.openDialog(); @@ -89,7 +91,11 @@ export class ProfileViewModel extends ViewModel { } } - await this.userService.setActiveProfile(profile); + const completeProfile: ProfileModel | null = await this.userService.getProfileByUUID(profile.uuid); + if (!completeProfile) { + throw new InvalidOperationError(`Profile '${profile.uuid}' should exists`); + } + await this.userService.setActiveProfile(completeProfile); await this.routerService.navigateTo(ProviderViewModel); } diff --git a/app/src/views/ProfileView.vue b/app/src/views/ProfileView.vue index 28108d2..ff1ec33 100644 --- a/app/src/views/ProfileView.vue +++ b/app/src/views/ProfileView.vue @@ -24,7 +24,7 @@ const vm: ProfileViewModel = useViewModel(ProfileViewModel);