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);