Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
84240b6
feat: add native file dialogs and file system storage
CodeWithMa Apr 3, 2026
9c16508
chore: update lock files
CodeWithMa Apr 3, 2026
c5feaef
fix: use APP_INITIALIZER to load storage before app bootstrap
CodeWithMa Apr 3, 2026
9e21625
fix: remove duplicate file dialog in settings component
CodeWithMa Apr 3, 2026
eeeb2f7
fix: move confirmation dialog to adapter layer
CodeWithMa Apr 3, 2026
fac6583
fix: move confirm to after file selection in service layer
CodeWithMa Apr 3, 2026
92a64d6
fix: await async methods and add delay for confirm
CodeWithMa Apr 3, 2026
111465f
fix: return boolean from importData to indicate success
CodeWithMa Apr 3, 2026
93e8f8c
fix: update lastModifiedAt in signal after save
CodeWithMa Apr 3, 2026
e3c35c0
Remove not working timeout fix
CodeWithMa Apr 3, 2026
1863d51
fix: move data transformations to StorageService
CodeWithMa Apr 3, 2026
861c5ba
fix: re-throw import errors instead of returning null
CodeWithMa Apr 3, 2026
4c7137d
fix: add reject parameter to Promise to properly handle errors
CodeWithMa Apr 3, 2026
d036492
fix: add cancel event listener for file picker
CodeWithMa Apr 3, 2026
76551a4
feat: exportData returns boolean for cancellation handling
CodeWithMa Apr 3, 2026
91dd697
refactor: separate app configs per environment
CodeWithMa Apr 3, 2026
d2d7094
fix: restore environment file replacements alongside app.config
CodeWithMa Apr 3, 2026
a723aef
refactor: move adapter classes to environment files
CodeWithMa Apr 3, 2026
111eedd
fix: add fileReplacements to development configuration
CodeWithMa Apr 11, 2026
5b2a586
fix: make base environment self-sufficient with default adapters
CodeWithMa Apr 11, 2026
016a9f3
feat: add async action handling with resource loader
CodeWithMa Apr 11, 2026
18b6046
fix: use effect() to react to resource data loading
CodeWithMa Apr 11, 2026
a297467
fix: use set() instead of reload() to prevent flicker
CodeWithMa Apr 11, 2026
5a297db
fix: remove loading indicator to prevent flicker
CodeWithMa Apr 11, 2026
94ddbc7
fix: use effect() to expand groups reactively in ItemListComponent
CodeWithMa Apr 11, 2026
f00d3c3
fix: include 'success' in Settings saved message
CodeWithMa Apr 11, 2026
7e04dce
fix: wrap GroupManagerComponent async methods with withAsyncAction
CodeWithMa Apr 11, 2026
b8a4818
refactor: remove misleading providedIn: root from adapters
CodeWithMa Apr 11, 2026
2b72cf3
refactor: remove @Injectable decorator entirely from adapters
CodeWithMa Apr 11, 2026
45e857c
fix: prevent data loss by blocking writes during loading
CodeWithMa Apr 11, 2026
4452fba
fix: guard effects against undefined data during loading
CodeWithMa Apr 11, 2026
1be5eac
style: add .message, .error-message, .success-message styles to group…
CodeWithMa Apr 11, 2026
eebc98e
fix: show error feedback in updateShowCompleted
CodeWithMa Apr 11, 2026
43a4aa5
fix: add error handling and state rollback to components
CodeWithMa Apr 11, 2026
cfd88eb
refactor: extract duplicated code into shared utility modules
CodeWithMa Apr 11, 2026
c7e60fc
refactor: simplify withAsyncAction usage by removing unnecessary IIFEs
CodeWithMa Apr 11, 2026
47d1be3
refactor: replace error signal with typed message signal and simplify…
CodeWithMa Apr 11, 2026
94ac803
fix: move effect into injection context, guard computed against null …
CodeWithMa Apr 11, 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
6 changes: 6 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"@angular/router": "^21.2.6",
"@angular/service-worker": "^21.2.6",
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.5",
"rxjs": "~7.8.0",
"tslib": "^2.3.0"
},
Expand Down
66 changes: 66 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.10.3", features = [] }
tauri-plugin-log = "2"
tauri-plugin-dialog = "2.6.0"
tauri-plugin-fs = "2.4.5"
6 changes: 5 additions & 1 deletion src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"main"
],
"permissions": [
"core:default"
"core:default",
"dialog:default",
"fs:default",
"fs:allow-app-read-recursive",
"fs:allow-app-write-recursive"
]
}
2 changes: 2 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
Expand Down
10 changes: 9 additions & 1 deletion src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ import { provideServiceWorker } from '@angular/service-worker';

import { routes } from './app.routes';
import { environment } from '../environments/environment';
import { StorageService } from './services/storage.service';
import { STORAGE_ADAPTER, IStorageAdapter } from './services/storage.adapter';
import { IMPORT_EXPORT_ADAPTER, IImportExportAdapter } from './services/import-export.adapter';

