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