diff --git a/package.json b/package.json
index 4edeea5..d727bd0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "mytaskapplication",
- "version": "1.6.0",
+ "version": "1.7.0",
"main": "app.js",
"author": {
"name": "Maik Roland Damm",
diff --git a/src/app/app.component.html b/src/app/app.component.html
index d26f0a4..6142220 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,19 +1,13 @@
-
-
-
- @if(showNotifications) {
-
- }
-
-
-
-
-
-
+
+
+
+
+
+ @if(showNotifications) {
+
+ }
+
-
-
-
-
-
+
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 16a3b79..c505e13 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,43 +1,52 @@
-import {Component, inject} from '@angular/core';
+import {Component, inject, OnInit} from '@angular/core';
import {HeaderComponent} from './components/header/header.component';
-import {FilterControlsComponent} from './components/filter-controls/filter-controls.component';
-import {TaskAddComponent} from './components/task-add/task-add.component';
-import {TaskListComponent} from './components/task-list/task-list.component';
-import {TodoProgressbarComponent} from './components/todo-progressbar/todo-progressbar.component';
import {FooterComponent} from './components/footer/footer.component';
import {AnnouncementBoxComponent} from './components/announcement-box/announcement-box.component';
import {DarkModeService} from './services/dark-mode.service';
import {SettingsService} from './services/settings.service';
import {NotificationBoxComponent} from './components/notification-box/notification-box.component';
+import {Router, RouterOutlet} from '@angular/router';
+import {Account} from './interfaces/account';
+import {AccountService} from './services/account.service';
@Component({
selector: 'app-root',
- imports: [HeaderComponent, FilterControlsComponent, TaskAddComponent, TaskListComponent, TodoProgressbarComponent, FooterComponent, AnnouncementBoxComponent, NotificationBoxComponent],
+ imports: [HeaderComponent,FooterComponent, AnnouncementBoxComponent, NotificationBoxComponent, RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
-export class AppComponent {
+export class AppComponent implements OnInit {
title = 'untitled1';
+ useraccount: Account | undefined
private settingsService = inject(SettingsService)
protected settings = this.settingsService.getSettings();
+ private initialChoice = this.settings()[0]?.initialChoice;
+
get isDarkMode(): boolean {
return this.darkModeService.isDarkMode();
}
- get showProgressBar():boolean {
- return this.settings()[0]?.showProgressBar || false
- }
-
get showNotifications():boolean {
return this.settings()[0]?.showNotifications || false
}
- constructor(private darkModeService: DarkModeService) {}
+ constructor(
+ private darkModeService: DarkModeService,
+ private router: Router,
+ private accountService: AccountService) {
+ this.accountService.accountChanged.subscribe(account => {
+ this.useraccount = account;
+ });
- toggleDarkMode(): void {
- this.darkModeService.toggleDarkMode();
}
+ ngOnInit() {
+ if(this.initialChoice === undefined) {
+ void this.router.navigate(['/initial-choice']);
+ } else
+ if(this.settings()[0]?.initialChoice === 'offline')
+ this.useraccount = this.accountService.loadLocalAccount()
+ }
}
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 235192a..347cb2c 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -1,5 +1,14 @@
import { Routes } from '@angular/router';
+import {TasksComponent} from './components/page/tasks/tasks.component';
+import {InitialChoiceComponent} from './components/initial-choice/initial-choice.component';
+import {CreateAccountComponent} from './components/account/create-account/create-account.component';
+import {TaskMigrationComponent} from './components/task-migration/task-migration.component';
export const routes: Routes = [
- { path: '', redirectTo: 'index.html', pathMatch: 'full' },
+ { path: '', redirectTo: 'tasks', pathMatch: 'full' },
+ { path: 'initial-choice', component: InitialChoiceComponent},
+ { path: 'account/create/:action', component: CreateAccountComponent },
+ { path: 'tasks', component: TasksComponent },
+ { path: 'tasks/migration', component: TaskMigrationComponent },
+ { path: '**', redirectTo: 'index.html' }
];
diff --git a/src/app/components/account/create-account/create-account.component.css b/src/app/components/account/create-account/create-account.component.css
new file mode 100644
index 0000000..861e0b4
--- /dev/null
+++ b/src/app/components/account/create-account/create-account.component.css
@@ -0,0 +1,62 @@
+.card {
+ border-radius: 0.15rem;
+}
+
+form > *, form > * > * > * {
+ border-radius: 0;
+}
+
+form > button {
+ background-color: #4c8bf5;
+ border: 0;
+ color: #fff;
+}
+
+form > button:hover {
+ background-color: #9463f7;
+ color: rgba(255, 255, 255, 0.85);
+}
+
+
+/* Dark theme styles for task-add component */
+.dark-theme {
+ background-color: var(--surface-color);
+ border-color: var(--border-color);
+ color: var(--text-color);
+}
+
+/* Style for input fields in dark mode */
+.dark-theme .form-control {
+ background-color: #1a1a2e; /* Dark background for input */
+ color: var(--text-color); /* White text */
+ border-color: var(--border-color);
+}
+
+/* Style for placeholder text in dark mode */
+.dark-theme .form-control::placeholder {
+ color: rgba(255, 255, 255, 0.6); /* Slightly dimmed for placeholder */
+}
+
+/* Style for focused input in dark mode */
+.dark-theme .form-control:focus {
+ background-color: #2a2a3e; /* Slightly lighter when focused */
+ box-shadow: 0 0 0 0.25rem rgba(187, 134, 252, 0.25); /* Glow that matches primary color */
+}
+
+/* Dark theme specific styles for file input */
+.dark-theme input[type="file"].form-control {
+ background-color: #1a1a2e;
+ color: var(--text-color);
+}
+
+.dark-theme input[type="file"].form-control::file-selector-button {
+ background-color: #333345;
+ color: white;
+}
+
+.dark-theme input[type="file"].form-control::file-selector-button:hover {
+ background-color: #4c4c6d;
+}
+
+
+
diff --git a/src/app/components/account/create-account/create-account.component.html b/src/app/components/account/create-account/create-account.component.html
new file mode 100644
index 0000000..4356f4a
--- /dev/null
+++ b/src/app/components/account/create-account/create-account.component.html
@@ -0,0 +1,73 @@
+
+
+
Local Account Setup
+
+ To create your local account, simply enter a username, choose a password (which will encrypt all your data), and
+ upload an avatar of your choice. Be sure to remember this password—if you ever export your data and import it on
+ another device, you’ll need the exact same secret to restore access.
+
+
+
+
+
diff --git a/src/app/components/account/create-account/create-account.component.spec.ts b/src/app/components/account/create-account/create-account.component.spec.ts
new file mode 100644
index 0000000..b473c88
--- /dev/null
+++ b/src/app/components/account/create-account/create-account.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CreateAccountComponent } from './create-account.component';
+
+describe('CreateAccountComponent', () => {
+ let component: CreateAccountComponent;
+ let fixture: ComponentFixture
;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [CreateAccountComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(CreateAccountComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/account/create-account/create-account.component.ts b/src/app/components/account/create-account/create-account.component.ts
new file mode 100644
index 0000000..83eac54
--- /dev/null
+++ b/src/app/components/account/create-account/create-account.component.ts
@@ -0,0 +1,118 @@
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {DarkModeService} from '../../../services/dark-mode.service';
+import {NgClass} from '@angular/common';
+import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms';
+import {AccountService} from '../../../services/account.service';
+import {Account} from '../../../interfaces/account';
+
+@Component({
+ selector: 'app-create-account',
+ imports: [
+ NgClass,
+ FormsModule,
+ ReactiveFormsModule
+ ],
+ templateUrl: './create-account.component.html',
+ styleUrl: './create-account.component.css'
+})
+export class CreateAccountComponent implements OnInit {
+ action:string = '';
+ offline: boolean = false;
+ accountForm: FormGroup;
+ selectedFile: File | null = null;
+
+ private localAccount: Account = {
+ offline: true,
+ username: ''
+ };
+
+ private onlineAccount: Account = {
+ offline: false,
+ username: ''
+ };
+
+ constructor(private fb: FormBuilder,
+ private route: ActivatedRoute,
+ private darkModeService: DarkModeService,
+ private accountService: AccountService)
+ {
+ this.accountForm = this.fb.group({
+ username: ['', Validators.required],
+ password: ['', Validators.required],
+ });
+
+ }
+
+ get isDarkMode(): boolean {
+ return this.darkModeService.isDarkMode();
+ }
+
+
+ onFileSelected(event: any) {
+ if (event.target.files.length > 0) {
+ this.selectedFile = event.target.files[0];
+ }
+ }
+
+ submitForm() {
+ if (this.accountForm.valid && this.selectedFile) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const avatar = e.target?.result as string;
+ this.createAccount(
+ this.accountForm.get('username')?.value,
+ this.accountForm.get('password')?.value,
+ this.accountForm.get('email')?.value.replace('@', '_at_').replace('.', '_dot_').replace(' ', '_'),
+ avatar
+ );
+ };
+ reader.readAsDataURL(this.selectedFile);
+ } else {
+ // Mark all fields as touched to trigger validation messages
+ this.accountForm.markAllAsTouched();
+ }
+ }
+
+
+ ngOnInit(): void {
+ this.route.params.subscribe(params => {
+ this.action = params['action'];
+ this.handleAction();
+ })
+ }
+
+ createAccount(name: string, password: string, email: string, avatar: string) {
+ if(this.offline) {
+ this.localAccount.offline = true;
+ this.localAccount.username = name;
+ this.localAccount.localStorageKey = password;
+ this.localAccount.avatar = avatar;
+ this.accountService.createAccount(this.localAccount);
+ } else {
+ this.onlineAccount.offline = false;
+ this.onlineAccount.username = name;
+ this.onlineAccount.email = email;
+ this.onlineAccount.password = password;
+ this.onlineAccount.avatar = avatar;
+ this.accountService.createAccount(this.onlineAccount);
+ }
+ }
+
+ private handleAction() {
+ switch (this.action) {
+ case 'local':
+ console.log('Account creation mode');
+ this.offline = true;
+ // Initialize form for creation
+ break;
+ case 'online':
+ console.log('Account editing mode');
+ // Load account data and initialize form for editing
+ break;
+ default:
+ console.log('Unknown action:', this.action);
+ // Handle unknown action
+ }
+ }
+}
diff --git a/src/app/components/footer/footer.component.html b/src/app/components/footer/footer.component.html
index 571effe..58ae1af 100644
--- a/src/app/components/footer/footer.component.html
+++ b/src/app/components/footer/footer.component.html
@@ -7,18 +7,19 @@
-
-
+
+
+
+
+
+
+
+
diff --git a/src/app/components/footer/footer.component.ts b/src/app/components/footer/footer.component.ts
index f128036..446fa98 100644
--- a/src/app/components/footer/footer.component.ts
+++ b/src/app/components/footer/footer.component.ts
@@ -1,4 +1,4 @@
-import {Component, HostListener, inject, OnInit} from '@angular/core';
+import {Component, HostListener, inject, Input, OnInit} from '@angular/core';
import {VersionService} from '../../services/version.service';
import {AnnouncementService} from '../../services/announcement.service';
import {Announcement} from '../../interfaces/announcement';
@@ -11,6 +11,7 @@ import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {ModalService} from '../../services/modal.service';
import {TasksService} from '../../services/tasks.service';
import {NotificationService} from '../../services/notification.service';
+import {Account} from '../../interfaces/account';
@Component({
@@ -131,6 +132,49 @@ export class FooterComponent implements OnInit {
// Helper method to update settings in the service
+ @Input() useraccount!: Account|undefined;
+
+ getUserAvatar(): string {
+ if (!this.useraccount?.avatar) {
+ return 'assets/default-avatar.svg'; // Default avatar path
+ }
+
+ // If the avatar is already a data URL, return it directly
+ if (this.useraccount.avatar.startsWith('data:')) {
+ return this.useraccount.avatar;
+ }
+
+ // Otherwise, try to decode it (if it's just a base64 string without the data URL prefix)
+ try {
+ return this.decodeBase64(this.useraccount.avatar);
+ } catch (error) {
+ console.error('Error getting user avatar:', error);
+ return 'assets/default-avatar.png';
+ }
+ }
+
+ decodeBase64(base64String?: string): string {
+ if (!base64String) {
+ return 'assets/default-avatar.png';
+ }
+
+ // If it's already a data URL, return it
+ if (base64String.startsWith('data:')) {
+ return base64String;
+ }
+
+ try {
+ // Try to convert a plain base64 string to a data URL
+ // This assumes the base64String is just the encoded data without the data URL prefix
+ return `data:image/jpeg;base64,${base64String}`;
+ } catch (error) {
+ console.error('Error decoding Base64 string:', error);
+ return 'assets/default-avatar.png';
+ }
+ }
+
+
+
private updateSettings(updates: Partial
): void {
const currentSettings = this.settingsService.getSettings()();
if (currentSettings && currentSettings.length > 0) {
@@ -156,6 +200,7 @@ export class FooterComponent implements OnInit {
this.tasksService.clearAllTasks();
localStorage.removeItem('AGTASKS_SETTINGS')
localStorage.removeItem('AGTASKS_ANNOUNCEMENTS')
+ localStorage.removeItem('AGTASKS_ACCOUNT')
this.notificationService.addNotification(
'Success!',
'All your user data has been deleted.')
diff --git a/src/app/components/initial-choice/initial-choice.component.css b/src/app/components/initial-choice/initial-choice.component.css
new file mode 100644
index 0000000..f694abf
--- /dev/null
+++ b/src/app/components/initial-choice/initial-choice.component.css
@@ -0,0 +1,33 @@
+.dark-theme {
+ img {
+ filter: invert(1);
+ }
+ .card-choice {
+ cursor: pointer;
+ color: white;
+ opacity: 0.5;
+ transition: all 0.2s ease-in-out;
+ }
+ .card-choice:hover {
+ opacity: 1;
+ transition: all 0.2s ease-in-out;
+ }
+}
+
+.card-choice {
+ cursor: pointer;
+ opacity: 0.5;
+ transition: all 0.2s ease-in-out;
+}
+.card-choice:hover {
+ opacity: 1;
+ transition: all 0.2s ease-in-out;
+}
+
+.countdown-timer {
+ font-size: 1.2rem;
+ font-weight: bold;
+ text-align: center;
+ margin-bottom: 1rem;
+ color: #dc3545; /* Bootstrap danger color */
+}
diff --git a/src/app/components/initial-choice/initial-choice.component.html b/src/app/components/initial-choice/initial-choice.component.html
new file mode 100644
index 0000000..b7c4ac5
--- /dev/null
+++ b/src/app/components/initial-choice/initial-choice.component.html
@@ -0,0 +1,49 @@
+
+
+ @if(showCountdown) {
+
+ Redirecting in {{countdown}} seconds...
+
+
+ }
+
Initial Choice
+
+ Welcome aboard! As this is your first time using our application, please decide whether you’d prefer Offline Mode—offering full
+ functionality without an internet connection and storing all your data securely on your device—or Online Mode, which provides seamless
+ access anytime you’re connected, with your information synchronized safely to the cloud. Choose the option that best fits your
+ needs, and let’s get started!
+
+
+
+
+
+
+
+
+
+
Offline
+
Full functionality without an internet connection and storing all your data securely on your device.
+
+
+
+
+
+
+
+
+
+
+
Online
+
provides seamless access anytime you’re connected, with your information synchronized safely to the cloud.
+
+
+
+
+
+
diff --git a/src/app/components/initial-choice/initial-choice.component.spec.ts b/src/app/components/initial-choice/initial-choice.component.spec.ts
new file mode 100644
index 0000000..ebb50d6
--- /dev/null
+++ b/src/app/components/initial-choice/initial-choice.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { InitialChoiceComponent } from './initial-choice.component';
+
+describe('InitialChoiceComponent', () => {
+ let component: InitialChoiceComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [InitialChoiceComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(InitialChoiceComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/initial-choice/initial-choice.component.ts b/src/app/components/initial-choice/initial-choice.component.ts
new file mode 100644
index 0000000..85f4ab1
--- /dev/null
+++ b/src/app/components/initial-choice/initial-choice.component.ts
@@ -0,0 +1,79 @@
+import {Component, inject} from '@angular/core';
+import {SettingsService} from '../../services/settings.service';
+import {NgClass, NgOptimizedImage} from '@angular/common';
+import {DarkModeService} from '../../services/dark-mode.service';
+import {NotificationService} from '../../services/notification.service';
+import {Router} from '@angular/router';
+
+@Component({
+ selector: 'app-initial-choice',
+ imports: [
+ NgClass,
+ NgOptimizedImage
+ ],
+ templateUrl: './initial-choice.component.html',
+ styleUrl: './initial-choice.component.css'
+})
+export class InitialChoiceComponent {
+
+ private readonly initialChoice: string|undefined = undefined;
+ private settingsService: SettingsService = inject(SettingsService);
+ private notificationService: NotificationService = inject(NotificationService);
+
+ public countdown: number = 5;
+ public showCountdown: boolean = false;
+
+ constructor(private darkModeService: DarkModeService,private router: Router
+ ) {
+ this.initialChoice = this.settingsService.getSettings()()[0]?.initialChoice;
+ }
+
+ get isDarkMode(): boolean {
+ return this.darkModeService.isDarkMode();
+ }
+
+ isInitialChoice(): boolean {
+ return this.initialChoice !== undefined;
+ }
+
+ setInitialChoice(choice: string) {
+ const currentSettings = this.settingsService.getSettings()();
+
+ // Create a new settings array with the initialChoice updated
+ const updatedSettings = currentSettings.map((setting, index) => {
+ if (index === 0) {
+ // Update the first settings object with the new initialChoice
+ return {
+ ...setting,
+ initialChoice: choice
+ };
+ }
+ return setting;
+ });
+
+
+ console.log("Initial choice set to: " + choice);
+ if (choice === 'offline') {
+ // Update the settings with the new array
+ this.settingsService.updateSettings(updatedSettings);
+ // Redirect to home
+ void this.router.navigate(['/account/create/local']);
+ } else if(choice === 'online') {
+ console.log("Online mode not implemented yet.")
+ this.notificationService.addNotification("Info:", "Online mode not implemented yet.");
+
+ // sleep and show countdown 5 sec
+ // this.showCountdown = true;
+ // const intervalId = setInterval(() => {
+ // this.countdown--;
+ //
+ // if (this.countdown <= 0) {
+ // clearInterval(intervalId);
+ // window.location.href = '/';
+ // }
+ // }, 1000);
+ }
+
+ }
+
+}
diff --git a/src/app/components/page/tasks/tasks.component.css b/src/app/components/page/tasks/tasks.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/components/page/tasks/tasks.component.html b/src/app/components/page/tasks/tasks.component.html
new file mode 100644
index 0000000..a5e5bfa
--- /dev/null
+++ b/src/app/components/page/tasks/tasks.component.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/src/app/components/page/tasks/tasks.component.spec.ts b/src/app/components/page/tasks/tasks.component.spec.ts
new file mode 100644
index 0000000..220a8e5
--- /dev/null
+++ b/src/app/components/page/tasks/tasks.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TasksComponent } from './tasks.component';
+
+describe('TasksComponent', () => {
+ let component: TasksComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [TasksComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(TasksComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/page/tasks/tasks.component.ts b/src/app/components/page/tasks/tasks.component.ts
new file mode 100644
index 0000000..bbe2248
--- /dev/null
+++ b/src/app/components/page/tasks/tasks.component.ts
@@ -0,0 +1,21 @@
+import {Component, inject} from '@angular/core';
+import {FilterControlsComponent} from '../../filter-controls/filter-controls.component';
+import {TaskAddComponent} from '../../task-add/task-add.component';
+import {TodoProgressbarComponent} from '../../todo-progressbar/todo-progressbar.component';
+import {TaskListComponent} from '../../task-list/task-list.component';
+import {SettingsService} from '../../../services/settings.service';
+
+@Component({
+ selector: 'app-tasks',
+ imports: [FilterControlsComponent, TaskAddComponent, TodoProgressbarComponent, TaskListComponent],
+ templateUrl: './tasks.component.html',
+ styleUrl: './tasks.component.css'
+})
+export class TasksComponent {
+ private settingsService = inject(SettingsService)
+ protected settings = this.settingsService.getSettings();
+
+ get showProgressBar():boolean {
+ return this.settings()[0]?.showProgressBar || false
+ }
+}
diff --git a/src/app/components/task-list/task-list.component.html b/src/app/components/task-list/task-list.component.html
index a15dba4..8b1c5cc 100644
--- a/src/app/components/task-list/task-list.component.html
+++ b/src/app/components/task-list/task-list.component.html
@@ -9,7 +9,7 @@