const storageAdapter: IStorageAdapter = new environment.storageAdapter();
const importExportAdapter: IImportExportAdapter = new environment.importExportAdapter();

export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
{ provide: STORAGE_ADAPTER, useValue: storageAdapter },
{ provide: IMPORT_EXPORT_ADAPTER, useValue: importExportAdapter },
...(environment.enableServiceWorker
? [
provideServiceWorker('ngsw-worker.js', {
Expand All @@ -18,4 +26,4 @@ export const appConfig: ApplicationConfig = {
]
: [])
]
};
};
99 changes: 66 additions & 33 deletions src/app/components/add-item/add-item.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Component, OnInit, signal } from '@angular/core';
import { Component, OnInit, signal, inject, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { WatchListService } from '../../services/watch-list.service';
import { GroupService } from '../../services/group.service';
import { StorageService } from '../../services/storage.service';
import { ItemType } from '../../models/item.model';
import { Group } from '../../models/group.model';
import { createAsyncAction, withAsyncAction } from '../../utils/async-action';

@Component({
selector: 'app-add-item',
Expand All @@ -14,6 +16,10 @@ import { Group } from '../../models/group.model';
<div class="add-item-container">
<h1>Add New Item</h1>

<div *ngIf="state.message()" class="message" [class.error-message]="state.message()?.type === 'error'" [class.success-message]="state.message()?.type === 'success'">
{{ state.message()?.text }}
</div>

<form (ngSubmit)="onSubmit()" class="add-item-form">
<div class="form-group">
<label for="title">Title *</label>
Expand Down Expand Up @@ -97,7 +103,7 @@ import { Group } from '../../models/group.model';
</div>

<div class="form-actions">
<button type="submit" class="submit-btn">Add Item</button>
<button type="submit" class="submit-btn" [disabled]="state.busy()">Add Item</button>
<button type="button" (click)="cancel()" class="cancel-btn">Cancel</button>
</div>
</form>
Expand Down Expand Up @@ -190,9 +196,33 @@ import { Group } from '../../models/group.model';
.cancel-btn:hover {
background: var(--accent-secondary-hover);
}

.message {
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}

.error-message {
background: light-dark(#f8d7da, #721c24);
color: light-dark(#721c24, #f8d7da);
border: 1px solid light-dark(#f5c6cb, #721c24);
}

.success-message {
background: light-dark(#d4edda, #155724);
color: light-dark(#155724, #d4edda);
border: 1px solid light-dark(#c3e6cb, #155724);
}
`]
})
export class AddItemComponent implements OnInit {
private readonly storageService = inject(StorageService);
private readonly watchListService = inject(WatchListService);
private readonly groupService = inject(GroupService);
private readonly router = inject(Router);

state = createAsyncAction();
groups = signal<Group[]>([]);

title = '';
Expand All @@ -202,21 +232,21 @@ export class AddItemComponent implements OnInit {
episode = 1;
totalEpisodes: number | undefined;

constructor(
private watchListService: WatchListService,
private groupService: GroupService,
private router: Router
) {}

ngOnInit(): void {
this.groups.set(this.groupService.getAllGroups());
// Ensure ungrouped is selected by default
if (this.groups().length > 0 && !this.groupId) {
const ungrouped = this.groups().find(g => g.id === 'ungrouped');
this.groupId = ungrouped ? ungrouped.id : this.groups()[0].id;
}
constructor() {
effect(() => {
const data = this.storageService.data();
if (data) {
this.groups.set(this.groupService.getAllGroups());
if (this.groups().length > 0 && !this.groupId) {
const ungrouped = this.groups().find(g => g.id === 'ungrouped');
this.groupId = ungrouped ? ungrouped.id : this.groups()[0].id;
}
}
});
}

ngOnInit(): void {}

onTypeChange(): void {
if (this.type === 'movie') {
this.season = 1;
Expand All @@ -225,25 +255,28 @@ export class AddItemComponent implements OnInit {
}
}

onSubmit(): void {
if (!this.title.trim()) {
return;
}

this.watchListService.addItem({
title: this.title.trim(),
type: this.type,
groupId: this.groupId,
status: 'not-started',
progress: this.type === 'series' ? {
season: this.season,
episode: this.episode,
totalEpisodes: this.totalEpisodes
} : undefined
});
onSubmit = withAsyncAction(
async () => {
if (!this.title.trim()) {
return;
}

this.router.navigate(['/items']);
}
await this.watchListService.addItem({
title: this.title.trim(),
type: this.type,
groupId: this.groupId,
status: 'not-started',
progress: this.type === 'series' ? {
season: this.season,
episode: this.episode,
totalEpisodes: this.totalEpisodes
} : undefined
});

this.router.navigate(['/items']);
},
this.state
);

cancel(): void {
this.router.navigate(['/items']);
Expand Down
Loading