Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ea31556
Setup new dashboard project
CBx0-dev Jul 20, 2026
e132579
Implement LoginView
CBx0-dev Jul 20, 2026
2078fe5
Design Update card
CBx0-dev Jul 20, 2026
d0d128e
Implement Audit panel
CBx0-dev Jul 20, 2026
c343f4e
Complete basic shape
CBx0-dev Jul 25, 2026
e6217d6
Make Login and Audit Panel functional
CBx0-dev Jul 25, 2026
e7c154e
Make syncing catalog compatible
CBx0-dev Jul 27, 2026
2551485
Create inital account
CBx0-dev Jul 27, 2026
6d23c4d
Make row reactive and add ellipse in pagination
CBx0-dev Jul 27, 2026
7084824
Add midnight auto-sync worker
CBx0-dev Jul 27, 2026
ebae838
Use public profiles
CBx0-dev Jul 27, 2026
008505f
Fix search bug
CBx0-dev Jul 27, 2026
c9b3cb4
Fix bug in provider sync
CBx0-dev Jul 27, 2026
350915d
Add stats route
CBx0-dev Jul 27, 2026
7a4a3ec
Dashboard progression
CBx0-dev Aug 6, 2026
d1557b0
Remove unused import
CBx0-dev Aug 6, 2026
1f4c3c6
Fix building errors
CBx0-dev Aug 6, 2026
2ef693d
Add debug lines
CBx0-dev Aug 6, 2026
0ccab86
Fix caprover definition
CBx0-dev Aug 6, 2026
5cf3921
Revert "Add debug lines"
CBx0-dev Aug 6, 2026
80e209b
Use direct stdout API
CBx0-dev Aug 7, 2026
60906d1
Fix Syncing bugs
CBx0-dev Aug 8, 2026
36b7952
Implement delete route
CBx0-dev Aug 8, 2026
b935074
Include root account in test
CBx0-dev Aug 8, 2026
ba2b1ab
Allow test account anything
CBx0-dev Aug 8, 2026
a6f696b
Fix create route
CBx0-dev Aug 8, 2026
8b6cbc8
Update tests
CBx0-dev Aug 8, 2026
de56019
Increment test timeout
CBx0-dev Aug 8, 2026
ad8de46
Use correct id
CBx0-dev Aug 8, 2026
8713a3d
Fix Bug detected by updating test case
CBx0-dev Aug 15, 2026
ad8a9c1
Fix diverging problem between standalone and client mode
CBx0-dev Aug 15, 2026
c61f326
Make properties nullable
CBx0-dev Aug 16, 2026
f2e5817
Remove console.log
CBx0-dev Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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
15 changes: 14 additions & 1 deletion api.Dockerfile
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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 \
Expand Down
4 changes: 3 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
2 changes: 1 addition & 1 deletion app/src/controls/DetailControl.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion app/src/controls/DetailControl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ const vm: DetailControlModel = useDialogControl(DetailControlModel);
</div>
<div class="shrink-0 flex justify-end items-center gap-5">
<div class="tooltip w-full max-w-[150px] h-fit" data-tip="Watch progression">
<progress class="progress progress-primary max-w-[150px]" :value="vm.watchProgression"
<progress class="progress progress-primary max-w-[150px]"
:value="vm.watchProgression"
max="100"/>
</div>
<div class="join">
Expand Down
21 changes: 12 additions & 9 deletions app/src/models/profile.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,28 @@ export interface ProfileApiModel extends Omit<ProfileDbModel, "tos_accepted" | "
export interface ProfileCreateModel {
name: string;
password: string;
password_salt: string;
background_color: string;
eye: ProfileEye;
mouth: ProfileMouth;
theme: string;
lang: SupportedLocals;
client_user: boolean;
dashboard_user: boolean;
}

/**
* Used for API only
*/
export interface ProfileUpdateModel {
name: string;
background_color: string;
eye: ProfileEye;
mouth: ProfileMouth;
theme: string;
lang: SupportedLocals;
tos_accepted: boolean;
name: string | null;
background_color: string | null;
eye: ProfileEye | null;
mouth: ProfileMouth | null;
theme: string | null;
lang: SupportedLocals | null;
tos_accepted: boolean | null;
dashboard_user: boolean | null;
client_user: boolean | null;
}

export function ProfileModel(
Expand Down Expand Up @@ -138,4 +141,4 @@ function clone(this: ProfileModel): ProfileModel {

obj.clone = obj.clone.bind(obj);
return obj;
}
}
5 changes: 5 additions & 0 deletions app/src/providers/aniworld/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export class AniWorldFetcher implements IInformationFetcher {
}

let mainGenreKey: string = genreList.getAttribute("data-main-genre") ?? "";
const seenGenres: Set<string> = new Set<string>();
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;
Expand All @@ -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});
}

Expand Down
7 changes: 6 additions & 1 deletion app/src/providers/sto/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,14 @@ export class StoFetcher implements IInformationFetcher {
if (!genreList) {
throw "Failed to extract genre list";
}

const seenGenres: Set<string> = new Set<string>();
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});
}

