diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md
new file mode 100644
index 0000000..3fb18d6
--- /dev/null
+++ b/.agents/AGENTS.md
@@ -0,0 +1,3 @@
+# Project Rules
+
+- **Git Operations**: Always ask the user for explicit confirmation/permission before executing any `git push` command (including pushes to remote branches or pull requests).
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..d5cb679
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,8 @@
+allowBuilds:
+ '@firebase/util': set this to true or false
+ '@parcel/watcher': set this to true or false
+ core-js: set this to true or false
+ esbuild: set this to true or false
+ lmdb: set this to true or false
+ msgpackr-extract: set this to true or false
+ protobufjs: set this to true or false
diff --git a/src/app/home/course/course.page.html b/src/app/home/course/course.page.html
index 6e41f71..a2636f7 100644
--- a/src/app/home/course/course.page.html
+++ b/src/app/home/course/course.page.html
@@ -8,6 +8,9 @@
{{ (courseProgress.duration - courseProgress.viewed) / 3600 | number:'1.0-1' }} hours left
}
+
+
+
diff --git a/src/app/home/course/course.page.scss b/src/app/home/course/course.page.scss
index cd800bf..7eab2e3 100644
--- a/src/app/home/course/course.page.scss
+++ b/src/app/home/course/course.page.scss
@@ -3,7 +3,7 @@ span a {
}
ion-title small {
- color: gray;
+ color: var(--flick-subtext-color, gray);
}
.e-learning-tag {
@@ -12,26 +12,17 @@ ion-title small {
span.date {
font-family: monospace, monospace;
- color: darkblue;
- @media (prefers-color-scheme: dark) {
- color: lightblue;
- }
+ color: var(--flick-date-color, darkblue);
}
span.date-divider {
margin: 0 0.2rem 0 0.2rem;
opacity: 0.5;
- color: darkblue;
- @media (prefers-color-scheme: dark) {
- color: lightblue;
- }
+ color: var(--flick-date-divider-color, darkblue);
}
.time-info {
- color: darkgoldenrod;
- @media (prefers-color-scheme: dark) {
- color: yellow;
- }
+ color: var(--flick-time-info-color, darkgoldenrod);
}
.ion-color .check-icon {
diff --git a/src/app/home/course/course.page.ts b/src/app/home/course/course.page.ts
index ca13bf6..ff615d3 100644
--- a/src/app/home/course/course.page.ts
+++ b/src/app/home/course/course.page.ts
@@ -10,6 +10,7 @@ import {
AlertController,
IonBackButton,
IonButton,
+ IonButtons,
IonCard,
IonCardContent,
IonCardHeader,
@@ -39,6 +40,8 @@ import type Player from 'video.js/dist/types/player';
import {ulid} from 'ulid';
import {AsyncPipe, DatePipe, DecimalPipe, NgClass} from '@angular/common';
import {ModalEvaluationComponent} from './modal-evaluation.component';
+import {ThemeService} from '../../theme.service';
+import {ThemeDropdownComponent} from '../../shared/theme-dropdown.component';
@Component({
selector: 'app-course',
@@ -70,6 +73,8 @@ import {ModalEvaluationComponent} from './modal-evaluation.component';
AsyncPipe,
DecimalPipe,
DatePipe,
+ IonButtons,
+ ThemeDropdownComponent,
]
})
export class CoursePage implements OnInit, AfterViewInit, OnDestroy {
@@ -79,6 +84,7 @@ export class CoursePage implements OnInit, AfterViewInit, OnDestroy {
private alertController = inject(AlertController);
private sanitizer = inject(DomSanitizer);
private modalCtrl = inject(ModalController);
+ themeService = inject(ThemeService);
@ViewChild('videoPlayer') videoPlayerElement: ElementRef;
videoPlayer: Player;
diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html
index 00a3e5b..83da16d 100644
--- a/src/app/home/home.page.html
+++ b/src/app/home/home.page.html
@@ -4,9 +4,10 @@
MDCU Recorded Lecture
+
Sign out
-
+
@@ -34,7 +35,7 @@
{{ response.last_played.video.course.name }}
@for (folder of Object.keys(response.years); track folder) {
-
{{ folder }}
diff --git a/src/app/home/home.page.scss b/src/app/home/home.page.scss
index 9879ce2..ed4e279 100644
--- a/src/app/home/home.page.scss
+++ b/src/app/home/home.page.scss
@@ -14,13 +14,7 @@ ion-card-title {
}
.course-name {
- color: #222222;
-}
-
-@media (prefers-color-scheme: dark) {
- .course-name {
- color: #cccccc;
- }
+ color: var(--flick-card-title-color, #222222);
}
.center-text {
@@ -33,21 +27,14 @@ ion-card-title {
}
.small-text {
- color: #aaaaaa;
+ color: var(--flick-subtext-color, #aaaaaa);
a {
- color: #bbbbbb;
- }
-
- @media (prefers-color-scheme: dark) {
- color: #555555;
- a {
- color: #666666;
- }
+ color: var(--flick-subtext-link-color, #bbbbbb);
}
}
.hover-card {
- transition: transform 0.2s ease;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
}
@@ -57,12 +44,12 @@ ion-card-title {
p {
padding: 0 1.5rem 0 1.5rem;
- color: gray;
+ color: var(--flick-subtext-color, gray);
text-align: center;
ion-icon {
margin: 0 0.6em;
- color: gray;
+ color: var(--flick-subtext-color, gray);
}
a {
@@ -72,4 +59,22 @@ p {
.text-sign-out {
margin-right: 0.5rem;
+}
+
+:host-context(body.pink-theme) {
+ ion-card-title {
+ text-shadow: 0 1px 3px rgba(136, 14, 79, 0.3);
+ }
+
+ .hover-card {
+ box-shadow: 0 2px 12px rgba(233, 30, 144, 0.2);
+
+ &:hover {
+ box-shadow: 0 6px 20px rgba(233, 30, 144, 0.35);
+ }
+ }
+
+ .center-spinner {
+ color: var(--ion-color-primary);
+ }
}
\ No newline at end of file
diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts
index 4a3b9c4..1c85241 100644
--- a/src/app/home/home.page.ts
+++ b/src/app/home/home.page.ts
@@ -3,9 +3,11 @@ import {Observable} from 'rxjs';
import {CourseListResponse, Lecture, ManService} from '../man.service';
import {Router, RouterLink} from '@angular/router';
import {AuthService} from '../auth.service';
-import {colorByFolderName} from '../../helpers';
+import {ThemeService} from '../theme.service';
+import {colorByFolderName, colorByFolderNamePink} from '../../helpers';
import {addIcons} from "ionicons";
import {logOutOutline} from "ionicons/icons";
+import {ThemeDropdownComponent} from '../shared/theme-dropdown.component';
import {
IonButton,
IonButtons,
@@ -32,12 +34,13 @@ import {AsyncPipe, NgStyle} from '@angular/common';
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
- imports: [IonHeader, IonToolbar, IonTitle, IonButtons, IonButton, IonIcon, IonContent, IonGrid, IonRow, IonCol, IonCard, RouterLink, NgStyle, IonCardHeader, IonCardTitle, AsyncPipe, IonCardContent, IonItem, IonLabel, IonText, IonSpinner]
+ imports: [IonHeader, IonToolbar, IonTitle, IonButtons, IonButton, IonIcon, IonContent, IonGrid, IonRow, IonCol, IonCard, RouterLink, NgStyle, IonCardHeader, IonCardTitle, AsyncPipe, IonCardContent, IonItem, IonLabel, IonText, IonSpinner, ThemeDropdownComponent]
})
export class HomePage implements OnInit {
private manService = inject(ManService);
private router = inject(Router);
private authService = inject(AuthService);
+ themeService = inject(ThemeService);
response$: Observable;
@@ -56,6 +59,13 @@ export class HomePage implements OnInit {
}
protected readonly colorByFolderName = colorByFolderName;
+ protected readonly colorByFolderNamePink = colorByFolderNamePink;
+
+ getCardColor(folder: string): string {
+ return this.themeService.isPinkMode ? colorByFolderNamePink(folder) : colorByFolderName(folder);
+ }
+
+
goToLastVideo(lastVideo: Lecture) {
return this.router.navigate(['home', 'course', lastVideo.course.id]);
diff --git a/src/app/home/list/list.page.html b/src/app/home/list/list.page.html
index 4a3a5df..fa42bdb 100644
--- a/src/app/home/list/list.page.html
+++ b/src/app/home/list/list.page.html
@@ -2,8 +2,11 @@
- {{ year || 'Loading'}}
+ {{ year || 'Loading'}}
+
+
+
diff --git a/src/app/home/list/list.page.scss b/src/app/home/list/list.page.scss
index 17dd012..ddfaa5c 100644
--- a/src/app/home/list/list.page.scss
+++ b/src/app/home/list/list.page.scss
@@ -7,3 +7,18 @@ ion-label {
text-overflow: initial !important;
white-space: normal !important;
}
+
+/* Pink Theme overrides */
+
+:host-context(body.pink-theme) {
+ ion-title > span {
+ text-shadow: 0 1px 3px rgba(136, 14, 79, 0.4);
+ }
+
+ ion-item {
+ --ion-item-border-color: #F48FB1;
+ }
+}
+
+
+
diff --git a/src/app/home/list/list.page.ts b/src/app/home/list/list.page.ts
index 1662678..23e4978 100644
--- a/src/app/home/list/list.page.ts
+++ b/src/app/home/list/list.page.ts
@@ -3,24 +3,30 @@ import {ActivatedRoute, Router, RouterLink} from '@angular/router';
import {EMPTY, Observable} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
import {ManService} from '../../man.service';
-import {colorByFolderName} from '../../../helpers';
-import {IonBackButton, IonContent, IonHeader, IonItem, IonLabel, IonList, IonTitle, IonToolbar} from '@ionic/angular/standalone';
+import {colorByFolderName, colorByFolderNamePink} from '../../../helpers';
+import {ThemeService} from '../../theme.service';
+import {ThemeDropdownComponent} from '../../shared/theme-dropdown.component';
+import {IonBackButton, IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonItem, IonLabel, IonList, IonTitle, IonToolbar} from '@ionic/angular/standalone';
import {AsyncPipe, NgStyle} from '@angular/common';
@Component({
selector: 'app-list',
templateUrl: './list.page.html',
styleUrls: ['./list.page.scss'],
- imports: [IonHeader, IonToolbar, RouterLink, IonBackButton, IonTitle, NgStyle, IonContent, IonList, IonItem, IonLabel, AsyncPipe]
+ imports: [IonHeader, IonToolbar, RouterLink, IonBackButton, IonTitle, NgStyle, IonContent, IonList, IonItem, IonLabel, AsyncPipe, IonButtons, ThemeDropdownComponent]
})
export class ListPage implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private manService = inject(ManService);
+ themeService = inject(ThemeService);
year: string;
list$: Observable<{ name: string, is_remote: boolean, id: number, link: string[] }[]>;
+ constructor() {
+ }
+
ngOnInit() {
this.list$ = this.route.paramMap.pipe(
switchMap(s => {
@@ -40,5 +46,11 @@ export class ListPage implements OnInit {
);
}
+ getYearColor(): string {
+ return this.themeService.isPinkMode ? colorByFolderNamePink(this.year) : colorByFolderName(this.year);
+ }
+
+
+
protected readonly colorByFolderName = colorByFolderName;
}
diff --git a/src/app/shared/theme-dropdown.component.ts b/src/app/shared/theme-dropdown.component.ts
new file mode 100644
index 0000000..ecb17bf
--- /dev/null
+++ b/src/app/shared/theme-dropdown.component.ts
@@ -0,0 +1,127 @@
+import {Component, inject} from '@angular/core';
+import {
+ IonButton,
+ IonIcon,
+ IonItem,
+ IonLabel,
+ IonList,
+ PopoverController,
+} from '@ionic/angular/standalone';
+import {ThemeService, THEME_OPTIONS, ThemeMode} from '../theme.service';
+import {AsyncPipe, NgClass} from '@angular/common';
+import {addIcons} from 'ionicons';
+import {sunny, moon, heart, phonePortrait, checkmark} from 'ionicons/icons';
+
+/**
+ * Popover content showing all theme options.
+ * This is presented inside the popover triggered by ThemeDropdownComponent.
+ */
+@Component({
+ selector: 'app-theme-popover',
+ template: `
+
+ @for (option of themeOptions; track option.mode) {
+
+
+ {{ option.label }}
+ @if (option.mode === (themeService.currentTheme$ | async)) {
+
+ }
+
+ }
+
+ `,
+ styles: [`
+ .theme-popover-list {
+ padding: 0.25rem 0;
+ }
+
+ .theme-popover-list ion-item {
+ --min-height: 44px;
+ cursor: pointer;
+ }
+
+ .theme-option-icon {
+ font-size: 1.2rem;
+ margin-inline-end: 0.75rem;
+ }
+
+ .active-theme {
+ --background: var(--ion-color-primary-tint, rgba(56, 128, 255, 0.08));
+ font-weight: 600;
+ }
+ `],
+ imports: [IonList, IonItem, IonIcon, IonLabel, AsyncPipe],
+})
+export class ThemePopoverComponent {
+ themeService = inject(ThemeService);
+ private popoverCtrl = inject(PopoverController);
+
+ /** Expose the theme options config to the template */
+ themeOptions = THEME_OPTIONS;
+
+ selectTheme(mode: ThemeMode): void {
+ this.themeService.setTheme(mode);
+ this.popoverCtrl.dismiss();
+ }
+}
+
+/**
+ * Reusable theme dropdown trigger button.
+ * Place this in any toolbar or container — it renders a single icon button
+ * that opens a popover with all theme options.
+ */
+@Component({
+ selector: 'app-theme-dropdown',
+ template: `
+
+
+
+ `,
+ styles: [`
+ :host {
+ display: inline-flex;
+ align-items: center;
+ }
+ `],
+ imports: [IonButton, IonIcon],
+})
+export class ThemeDropdownComponent {
+ themeService = inject(ThemeService);
+ private popoverCtrl = inject(PopoverController);
+
+ constructor() {
+ addIcons({sunny, moon, heart, phonePortrait, checkmark});
+ }
+
+ /** Map the current theme to the appropriate trigger icon */
+ getIcon(): string {
+ const option = THEME_OPTIONS.find(o => o.mode === this.themeService.currentTheme);
+ return option?.icon ?? 'sunny';
+ }
+
+ /** Map the current theme to icon color */
+ getIconColor(): string {
+ const option = THEME_OPTIONS.find(o => o.mode === this.themeService.currentTheme);
+ return option?.iconColor ?? 'warning';
+ }
+
+ async openPopover(event: Event): Promise {
+ const popover = await this.popoverCtrl.create({
+ component: ThemePopoverComponent,
+ event,
+ translucent: true,
+ showBackdrop: false,
+ dismissOnSelect: true,
+ });
+ await popover.present();
+ }
+}
diff --git a/src/app/theme.service.ts b/src/app/theme.service.ts
new file mode 100644
index 0000000..84e5b32
--- /dev/null
+++ b/src/app/theme.service.ts
@@ -0,0 +1,177 @@
+import {Injectable} from '@angular/core';
+import {BehaviorSubject, Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+
+/** The user-selectable theme modes */
+export type ThemeMode = 'light' | 'dark' | 'pink' | 'device';
+
+/** Config entry for each theme option (used by the dropdown component) */
+export interface ThemeOption {
+ mode: ThemeMode;
+ label: string;
+ icon: string;
+ iconColor: string;
+}
+
+/** All available theme options — single source of truth for the dropdown */
+export const THEME_OPTIONS: ThemeOption[] = [
+ {mode: 'light', label: 'Light theme', icon: 'sunny', iconColor: 'warning'},
+ {mode: 'dark', label: 'Dark theme', icon: 'moon', iconColor: 'medium'},
+ {mode: 'pink', label: 'Pink theme', icon: 'heart', iconColor: 'danger'},
+ {mode: 'device', label: 'Device default', icon: 'phone-portrait', iconColor: 'medium'},
+];
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ThemeService {
+ private readonly STORAGE_KEY_PREFIX = 'themePrefs_';
+ private currentUserId = 'guest';
+
+ private themeMode = new BehaviorSubject('device');
+
+ /** Observable: the user-selected theme mode */
+ currentTheme$ = this.themeMode.asObservable();
+
+ /** Observable: the resolved effective theme (device → actual light/dark) */
+ effectiveTheme$: Observable<'light' | 'dark' | 'pink'> = this.themeMode.pipe(
+ map(mode => this.resolveEffective(mode)),
+ );
+
+ /** Backward-compatible observables for CSS class logic */
+ isDarkMode$ = this.effectiveTheme$.pipe(map(t => t === 'dark'));
+ isPinkMode$ = this.effectiveTheme$.pipe(map(t => t === 'pink'));
+
+ private systemDarkMediaQuery: MediaQueryList | null = null;
+ private systemDarkListener: ((e: MediaQueryListEvent) => void) | null = null;
+
+ constructor() {
+ this.setupSystemMediaListener();
+ this.initTheme();
+ }
+
+ /** Set up listener for system dark mode changes */
+ private setupSystemMediaListener(): void {
+ if (typeof window !== 'undefined' && window.matchMedia) {
+ this.systemDarkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ this.systemDarkListener = () => {
+ // Re-apply theme if in device mode so CSS classes update
+ if (this.themeMode.value === 'device') {
+ this.applyTheme();
+ // Re-emit so derived observables update
+ this.themeMode.next('device');
+ }
+ };
+ this.systemDarkMediaQuery.addEventListener('change', this.systemDarkListener);
+ }
+ }
+
+ /** Initialize theme for current state/guest */
+ private initTheme(): void {
+ const mode = this.loadPreference(this.currentUserId);
+ this.themeMode.next(mode);
+ this.applyTheme();
+ }
+
+ /** Load preferences for a specific user (call on login) */
+ loadForUser(uid: string): void {
+ this.currentUserId = uid || 'guest';
+ const mode = this.loadPreference(this.currentUserId);
+ this.themeMode.next(mode);
+ this.applyTheme();
+ }
+
+ /** Clear preferences from active state (call on logout) */
+ clearForUser(): void {
+ this.currentUserId = 'guest';
+ const mode = this.loadPreference('guest');
+ this.themeMode.next(mode);
+ this.applyTheme();
+ }
+
+ /** Set the active theme mode */
+ setTheme(mode: ThemeMode): void {
+ this.themeMode.next(mode);
+ this.applyTheme();
+ this.savePreference();
+ }
+
+ /** Synchronous getter for backward compatibility */
+ get isDarkMode(): boolean {
+ return this.resolveEffective(this.themeMode.value) === 'dark';
+ }
+
+ get isPinkMode(): boolean {
+ return this.resolveEffective(this.themeMode.value) === 'pink';
+ }
+
+ get currentTheme(): ThemeMode {
+ return this.themeMode.value;
+ }
+
+ /** Resolve 'device' to actual light/dark based on system preference */
+ private resolveEffective(mode: ThemeMode): 'light' | 'dark' | 'pink' {
+ if (mode === 'device') {
+ const systemDark = this.systemDarkMediaQuery?.matches ?? false;
+ return systemDark ? 'dark' : 'light';
+ }
+ return mode;
+ }
+
+ /** Load preference from localStorage with migration from old format */
+ private loadPreference(uid: string): ThemeMode {
+ try {
+ const raw = localStorage.getItem(this.STORAGE_KEY_PREFIX + uid);
+ if (raw) {
+ const parsed = JSON.parse(raw);
+
+ // New format: { theme: 'light' | 'dark' | 'pink' | 'device' }
+ const validModes = THEME_OPTIONS.map(o => o.mode);
+ if (typeof parsed.theme === 'string' && validModes.includes(parsed.theme as ThemeMode)) {
+ return parsed.theme as ThemeMode;
+ }
+
+ // Old format migration: { darkMode: boolean, pinkMode: boolean }
+ if (typeof parsed.darkMode === 'boolean' || typeof parsed.pinkMode === 'boolean') {
+ if (parsed.pinkMode === true) {
+ return 'pink';
+ }
+ if (parsed.darkMode === true) {
+ return 'dark';
+ }
+ return 'light';
+ }
+ }
+ } catch {
+ // Ignore parse errors
+ }
+
+ // Default: follow device
+ return 'device';
+ }
+
+ private savePreference(): void {
+ localStorage.setItem(
+ this.STORAGE_KEY_PREFIX + this.currentUserId,
+ JSON.stringify({theme: this.themeMode.value}),
+ );
+ }
+
+ private applyTheme(): void {
+ const updateClasses = () => {
+ const body = document.body;
+ const effective = this.resolveEffective(this.themeMode.value);
+
+ body.classList.toggle('dark-theme', effective === 'dark');
+ body.classList.toggle('pink-theme', effective === 'pink');
+ };
+
+ if (typeof document !== 'undefined' && 'startViewTransition' in document) {
+ (document as any).startViewTransition(() => {
+ updateClasses();
+ });
+ } else {
+ updateClasses();
+ }
+ }
+}
diff --git a/src/app/welcome/welcome.page.html b/src/app/welcome/welcome.page.html
index 9b5b603..f69bbce 100644
--- a/src/app/welcome/welcome.page.html
+++ b/src/app/welcome/welcome.page.html
@@ -1,4 +1,7 @@
+
diff --git a/src/app/welcome/welcome.page.scss b/src/app/welcome/welcome.page.scss
index fd92e92..080d8a3 100644
--- a/src/app/welcome/welcome.page.scss
+++ b/src/app/welcome/welcome.page.scss
@@ -7,6 +7,8 @@ ion-content {
#mainCard {
margin: 20vh auto 0.4rem auto;
border-radius: 1rem;
+ box-shadow: var(--flick-welcome-card-shadow, 0 4px 24px rgba(0, 0, 0, 0.08));
+ border: var(--flick-welcome-card-border, none);
}
.browserCard {
@@ -15,5 +17,16 @@ ion-content {
.small-text {
font-size: 0.75rem;
+ color: var(--flick-subtext-color, #aaaaaa);
}
}
+
+.theme-toggle-container {
+ position: absolute;
+ top: 0.5rem;
+ right: 0.5rem;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
diff --git a/src/app/welcome/welcome.page.ts b/src/app/welcome/welcome.page.ts
index 3b61c06..fe8a81d 100644
--- a/src/app/welcome/welcome.page.ts
+++ b/src/app/welcome/welcome.page.ts
@@ -16,6 +16,8 @@ import {HttpErrorResponse} from "@angular/common/http";
import {Subscription} from "rxjs";
import {User} from "@angular/fire/auth";
import {AuthService} from "../auth.service";
+import {ThemeService} from "../theme.service";
+import {ThemeDropdownComponent} from "../shared/theme-dropdown.component";
@Component({
selector: "app-welcome",
@@ -29,6 +31,7 @@ import {AuthService} from "../auth.service";
IonCardContent,
IonButton,
IonText,
+ ThemeDropdownComponent,
],
})
export class WelcomePage implements OnInit, OnDestroy {
@@ -37,6 +40,7 @@ export class WelcomePage implements OnInit, OnDestroy {
private manService = inject(ManService);
private loadingCtrl = inject(LoadingController);
alertCtrl = inject(AlertController);
+ themeService = inject(ThemeService);
authStateSubscription: Subscription;
isAuthChecked: boolean;
@@ -59,6 +63,7 @@ export class WelcomePage implements OnInit, OnDestroy {
this.authStateSubscription = this.authService.user.subscribe((user) => {
this.user = user;
if (user) {
+ this.themeService.loadForUser(user.uid);
// User is signed in.
this.goToHome().then(() => {
loading.dismiss();
@@ -97,6 +102,7 @@ export class WelcomePage implements OnInit, OnDestroy {
}
logout() {
+ this.themeService.clearForUser();
this.authService
.signOut()
.then()
diff --git a/src/global.scss b/src/global.scss
index 906b180..5d417d6 100644
--- a/src/global.scss
+++ b/src/global.scss
@@ -42,10 +42,40 @@
display: inline-block;
}
-@media (prefers-color-scheme: dark) {
- .e-learning-tag {
- border: #15803D solid 1px;
- background-color: #052E16;
- color: #15803D;
- }
+body.dark-theme .e-learning-tag {
+ border: #15803D solid 1px;
+ background-color: #052E16;
+ color: #15803D;
}
+
+$theme-transition-duration: 0.3s !default;
+$theme-transition-timing: ease !default;
+
+@mixin theme-transition {
+ transition: background-color $theme-transition-duration $theme-transition-timing,
+ color $theme-transition-duration $theme-transition-timing,
+ border-color $theme-transition-duration $theme-transition-timing,
+ box-shadow $theme-transition-duration $theme-transition-timing,
+ fill $theme-transition-duration $theme-transition-timing;
+}
+
+body,
+ion-content,
+ion-toolbar,
+ion-card,
+ion-item,
+ion-header,
+ion-footer,
+ion-title,
+ion-button,
+ion-list,
+.e-learning-tag {
+ @include theme-transition;
+}
+
+body.pink-theme .e-learning-tag {
+ border: #E91E90 solid 1px;
+ background-color: #FCE4EC;
+ color: #E91E90;
+}
+
diff --git a/src/helpers.ts b/src/helpers.ts
index e07f4e0..9f04787 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -11,3 +11,18 @@ export function colorByFolderName(name: string) {
};
return colorMap[name] || 'gray';
}
+
+export function colorByFolderNamePink(name: string) {
+ const colorMap = {
+ '1st year': '#F48FB1',
+ '2nd year': '#E91E90',
+ '3rd year': '#AD1457',
+ '4th year': '#C2185B',
+ '5th year': '#D81B60',
+ '6th year': '#880E4F',
+ 'NLE1': '#EC407A',
+ 'NLE2': '#F06292'
+ };
+ return colorMap[name] || '#B07090';
+}
+
diff --git a/src/theme/variables.scss b/src/theme/variables.scss
index e111d61..5c45561 100644
--- a/src/theme/variables.scss
+++ b/src/theme/variables.scss
@@ -74,156 +74,291 @@
--ion-color-light-contrast-rgb: 0, 0, 0;
--ion-color-light-shade: #d7d8da;
--ion-color-light-tint: #f5f6f9;
+
+ /** Custom FlickPlayer Design Tokens **/
+ --flick-date-color: darkblue;
+ --flick-date-divider-color: darkblue;
+ --flick-time-info-color: darkgoldenrod;
+ --flick-subtext-color: #aaaaaa;
+ --flick-subtext-link-color: #bbbbbb;
+ --flick-card-title-color: #222222;
+ --flick-welcome-card-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
+ --flick-welcome-card-border: none;
+}
+
+/*
+ * Dark Theme
+ * -------------------------------------------
+ * Toggled via body.dark-theme class
+ */
+
+body.dark-theme {
+ --ion-color-primary: #428cff;
+ --ion-color-primary-rgb: 66,140,255;
+ --ion-color-primary-contrast: #ffffff;
+ --ion-color-primary-contrast-rgb: 255,255,255;
+ --ion-color-primary-shade: #3a7be0;
+ --ion-color-primary-tint: #5598ff;
+
+ --ion-color-secondary: #50c8ff;
+ --ion-color-secondary-rgb: 80,200,255;
+ --ion-color-secondary-contrast: #ffffff;
+ --ion-color-secondary-contrast-rgb: 255,255,255;
+ --ion-color-secondary-shade: #46b0e0;
+ --ion-color-secondary-tint: #62ceff;
+
+ --ion-color-tertiary: #6a64ff;
+ --ion-color-tertiary-rgb: 106,100,255;
+ --ion-color-tertiary-contrast: #ffffff;
+ --ion-color-tertiary-contrast-rgb: 255,255,255;
+ --ion-color-tertiary-shade: #5d58e0;
+ --ion-color-tertiary-tint: #7974ff;
+
+ --ion-color-success: #2fdf75;
+ --ion-color-success-rgb: 47,223,117;
+ --ion-color-success-contrast: #000000;
+ --ion-color-success-contrast-rgb: 0,0,0;
+ --ion-color-success-shade: #29c467;
+ --ion-color-success-tint: #44e283;
+
+ --ion-color-warning: #ffd534;
+ --ion-color-warning-rgb: 255,213,52;
+ --ion-color-warning-contrast: #000000;
+ --ion-color-warning-contrast-rgb: 0,0,0;
+ --ion-color-warning-shade: #e0bb2e;
+ --ion-color-warning-tint: #ffd948;
+
+ --ion-color-danger: #ff4961;
+ --ion-color-danger-rgb: 255,73,97;
+ --ion-color-danger-contrast: #ffffff;
+ --ion-color-danger-contrast-rgb: 255,255,255;
+ --ion-color-danger-shade: #e04055;
+ --ion-color-danger-tint: #ff5b71;
+
+ --ion-color-dark: #f4f5f8;
+ --ion-color-dark-rgb: 244,245,248;
+ --ion-color-dark-contrast: #000000;
+ --ion-color-dark-contrast-rgb: 0,0,0;
+ --ion-color-dark-shade: #d7d8da;
+ --ion-color-dark-tint: #f5f6f9;
+
+ --ion-color-medium: #989aa2;
+ --ion-color-medium-rgb: 152,154,162;
+ --ion-color-medium-contrast: #000000;
+ --ion-color-medium-contrast-rgb: 0,0,0;
+ --ion-color-medium-shade: #86888f;
+ --ion-color-medium-tint: #a2a4ab;
+
+ --ion-color-light: #222428;
+ --ion-color-light-rgb: 34,36,40;
+ --ion-color-light-contrast: #ffffff;
+ --ion-color-light-contrast-rgb: 255,255,255;
+ --ion-color-light-shade: #1e2023;
+ --ion-color-light-tint: #383a3e;
+
+ /** Custom FlickPlayer Design Tokens **/
+ --flick-date-color: lightblue;
+ --flick-date-divider-color: lightblue;
+ --flick-time-info-color: yellow;
+ --flick-subtext-color: #555555;
+ --flick-subtext-link-color: #666666;
+ --flick-card-title-color: #cccccc;
+ --flick-welcome-card-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 1.5px 8px rgba(0, 0, 0, 0.2);
+ --flick-welcome-card-border: 1px solid rgba(255, 255, 255, 0.1);
}
-@media (prefers-color-scheme: dark) {
- /*
- * Dark Colors
- * -------------------------------------------
- */
-
- body {
- --ion-color-primary: #428cff;
- --ion-color-primary-rgb: 66,140,255;
- --ion-color-primary-contrast: #ffffff;
- --ion-color-primary-contrast-rgb: 255,255,255;
- --ion-color-primary-shade: #3a7be0;
- --ion-color-primary-tint: #5598ff;
-
- --ion-color-secondary: #50c8ff;
- --ion-color-secondary-rgb: 80,200,255;
- --ion-color-secondary-contrast: #ffffff;
- --ion-color-secondary-contrast-rgb: 255,255,255;
- --ion-color-secondary-shade: #46b0e0;
- --ion-color-secondary-tint: #62ceff;
-
- --ion-color-tertiary: #6a64ff;
- --ion-color-tertiary-rgb: 106,100,255;
- --ion-color-tertiary-contrast: #ffffff;
- --ion-color-tertiary-contrast-rgb: 255,255,255;
- --ion-color-tertiary-shade: #5d58e0;
- --ion-color-tertiary-tint: #7974ff;
-
- --ion-color-success: #2fdf75;
- --ion-color-success-rgb: 47,223,117;
- --ion-color-success-contrast: #000000;
- --ion-color-success-contrast-rgb: 0,0,0;
- --ion-color-success-shade: #29c467;
- --ion-color-success-tint: #44e283;
-
- --ion-color-warning: #ffd534;
- --ion-color-warning-rgb: 255,213,52;
- --ion-color-warning-contrast: #000000;
- --ion-color-warning-contrast-rgb: 0,0,0;
- --ion-color-warning-shade: #e0bb2e;
- --ion-color-warning-tint: #ffd948;
-
- --ion-color-danger: #ff4961;
- --ion-color-danger-rgb: 255,73,97;
- --ion-color-danger-contrast: #ffffff;
- --ion-color-danger-contrast-rgb: 255,255,255;
- --ion-color-danger-shade: #e04055;
- --ion-color-danger-tint: #ff5b71;
-
- --ion-color-dark: #f4f5f8;
- --ion-color-dark-rgb: 244,245,248;
- --ion-color-dark-contrast: #000000;
- --ion-color-dark-contrast-rgb: 0,0,0;
- --ion-color-dark-shade: #d7d8da;
- --ion-color-dark-tint: #f5f6f9;
-
- --ion-color-medium: #989aa2;
- --ion-color-medium-rgb: 152,154,162;
- --ion-color-medium-contrast: #000000;
- --ion-color-medium-contrast-rgb: 0,0,0;
- --ion-color-medium-shade: #86888f;
- --ion-color-medium-tint: #a2a4ab;
-
- --ion-color-light: #222428;
- --ion-color-light-rgb: 34,36,40;
- --ion-color-light-contrast: #ffffff;
- --ion-color-light-contrast-rgb: 255,255,255;
- --ion-color-light-shade: #1e2023;
- --ion-color-light-tint: #383a3e;
- }
-
- /*
- * iOS Dark Theme
- * -------------------------------------------
- */
-
- .ios body {
- --ion-background-color: #000000;
- --ion-background-color-rgb: 0,0,0;
-
- --ion-text-color: #ffffff;
- --ion-text-color-rgb: 255,255,255;
-
- --ion-color-step-50: #0d0d0d;
- --ion-color-step-100: #1a1a1a;
- --ion-color-step-150: #262626;
- --ion-color-step-200: #333333;
- --ion-color-step-250: #404040;
- --ion-color-step-300: #4d4d4d;
- --ion-color-step-350: #595959;
- --ion-color-step-400: #666666;
- --ion-color-step-450: #737373;
- --ion-color-step-500: #808080;
- --ion-color-step-550: #8c8c8c;
- --ion-color-step-600: #999999;
- --ion-color-step-650: #a6a6a6;
- --ion-color-step-700: #b3b3b3;
- --ion-color-step-750: #bfbfbf;
- --ion-color-step-800: #cccccc;
- --ion-color-step-850: #d9d9d9;
- --ion-color-step-900: #e6e6e6;
- --ion-color-step-950: #f2f2f2;
-
- --ion-toolbar-background: #0d0d0d;
-
- --ion-item-background: #1c1c1c;
- --ion-item-background-activated: #313131;
- }
-
-
- /*
- * Material Design Dark Theme
- * -------------------------------------------
- */
-
- .md body {
- --ion-background-color: #121212;
- --ion-background-color-rgb: 18,18,18;
-
- --ion-text-color: #ffffff;
- --ion-text-color-rgb: 255,255,255;
-
- --ion-border-color: #222222;
-
- --ion-color-step-50: #1e1e1e;
- --ion-color-step-100: #2a2a2a;
- --ion-color-step-150: #363636;
- --ion-color-step-200: #414141;
- --ion-color-step-250: #4d4d4d;
- --ion-color-step-300: #595959;
- --ion-color-step-350: #656565;
- --ion-color-step-400: #717171;
- --ion-color-step-450: #7d7d7d;
- --ion-color-step-500: #898989;
- --ion-color-step-550: #949494;
- --ion-color-step-600: #a0a0a0;
- --ion-color-step-650: #acacac;
- --ion-color-step-700: #b8b8b8;
- --ion-color-step-750: #c4c4c4;
- --ion-color-step-800: #d0d0d0;
- --ion-color-step-850: #dbdbdb;
- --ion-color-step-900: #e7e7e7;
- --ion-color-step-950: #f3f3f3;
-
- --ion-item-background: #1A1B1E;
- }
-
- ion-title.title-large {
- --color: white;
- }
+.ios body.dark-theme {
+ --ion-background-color: #000000;
+ --ion-background-color-rgb: 0,0,0;
+
+ --ion-text-color: #ffffff;
+ --ion-text-color-rgb: 255,255,255;
+
+ --ion-color-step-50: #0d0d0d;
+ --ion-color-step-100: #1a1a1a;
+ --ion-color-step-150: #262626;
+ --ion-color-step-200: #333333;
+ --ion-color-step-250: #404040;
+ --ion-color-step-300: #4d4d4d;
+ --ion-color-step-350: #595959;
+ --ion-color-step-400: #666666;
+ --ion-color-step-450: #737373;
+ --ion-color-step-500: #808080;
+ --ion-color-step-550: #8c8c8c;
+ --ion-color-step-600: #999999;
+ --ion-color-step-650: #a6a6a6;
+ --ion-color-step-700: #b3b3b3;
+ --ion-color-step-750: #bfbfbf;
+ --ion-color-step-800: #cccccc;
+ --ion-color-step-850: #d9d9d9;
+ --ion-color-step-900: #e6e6e6;
+ --ion-color-step-950: #f2f2f2;
+
+ --ion-toolbar-background: #0d0d0d;
+
+ --ion-item-background: #1c1c1c;
+ --ion-item-background-activated: #313131;
+}
+
+.md body.dark-theme {
+ --ion-background-color: #121212;
+ --ion-background-color-rgb: 18,18,18;
+
+ --ion-text-color: #ffffff;
+ --ion-text-color-rgb: 255,255,255;
+
+ --ion-border-color: #222222;
+
+ --ion-color-step-50: #1e1e1e;
+ --ion-color-step-100: #2a2a2a;
+ --ion-color-step-150: #363636;
+ --ion-color-step-200: #414141;
+ --ion-color-step-250: #4d4d4d;
+ --ion-color-step-300: #595959;
+ --ion-color-step-350: #656565;
+ --ion-color-step-400: #717171;
+ --ion-color-step-450: #7d7d7d;
+ --ion-color-step-500: #898989;
+ --ion-color-step-550: #949494;
+ --ion-color-step-600: #a0a0a0;
+ --ion-color-step-650: #acacac;
+ --ion-color-step-700: #b8b8b8;
+ --ion-color-step-750: #c4c4c4;
+ --ion-color-step-800: #d0d0d0;
+ --ion-color-step-850: #dbdbdb;
+ --ion-color-step-900: #e7e7e7;
+ --ion-color-step-950: #f3f3f3;
+
+ --ion-item-background: #1A1B1E;
+}
+
+body.dark-theme ion-title.title-large {
+ --color: white;
+}
+
+/*
+ * Pink Theme
+ * -------------------------------------------
+ * Toggled via body.pink-theme class
+ */
+
+body.pink-theme {
+ /** primary — hot pink **/
+ --ion-color-primary: #E91E90;
+ --ion-color-primary-rgb: 233, 30, 144;
+ --ion-color-primary-contrast: #ffffff;
+ --ion-color-primary-contrast-rgb: 255, 255, 255;
+ --ion-color-primary-shade: #CD1A7F;
+ --ion-color-primary-tint: #EB359C;
+
+ /** secondary — soft rose **/
+ --ion-color-secondary: #F06292;
+ --ion-color-secondary-rgb: 240, 98, 146;
+ --ion-color-secondary-contrast: #ffffff;
+ --ion-color-secondary-contrast-rgb: 255, 255, 255;
+ --ion-color-secondary-shade: #D35680;
+ --ion-color-secondary-tint: #F2729D;
+
+ /** tertiary — deep magenta **/
+ --ion-color-tertiary: #AD1457;
+ --ion-color-tertiary-rgb: 173, 20, 87;
+ --ion-color-tertiary-contrast: #ffffff;
+ --ion-color-tertiary-contrast-rgb: 255, 255, 255;
+ --ion-color-tertiary-shade: #98124D;
+ --ion-color-tertiary-tint: #B52C68;
+
+ /** success — pink-tinted green **/
+ --ion-color-success: #E91E63;
+ --ion-color-success-rgb: 233, 30, 99;
+ --ion-color-success-contrast: #ffffff;
+ --ion-color-success-contrast-rgb: 255, 255, 255;
+ --ion-color-success-shade: #CD1A57;
+ --ion-color-success-tint: #EB3573;
+
+ /** warning — warm peach **/
+ --ion-color-warning: #FF8A80;
+ --ion-color-warning-rgb: 255, 138, 128;
+ --ion-color-warning-contrast: #000000;
+ --ion-color-warning-contrast-rgb: 0, 0, 0;
+ --ion-color-warning-shade: #E07970;
+ --ion-color-warning-tint: #FF968D;
+
+ /** danger — deep crimson **/
+ --ion-color-danger: #C2185B;
+ --ion-color-danger-rgb: 194, 24, 91;
+ --ion-color-danger-contrast: #ffffff;
+ --ion-color-danger-contrast-rgb: 255, 255, 255;
+ --ion-color-danger-shade: #AB1550;
+ --ion-color-danger-tint: #C82F6B;
+
+ /** dark **/
+ --ion-color-dark: #4A0E2B;
+ --ion-color-dark-rgb: 74, 14, 43;
+ --ion-color-dark-contrast: #ffffff;
+ --ion-color-dark-contrast-rgb: 255, 255, 255;
+ --ion-color-dark-shade: #410C26;
+ --ion-color-dark-tint: #5C2640;
+
+ /** medium **/
+ --ion-color-medium: #B07090;
+ --ion-color-medium-rgb: 176, 112, 144;
+ --ion-color-medium-contrast: #ffffff;
+ --ion-color-medium-contrast-rgb: 255, 255, 255;
+ --ion-color-medium-shade: #9B637F;
+ --ion-color-medium-tint: #B87E9B;
+
+ /** light **/
+ --ion-color-light: #FFF0F5;
+ --ion-color-light-rgb: 255, 240, 245;
+ --ion-color-light-contrast: #4A0E2B;
+ --ion-color-light-contrast-rgb: 74, 14, 43;
+ --ion-color-light-shade: #E0D3D8;
+ --ion-color-light-tint: #FFF2F6;
+
+ --ion-background-color: #FFF0F5;
+ --ion-background-color-rgb: 255, 240, 245;
+
+ --ion-text-color: #4A0E2B;
+ --ion-text-color-rgb: 74, 14, 43;
+
+ --ion-toolbar-background: #F8BBD0;
+ --ion-toolbar-color: #4A0E2B;
+
+ --ion-item-background: #FCE4EC;
+ --ion-item-background-activated: #F8BBD0;
+
+ --ion-border-color: #F48FB1;
+
+ --ion-color-step-50: #FCE4EC;
+ --ion-color-step-100: #F8BBD0;
+ --ion-color-step-150: #F48FB1;
+ --ion-color-step-200: #F06292;
+ --ion-color-step-250: #EC407A;
+ --ion-color-step-300: #E91E63;
+ --ion-color-step-350: #D81B60;
+ --ion-color-step-400: #C2185B;
+ --ion-color-step-450: #AD1457;
+ --ion-color-step-500: #880E4F;
+ --ion-color-step-550: #7B0C47;
+ --ion-color-step-600: #6E0A3F;
+ --ion-color-step-650: #610937;
+ --ion-color-step-700: #54072F;
+ --ion-color-step-750: #470627;
+ --ion-color-step-800: #3A041F;
+ --ion-color-step-850: #2D0317;
+ --ion-color-step-900: #20020F;
+ --ion-color-step-950: #130107;
+
+ --ion-card-background: #FCE4EC;
+ --ion-tab-bar-background: #F8BBD0;
+
+ /** Custom FlickPlayer Design Tokens **/
+ --flick-date-color: #AD1457;
+ --flick-date-divider-color: #AD1457;
+ --flick-time-info-color: #E91E90;
+ --flick-subtext-color: #C48BA0;
+ --flick-subtext-link-color: #D4A0B5;
+ --flick-card-title-color: white;
+ --flick-welcome-card-shadow: 0 4px 24px rgba(233, 30, 144, 0.18), 0 1.5px 8px rgba(244, 143, 177, 0.12);
+ --flick-welcome-card-border: 1px solid rgba(244, 143, 177, 0.3);
}