Expand Down
8 changes: 5 additions & 3 deletions app/src/services/client/resource.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@ 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";

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<string> {
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<void> {
Expand All @@ -30,4 +32,4 @@ class ResourceServiceImpl implements ResourceService {
export default {
key: ResourceService,
ctor: ResourceServiceImpl
} satisfies ServiceDeclaration<ResourceService>;
} satisfies ServiceDeclaration<ResourceService>;
17 changes: 10 additions & 7 deletions app/src/services/client/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class UserServiceImpl extends ApiServiceBase implements UserService {
}

public async getProfiles(): Promise<ProfileModel[]> {
const rows: ProfileApiModel[] = await this.get<ProfileApiModel[]>(["api", "profiles"]);
const rows: ProfileApiModel[] = await this.get<ProfileApiModel[]>(["api", "profiles", "public"]);

return rows.map(row => ProfileModel(
row.profile_id,
Expand All @@ -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
));
}

Expand Down Expand Up @@ -163,13 +163,14 @@ export class UserServiceImpl extends ApiServiceBase implements UserService {

const row: ProfileApiModel = await this.post<ProfileApiModel, ProfileCreateModel>(["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(
Expand Down Expand Up @@ -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
});
}

Expand Down
12 changes: 9 additions & 3 deletions app/src/views/ProfileView.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -63,7 +65,7 @@ export class ProfileViewModel extends ViewModel {
this.profiles = await this.userService.getProfiles();
return;
}

this.isProfileSetupFormCancellable = false;
await nextTick();
if (this.profileSetupControl) {
Expand All @@ -77,7 +79,7 @@ export class ProfileViewModel extends ViewModel {
}
}

public async onProfileItem(profile: ProfileModel): Promise<void> {
public async onProfileBtn(profile: ProfileModel): Promise<void> {
if (AppEnv.isClientMode) {
using dialog: PinDialogModel = this.dialogService.initDialog(PinDialogModel, profile);
await dialog.openDialog();
Expand All @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/views/ProfileView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const vm: ProfileViewModel = useViewModel(ProfileViewModel);
<button v-for="profile of vm.profiles"
:key="profile.profile_id"
class="group flex flex-col items-center gap-2 focus:outline-none"
@click="vm.onProfileItem(profile)">
@click="vm.onProfileBtn(profile)">
<div class="relative rounded-box border border-base-300 bg-base-100 transition hover:border-primary group-focus:border-primary group-focus:ring-2 group-focus:ring-primary overflow-clip">
<img :src="vm.getProfilePicture(profile)"
:alt="profile.name"
Expand Down
6 changes: 3 additions & 3 deletions app/src/views/ProviderView.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ export class ProviderViewModel extends ViewModel {
await this.routerService.navigateTo(SettingsViewModel);
}

public onProfileBtn(): void {
public async onProfileBtn(): Promise<void> {
if (AppEnv.isClientMode) {
this.userService.logout();
await this.userService.logout();
}

this.userService.setActiveProfile(null);
await this.userService.setActiveProfile(null);

this.routerService.navigateBack();
}
Expand Down
29 changes: 22 additions & 7 deletions app/src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";

import {type App, type Plugin} from "vue";
import {createMVVM, DIContainer} from "vue-mvvm";

Expand Down Expand Up @@ -39,6 +42,10 @@ interface CliOptions {
output: "text" | "json";
}

interface SeriesCliOptions {
savePreview: string | undefined;
}

class OutputHandler {
private spinner: Ora | null = null;
public readonly isJson: boolean = false;
Expand Down Expand Up @@ -78,15 +85,17 @@ class OutputHandler {

result(data: unknown, textFormatter: () => void): void {
if (this.isJson) {
console.log(JSON.stringify(data));
process.stdout.write(JSON.stringify(data));
process.stdout.write("\n");
} else {
textFormatter();
}
}

error(message: string): void {
if (this.isJson) {
console.error(JSON.stringify({error: message}));
process.stdout.write(JSON.stringify({error: message}));
process.stdout.write("\n");
} else {
console.error(chalk.red.bold("Error: ") + chalk.red(message));
}
Expand Down Expand Up @@ -138,14 +147,20 @@ program

program
.command("series <guid>")
.option("--save-preview <path>")
.description("Get series details")
.action(async (guid: string) => {
const options: CliOptions = program.opts();
const out: OutputHandler = new OutputHandler(options.output);
.action(async (guid: string, cliOptions: SeriesCliOptions) => {
const globalOptions: CliOptions = program.opts();
const out: OutputHandler = new OutputHandler(globalOptions.output);
try {
const fetcher: IInformationFetcher = await getFetcher(options);
const fetcher: IInformationFetcher = await getFetcher(globalOptions);
out.startSpinner(`Fetching series ${guid}...`);
const [model, genres] = await fetcher.getSeries(guid);
const [model, genres, previewImage] = await fetcher.getSeries(guid);

if (cliOptions.savePreview && model.preview_image && previewImage) {
await fs.writeFile(path.join(cliOptions.savePreview, model.preview_image), previewImage);
}

out.stopSpinner(true, `Series ${guid} fetched`);
out.result({series: model, genres}, () => {
out.log(chalk.green.bold(`\n${model.title}`));
Expand Down
Loading
Loading