unoffical wafrn mirror wafrn.net
atproto social-network activitypub
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(theme-service,settings-service): Theme names and functionality

+271 -154
+4 -4
packages/frontend/src/app/components/color-scheme-switcher/color-scheme-switcher.component.html
··· 5 5 title="Change color scheme" 6 6 aria-label="Change color scheme" 7 7 [ngClass]="iconClass" 8 - matTooltip="{{ colorThemeData[theme()] }} mode" 8 + matTooltip="{{ lightDarkModeData[theme()] }} mode" 9 9 > 10 10 <svg 11 11 xmlns="http://www.w3.org/2000/svg" ··· 35 35 </svg> 36 36 </button> 37 37 <mat-menu #themeMenu="matMenu"> 38 - @for (entry of colorThemeData | keyvalue: null; track entry.key) { 38 + @for (entry of lightDarkModeData | keyvalue: null; track entry.key) { 39 39 <button 40 40 class="theme-selector-button" 41 41 [class.selected]="theme() === entry.key" ··· 53 53 class="theme-toggle" 54 54 title="Toggle theme" 55 55 aria-label="Toggle theme" 56 - matTooltip="{{ colorSchemeData[colorScheme()].name }} theme" 56 + matTooltip="{{ themeData[colorScheme()].name }} theme" 57 57 > 58 58 <fa-icon [icon]="paletteIcon"></fa-icon> 59 59 </button> ··· 76 76 (click)="setColorScheme(variant)" 77 77 [class.selected]="colorScheme() === variant" 78 78 > 79 - {{ $any(colorSchemeData)[variant].name }} 79 + {{ $any(themeData)[variant].name }} 80 80 </button> 81 81 } 82 82 </ng-template>
+16 -16
packages/frontend/src/app/components/color-scheme-switcher/color-scheme-switcher.component.ts
··· 9 9 import { 10 10 AdditionalStyleMode, 11 11 additionalStyleModesData, 12 - ColorScheme, 13 - colorSchemeData, 14 - colorSchemeGroupList, 15 - ColorSchemeGroupList, 16 - ColorTheme, 17 - colorThemeData, 12 + Theme, 13 + themeData, 14 + themeGroupList, 15 + ThemeGroupList, 16 + LightDarkMode, 17 + lightDarkModeData, 18 18 ThemeService 19 19 } from 'src/app/services/theme.service' 20 20 ··· 37 37 styleUrl: './color-scheme-switcher.component.scss' 38 38 }) 39 39 export class ColorSchemeSwitcherComponent { 40 - colorScheme: Signal<ColorScheme> 41 - theme: Signal<ColorTheme> 40 + colorScheme: Signal<Theme> 41 + theme: Signal<LightDarkMode> 42 42 additionalStyleModes: { [key in AdditionalStyleMode]: WritableSignal<boolean> } 43 43 44 44 // Data copies 45 - colorSchemeData = colorSchemeData 46 - colorThemeData = colorThemeData 45 + themeData = themeData 46 + lightDarkModeData = lightDarkModeData 47 47 additionalStyleModesData = additionalStyleModesData 48 48 49 49 // Function copies ··· 52 52 toggleAdditionalStyleMode: Function 53 53 54 54 // Theme categories 55 - colorSchemeGroupList: ColorSchemeGroupList 55 + colorSchemeGroupList: ThemeGroupList 56 56 57 57 // Icons 58 58 paletteIcon = faPalette ··· 61 61 iconClass = '' 62 62 63 63 constructor(themeService: ThemeService) { 64 - this.colorScheme = themeService.colorScheme 65 - this.theme = themeService.theme 64 + this.colorScheme = themeService.theme 65 + this.theme = themeService.lightDarkMode 66 66 this.additionalStyleModes = themeService.additionalStyleModes 67 67 68 - this.setColorScheme = themeService.setColorScheme.bind(themeService) 69 - this.setTheme = themeService.setTheme.bind(themeService) 68 + this.setColorScheme = themeService.setTheme.bind(themeService) 69 + this.setTheme = themeService.setLightDarkMode.bind(themeService) 70 70 this.toggleAdditionalStyleMode = themeService.toggleAdditionalStyleMode.bind(themeService) 71 71 72 - this.colorSchemeGroupList = colorSchemeGroupList 72 + this.colorSchemeGroupList = themeGroupList 73 73 74 74 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.updateIconTheme.bind(this)) 75 75 }
+2 -2
packages/frontend/src/app/components/setting-theme-switcher/setting-theme-switcher.component.html
··· 1 1 <mat-form-field class="block w-full mat-form-field-no-padding" floatLabel="always"> 2 2 <mat-label>{{ 'settings.theme' | translate }}</mat-label> 3 - <mat-select [value]="colorScheme()" (selectionChange)="setColorScheme($event)"> 3 + <mat-select [value]="colorScheme()" (selectionChange)="setTheme($event)"> 4 4 @for (group of colorSchemeGroupList | KeyValue; track $index) { 5 5 <mat-optgroup [label]="group.value.name" class="theme-group"> 6 6 @for (entry of group.value.entries; track $index) { ··· 13 13 <p class="setting-description">{{ 'settings.themeDescription' | translate }}</p> 14 14 <mat-form-field class="block w-full mat-form-field-no-padding" floatLabel="always"> 15 15 <mat-label>{{ 'settings.lightDarkMode' | translate }}</mat-label> 16 - <mat-select [value]="colorTheme()" (selectionChange)="setColorTheme($event)"> 16 + <mat-select [value]="lightDarkMode()" (selectionChange)="setLightDarkMode($event)"> 17 17 @for (entry of colorThemeData | KeyValue; track $index) { 18 18 <mat-option [value]="entry.key">{{ entry.value }}</mat-option> 19 19 }
+20 -20
packages/frontend/src/app/components/setting-theme-switcher/setting-theme-switcher.component.ts
··· 6 6 import { 7 7 AdditionalStyleMode, 8 8 additionalStyleModesData, 9 - ColorScheme, 10 - colorSchemeData, 11 - colorSchemeGroupList, 12 - ColorTheme, 13 - colorThemeData, 9 + Theme, 10 + themeData, 11 + themeGroupList, 12 + LightDarkMode, 13 + lightDarkModeData, 14 14 ThemeService 15 15 } from 'src/app/services/theme.service' 16 16 ··· 22 22 }) 23 23 export class SettingThemeSwitcherComponent { 24 24 // light/dark 25 - colorTheme: WritableSignal<ColorTheme> 26 - colorThemeData = colorThemeData 25 + lightDarkMode: WritableSignal<LightDarkMode> 26 + colorThemeData = lightDarkModeData 27 27 28 28 // colors 29 - colorScheme: WritableSignal<ColorScheme> 30 - colorSchemeData = colorSchemeData 31 - colorSchemeGroupList = colorSchemeGroupList 29 + colorScheme: WritableSignal<Theme> 30 + colorSchemeData = themeData 31 + colorSchemeGroupList = themeGroupList 32 32 33 33 // style modes 34 34 additionalStyleModes 35 35 additionalStyleModesSelect 36 36 additionalStyleModesData = additionalStyleModesData 37 - setAdditionalStyleMode 38 37 39 38 constructor(private themeService: ThemeService) { 40 - this.colorTheme = themeService.theme 39 + this.lightDarkMode = themeService.lightDarkMode 41 40 42 - this.colorScheme = themeService.colorScheme 41 + this.colorScheme = themeService.theme 43 42 44 43 this.additionalStyleModes = themeService.additionalStyleModes 45 44 this.additionalStyleModesSelect = Object.entries(this.additionalStyleModes) 46 45 .filter(([_, enabled]) => enabled()) 47 46 .map(([val, _]) => val) as AdditionalStyleMode[] 48 - this.setAdditionalStyleMode = themeService.setAdditionalStyleMode 49 47 } 50 48 51 - setColorTheme(event: MatSelectChange) { 52 - this.themeService.setTheme(event.value) 49 + setLightDarkMode(event: MatSelectChange) { 50 + this.themeService.setLightDarkMode(event.value) 53 51 } 54 - setColorScheme(event: MatSelectChange) { 55 - this.themeService.setColorScheme(event.value) 52 + setTheme(event: MatSelectChange) { 53 + console.log('setting theme to', event.value) 54 + this.themeService.setTheme(event.value) 56 55 } 57 56 setAdditionalStyleModes(event: MatSelectChange) { 58 57 this.additionalStyleModesSelect = event.value as AdditionalStyleMode[] ··· 60 59 const allModes = Object.keys(this.additionalStyleModesData) as AdditionalStyleMode[] 61 60 const enabledModes = this.additionalStyleModesSelect 62 61 const disabledModes = allModes.filter((mode) => !this.additionalStyleModesSelect.includes(mode)) 63 - enabledModes.forEach((mode) => this.setAdditionalStyleMode(mode, true)) 64 - disabledModes.forEach((mode) => this.setAdditionalStyleMode(mode, false)) 62 + enabledModes.forEach((mode) => this.additionalStyleModes[mode].set(true)) 63 + disabledModes.forEach((mode) => this.additionalStyleModes[mode].set(false)) 64 + this.themeService.syncAdditionalStyleMode() 65 65 } 66 66 }
+18 -18
packages/frontend/src/app/pages/profile/edit-profile/edit-profile.component.ts
··· 12 12 import { 13 13 AdditionalStyleMode, 14 14 additionalStyleModesData, 15 - ColorScheme, 16 - colorSchemeData, 17 - ColorSchemeGroupList, 18 - colorSchemeGroupList, 19 - ColorTheme, 20 - colorThemeData, 15 + Theme, 16 + themeData, 17 + ThemeGroupList, 18 + themeGroupList, 19 + LightDarkMode, 20 + lightDarkModeData, 21 21 ThemeService 22 22 } from 'src/app/services/theme.service' 23 23 import { faPlus, faXmark } from '@fortawesome/free-solid-svg-icons' ··· 109 109 survivedTimeout: ReturnType<typeof setTimeout> | undefined 110 110 lockout = false 111 111 112 - colorScheme: Signal<ColorScheme> 112 + colorScheme: Signal<Theme> 113 113 colorSchemeSelect = '' 114 - theme: Signal<ColorTheme> 114 + theme: Signal<LightDarkMode> 115 115 themeSelect = '' 116 116 additionalStyleModes: { [key in AdditionalStyleMode]: WritableSignal<boolean> } 117 117 additionalStyleModesSelect: AdditionalStyleMode[] ··· 120 120 appLanguage: string 121 121 122 122 // Data copies 123 - colorSchemeData = colorSchemeData 124 - colorThemeData = colorThemeData 123 + colorSchemeData = themeData 124 + colorThemeData = lightDarkModeData 125 125 additionalStyleModesData = additionalStyleModesData 126 126 127 127 // Function copies ··· 130 130 setAdditionalStyleMode: Function 131 131 132 132 // Theme categories 133 - colorSchemeGroupList: ColorSchemeGroupList 133 + colorSchemeGroupList: ThemeGroupList 134 134 135 135 constructor( 136 136 private jwtService: JwtService, ··· 141 141 private themeService: ThemeService, 142 142 private translationService: TranslateService 143 143 ) { 144 - this.colorScheme = themeService.colorScheme 144 + this.colorScheme = themeService.theme 145 145 this.colorSchemeSelect = this.colorScheme() 146 - this.theme = themeService.theme 146 + this.theme = themeService.lightDarkMode 147 147 this.themeSelect = this.theme() 148 148 this.additionalStyleModes = themeService.additionalStyleModes 149 149 this.additionalStyleModesSelect = Object.entries(this.additionalStyleModes) 150 150 .filter(([_, enabled]) => enabled()) 151 151 .map(([val, _]) => val) as AdditionalStyleMode[] 152 152 153 - this.setColorScheme = themeService.setColorScheme 154 - this.setTheme = themeService.setTheme 153 + this.setColorScheme = themeService.setTheme 154 + this.setTheme = themeService.setLightDarkMode 155 155 this.setAdditionalStyleMode = themeService.setAdditionalStyleMode 156 156 157 - this.colorSchemeGroupList = colorSchemeGroupList 157 + this.colorSchemeGroupList = themeGroupList 158 158 159 159 this.themeService.setCustomCSS('') 160 160 ··· 211 211 const alsoKnownAs = publicOptions.find((elem) => elem.optionName == 'fediverse.public.alsoKnownAs') 212 212 try { 213 213 this.editProfileForm.controls['alsoKnownAs'].patchValue(JSON.parse(alsoKnownAs?.optionValue || '')) 214 - } catch (_) { } 214 + } catch (_) {} 215 215 const askLevel = publicOptions.find((elem) => elem.optionName == 'wafrn.public.asks') 216 216 this.editProfileForm.controls['asksLevel'].patchValue(askLevel ? parseInt(askLevel.optionValue) : 2) 217 217 this.editProfileForm.controls['forceClassicAudioPlayer'].patchValue( ··· 269 269 if (fediAttachments) { 270 270 try { 271 271 this.fediAttachments = JSON.parse(fediAttachments.optionValue) 272 - } catch (error) { } 272 + } catch (error) {} 273 273 } 274 274 const localStorageNotificationsFrom = localStorage.getItem('notificationsFrom') 275 275 if (localStorageNotificationsFrom) {
+118 -19
packages/frontend/src/app/services/settings.service.ts
··· 21 21 import { UtilsService } from './utils.service' 22 22 import { HttpClient } from '@angular/common/http' 23 23 import { EnvironmentService } from './environment.service' 24 - import { catchError, debounceTime, fromEvent, lastValueFrom, of, timeout } from 'rxjs' 24 + import { catchError, debounceTime, filter, fromEvent, lastValueFrom, of, Subject, timeout } from 'rxjs' 25 25 import { PostsService } from './posts.service' 26 26 import { MessageService } from './message.service' 27 27 import { LoginService } from './login.service' ··· 35 35 import { SettingDropListComponent } from '../components/setting-drop-list/setting-drop-list.component' 36 36 import { SETTINGS_TOKEN } from '../pages/settings/settings.component' 37 37 import { replyBarItems } from '../components/post-action-buttons/post-action-buttons.component' 38 + import { toObservable } from '@angular/core/rxjs-interop' 38 39 39 40 // All setting keys for use throughout the app 40 41 const settingKeyVariants = [ ··· 46 47 'hideProfileNotLoggedIn', 47 48 'disableEmailNotifications', 48 49 // everything else - stored in the options table 50 + 'theme', // This and v 51 + 'lightDarkMode', // this option are weirdly named in localStorage because legacy reasons 52 + 'additionalStyleModes', 49 53 'rssOptions', 50 54 'alsoKnownAs', 51 55 'forceClassicLogo', ··· 78 82 type SettingKeyTuple = typeof settingKeyVariants 79 83 export type SettingKey = SettingKeyTuple[number] 80 84 81 - export type SettingFormTypes = 'checkbox' | 'select' | 'input' | 'textarea' | 'user' | 'list' 85 + // 'user' and 'raw' are modified by specific components 86 + export type SettingFormTypes = 'checkbox' | 'select' | 'input' | 'textarea' | 'user' | 'list' | 'raw' 82 87 83 88 export type SettingListItem = { 84 89 value: string ··· 87 92 88 93 // Setting type cannot be numbers because of a bug with mat-select 89 94 // Simply write your numbers as strings (agony) 90 - export type SettingValueType = string | boolean | SettingListItem[] 95 + export type SettingValueType = string | boolean | SettingListItem[] | string[] 91 96 export interface SettingDataEntry { 92 97 key: SettingKey // Copy of key for components to use 93 98 translationKey: string ··· 223 228 type: 'user', 224 229 default: '' 225 230 }, 231 + theme: { 232 + key: 'theme', 233 + translationKey: 'settings.theme', 234 + serverKey: 'wafrn.colorScheme', // Legacy 235 + localStorageKey: 'colorScheme', // Legacy 236 + type: 'raw', // Not actually used 237 + default: 'default', 238 + convertFromStorage: this.convertRawStringFrom 239 + }, 240 + lightDarkMode: { 241 + key: 'lightDarkMode', 242 + translationKey: 'settings.lightDarkMode', 243 + serverKey: 'wafrn.theme', // Legacy 244 + localStorageKey: 'theme', // Legacy 245 + type: 'raw', // Not actually used 246 + default: 'auto', 247 + convertFromStorage: this.convertRawStringFrom 248 + }, 249 + additionalStyleModes: { 250 + key: 'additionalStyleModes', 251 + translationKey: 'settings.additionalStyleModes', 252 + serverKey: 'wafrn.additionalStyleModes', 253 + localStorageKey: 'additionalStyleModes', 254 + type: 'list', 255 + default: [], 256 + convertFromStorage: this.convertListFrom, 257 + convertToStorage: this.convertListTo 258 + }, 226 259 forceClassicLogo: { 227 260 key: 'forceClassicLogo', 228 261 translationKey: 'settings.forceClassicLogo', ··· 442 475 serverKey: 'wafrn.postReplyBarOrder', 443 476 localStorageKey: 'postReplyBarOrder', 444 477 type: 'list', 445 - default: this.convertToListDefault([...replyBarItems]), 478 + default: this.convertToOrderListDefault([...replyBarItems]), 446 479 dropListData: { 447 480 quote: { icon: faQuoteLeft, translationKey: 'settings.postReplyBarOrderOptions.quote' }, 448 481 rewoot: { icon: faRepeat, translationKey: 'settings.postReplyBarOrderOptions.rewoot' }, ··· 462 495 serverKey: 'wafrn.postActionsButtonBarOrder', 463 496 localStorageKey: 'postActionsButtonBarOrder', 464 497 type: 'list', 465 - default: this.convertToListDefault([...replyBarItems]), 498 + default: this.convertToOrderListDefault([...replyBarItems]), 466 499 dropListData: { 467 500 // Duplicate from above 468 501 quote: { icon: faQuoteLeft, translationKey: 'settings.postReplyBarOrderOptions.quote' }, ··· 659 692 660 693 public settingsModified = signal(false) 661 694 public settingsLoading = signal(false) 695 + public settingsLoadedFromLogin = new Subject<void>() 662 696 663 697 constructor( 664 698 private dashboardService: DashboardService, ··· 698 732 } 699 733 }) 700 734 } 735 + 736 + // Update settings when logging in (and notify everyone) 737 + toObservable(loginService.loggedIn) 738 + .pipe(filter((logged) => logged)) 739 + .subscribe(() => { 740 + this.values = this.getLocalStorageValues() 741 + this.settingsLoadedFromLogin.next() 742 + }) 701 743 702 744 // Update settings on other tabs change 703 745 fromEvent(window, 'storage') ··· 753 795 754 796 async updateProfile(): Promise<boolean> { 755 797 // Map the non-required options into a specific key of the payload 756 - const options: { name: string; value: string }[] = Object.entries(this.data) 757 - .filter(([_key, entry]) => entry.profileOption !== true && entry.serverKey !== undefined) 758 - .map(([_key, entry]) => { 759 - const rawValue = this.values[entry.key] ?? '' 760 - let convertedValue: string = '' 761 - if (entry.convertToStorage) { 762 - convertedValue = entry.convertToStorage(rawValue) 763 - } else { 764 - convertedValue = this.values[entry.key]!.toString() 765 - } 766 - 767 - return { name: entry.serverKey!, value: convertedValue } 768 - }) 798 + const options: { name: string; value: string }[] = this.getSettingsAsOptions() 769 799 770 800 // Add Fediverse attachments 771 801 // Ignore attachment if it doesn't have both fields ··· 811 841 } 812 842 } 813 843 844 + private getSettingsAsOptions(): { name: string; value: string }[] { 845 + return Object.entries(this.data) 846 + .filter(([_key, entry]) => entry.profileOption !== true && entry.serverKey !== undefined) 847 + .map(([_key, entry]) => ({ name: entry.serverKey!, value: this.getSettingValueAsString(entry.key) })) 848 + } 849 + 850 + private getSettingValueAsString(key: SettingKey): string { 851 + const value = this.values[key] 852 + if (value === undefined) return '' 853 + if (this.data[key].convertToStorage) { 854 + return this.data[key].convertToStorage(value) 855 + } else { 856 + return value.toString() 857 + } 858 + } 859 + 814 860 async updateMultipleAccountData() { 815 861 // Update multiple account saved data 816 862 const currentBlog = this.loginService.currentAccount() ··· 824 870 } 825 871 } 826 872 873 + /** 874 + * @description Force updates a value, optionally writing it to localStorage and then to the server. 875 + * 876 + * Default is to update localStorage 877 + * 878 + * Used for when you need immediate setting propagation and saving like themes. 879 + * 880 + * @returns whether the server was able to save your value 881 + */ 882 + async forceUpdateValue(keyOrKeys: SettingKey | SettingKey[], updateLocalStorage: boolean = true): Promise<boolean> { 883 + let keyList: SettingKey[] 884 + if (Array.isArray(keyOrKeys)) { 885 + keyList = keyOrKeys 886 + } else { 887 + keyList = [keyOrKeys] 888 + } 889 + 890 + // Optionally write to localStorage 891 + if (updateLocalStorage) { 892 + keyList.forEach((key) => { 893 + const localStorageKey = this.data[key].localStorageKey 894 + const newValue = this.values[key] 895 + if (localStorageKey && newValue !== undefined) { 896 + localStorage.setItem(localStorageKey, this.getSettingValueAsString(key)) 897 + } 898 + }) 899 + } 900 + 901 + // Write options to the server 902 + if (this.loginService.loggedIn()) { 903 + const options: { name: string; value: string }[] = this.getSettingsAsOptions() 904 + const res = await lastValueFrom( 905 + this.http.post<{ success: boolean }>(`${EnvironmentService.environment.baseUrl}/editOptions`, { options }).pipe( 906 + timeout(60000), // if it doesn't return in a full minute you've got problems 907 + catchError((_err) => of({ success: false })) 908 + ) 909 + ) 910 + if (res.success) { 911 + return true 912 + } else { 913 + return false 914 + } 915 + } 916 + return false 917 + } 918 + 827 919 makeInject(data: any) { 828 920 return Injector.create({ providers: [{ provide: SETTINGS_TOKEN, useValue: data }] }) 829 921 } 830 922 831 923 // Drop list conversion for default orderings 832 - convertToListDefault(list: string[]): SettingListItem[] { 924 + // ONLY for type SettingListItem type 925 + convertToOrderListDefault(list: string[]): SettingListItem[] { 833 926 return list.map((item) => ({ 834 927 value: item, 835 928 enabled: true ··· 848 941 return `"${list}"` 849 942 } 850 943 944 + // Evil 945 + convertRawStringFrom(value: string): string { 946 + return value 947 + } 948 + 851 949 convertCommaListFrom(list: string): string { 852 950 try { 853 951 return JSON.parse(list).replaceAll(',', '\n') ··· 860 958 return `"${list.replaceAll('\n', ',')}"` 861 959 } 862 960 961 + // Generic for array-like types 863 962 convertListFrom(list: string): SettingValueType { 864 963 try { 865 964 return JSON.parse(list)
+88 -70
packages/frontend/src/app/services/theme.service.ts
··· 1 - import { effect, Injectable, signal, WritableSignal } from '@angular/core' 1 + import { Injectable, signal, WritableSignal } from '@angular/core' 2 2 import { LoginService } from './login.service' 3 3 import { HttpClient } from '@angular/common/http' 4 - import { debounceTime, filter, firstValueFrom, fromEvent, merge } from 'rxjs' 4 + import { debounceTime, filter, firstValueFrom, fromEvent, merge, tap } from 'rxjs' 5 5 import { EnvironmentService } from './environment.service' 6 6 import { toObservable } from '@angular/core/rxjs-interop' 7 + import { SettingListItem, SettingsService } from './settings.service' 7 8 8 9 // !! NOTE FOR ADDING THEMES !! // 9 10 // 10 11 // If you want to add a theme, you must: 11 - // - Add it to `colorSchemeVariants` 12 - // - Fill out its `colorSchemeData` (name data and if the theme forces light/dark) entry 12 + // - Add it to `themeVariants` 13 + // - Fill out its `themeData` (name data and if the theme forces light/dark) entry 13 14 // - Compatibility allows you to force dark/light if you need. 14 15 // - Auto Reset makes the theme be reset to default on reload 15 16 // - Add the theme as a CSS file in `/assets/themes/name.css` 16 17 // - Add a link file to it in theme-manager.component.html 17 - // - Add your theme to a group in `colorSchemeGroupList` 18 + // - Add your theme to a group in `themeGroupList` 18 19 19 20 // !! NOTE FOR ADDING MODES !! // 20 21 // 21 22 // If you want to add a style mode, you must: 22 23 // - Add it to `additionalStyleModeVariants` 23 24 // - Fill out its `additionalStyleModesData` 24 - // 25 - // Note: This uses the raw names of the mode setting. 26 - // DO NOT OVERRIDE OTHER LOCAL STORAGE ENTRIES! There is no check :3 25 + // - Add a link file to it in theme-manager.component.html 27 26 28 - const colorSchemeVariants = [ 27 + const themeVariants = [ 29 28 'default', 30 29 'tan', 31 30 'green', ··· 48 47 'catppuccin_macchiato', 49 48 'catppuccin_mocha' 50 49 ] as const 51 - type ColorSchemeTuple = typeof colorSchemeVariants 52 - export type ColorScheme = ColorSchemeTuple[number] 50 + type ThemeTuple = typeof themeVariants 51 + export type Theme = ThemeTuple[number] 53 52 54 - type ColorSchemeData = { 55 - [key in ColorScheme]: { 53 + type ThemeData = { 54 + [key in Theme]: { 56 55 name: string 57 56 compatibility: 'light' | 'dark' | 'both' 58 57 autoReset?: boolean 59 58 } 60 59 } 61 60 62 - export const colorSchemeData: ColorSchemeData = { 61 + export const themeData: ThemeData = { 63 62 default: { name: 'Default', compatibility: 'both' }, 64 63 tan: { name: 'Tan', compatibility: 'both' }, 65 64 green: { name: 'Green', compatibility: 'both' }, ··· 83 82 catppuccin_mocha: { name: 'Catppuccin Mocha', compatibility: 'both' } 84 83 } 85 84 86 - const colorSchemeGroupVariants = [ 87 - 'defaultThemes', 88 - 'computeryThemes', 89 - 'experimentalThemes', 90 - 'programmersThemes' 91 - ] as const 92 - type ColorSchemeGroupTuple = typeof colorSchemeGroupVariants 93 - export type ColorSchemeGroup = ColorSchemeGroupTuple[number] 94 - export type ColorSchemeGroupList = { 95 - [key in ColorSchemeGroup]: { 85 + const themeGroupVariants = ['defaultThemes', 'computeryThemes', 'experimentalThemes', 'programmersThemes'] as const 86 + type ThemeGroupTuple = typeof themeGroupVariants 87 + export type ThemeGroup = ThemeGroupTuple[number] 88 + export type ThemeGroupList = { 89 + [key in ThemeGroup]: { 96 90 name: string 97 - entries: ColorScheme[] 91 + entries: Theme[] 98 92 } 99 93 } 100 94 101 - export const colorSchemeGroupList: ColorSchemeGroupList = { 95 + export const themeGroupList: ThemeGroupList = { 102 96 defaultThemes: { 103 97 name: 'Default theme variants', 104 98 entries: ['default', 'tan', 'green', 'gold', 'red', 'pink', 'purple', 'blue'] ··· 117 111 } 118 112 } 119 113 120 - const colorThemeVariants = ['light', 'dark', 'auto'] as const 121 - type ColorThemeTuple = typeof colorThemeVariants 122 - export type ColorTheme = ColorThemeTuple[number] 114 + const lightDarkModeVariants = ['light', 'dark', 'auto'] as const 115 + type lightDarkModeTuple = typeof lightDarkModeVariants 116 + export type LightDarkMode = lightDarkModeTuple[number] 123 117 124 - export type ColorThemeData = { [key in ColorTheme]: string } 125 - export const colorThemeData: ColorThemeData = { 118 + export type LightDarkModeData = { [key in LightDarkMode]: string } 119 + export const lightDarkModeData: LightDarkModeData = { 126 120 light: 'Light', 127 121 dark: 'Dark', 128 122 auto: 'Auto' 129 123 } 130 124 131 125 // Verifying that a theme/scheme is real 132 - function isColorTheme(value: string): value is ColorTheme { 133 - return colorThemeVariants.includes(value as ColorTheme) 126 + function isLightDarkMode(value: string | undefined): value is LightDarkMode { 127 + return value !== undefined && lightDarkModeVariants.includes(value as LightDarkMode) 128 + } 129 + 130 + function isTheme(value: string | undefined): value is Theme { 131 + return value !== undefined && themeVariants.includes(value as Theme) 134 132 } 135 133 136 - function isColorScheme(value: string): value is ColorScheme { 137 - return colorSchemeVariants.includes(value as ColorScheme) 134 + // Covers SettingListItem[] because type jank 135 + function isAdditionalStyleMode(value: string[] | SettingListItem[]): value is AdditionalStyleMode[] { 136 + return !value.some((mode) => !additionalStyleModeVariants.includes(mode as AdditionalStyleMode)) 138 137 } 139 138 140 139 // More styles! ··· 168 167 providedIn: 'root' 169 168 }) 170 169 export class ThemeService { 171 - public colorScheme = signal<ColorScheme>('default') 172 - public theme = signal<ColorTheme>('auto') 170 + public theme = signal<Theme>('default') 171 + public lightDarkMode = signal<LightDarkMode>('auto') 173 172 public additionalStyleModes: { [key in AdditionalStyleMode]: WritableSignal<boolean> } = { 174 173 centerLayout: signal(false), 175 174 topToolbar: signal(false), ··· 181 180 182 181 constructor( 183 182 private loginService: LoginService, 184 - private http: HttpClient 183 + private http: HttpClient, 184 + private settingService: SettingsService 185 185 ) { 186 - // Setup when logging in or out and run once (yay signals) 186 + // Setup when logging out, completing setting sync, and also run once (yay signals) 187 187 // Also watches change from other tabs 188 188 merge( 189 - toObservable(loginService.loggedIn), 189 + toObservable(loginService.loggedIn).pipe(filter((logged) => !logged)), 190 + this.settingService.settingsLoadedFromLogin.asObservable(), 190 191 fromEvent(window, 'storage').pipe( 191 192 filter((event) => ['theme', 'colorScheme'].includes((<StorageEvent>event).key ?? '')), 192 - debounceTime(500) 193 + debounceTime(200) 193 194 ) 194 195 ).subscribe(() => this.setup()) 195 196 } 196 197 197 198 setup() { 198 - const savedScheme = localStorage?.getItem('colorScheme') ?? 'default' 199 - if (isColorScheme(savedScheme)) this.setColorScheme(savedScheme) 199 + const theme = this.settingService.values.theme 200 + if (typeof theme === 'string' && isTheme(theme)) { 201 + this.setTheme(theme) 202 + } 200 203 201 - const savedTheme = localStorage?.getItem('theme') ?? 'auto' 202 - if (isColorTheme(savedTheme)) this.setTheme(savedTheme) 204 + const darkLightMode = this.settingService.values.lightDarkMode 205 + if (typeof darkLightMode === 'string' && isLightDarkMode(darkLightMode)) { 206 + this.setLightDarkMode(darkLightMode) 207 + } 203 208 204 - Object.entries(this.additionalStyleModes).forEach(([mode, value]) => { 205 - const savedMode = localStorage?.getItem(mode) ?? 'false' 206 - value.set(savedMode === 'true') 207 - }) 209 + const settingAdditionalStyleModes = this.settingService.values.additionalStyleModes 210 + if (Array.isArray(settingAdditionalStyleModes) && isAdditionalStyleMode(settingAdditionalStyleModes)) { 211 + additionalStyleModeVariants.forEach((mode) => { 212 + const enabled = settingAdditionalStyleModes.includes(mode) 213 + this.additionalStyleModes[mode].set(enabled) 214 + }) 215 + } 208 216 209 217 // Fan theme fallback for old browsers 210 218 const chromeVersionForCompatibilityReasons = this.getChromeVersion() 211 219 if (chromeVersionForCompatibilityReasons && chromeVersionForCompatibilityReasons < 122) { 212 220 // we force the fan theme on old browsers 213 - this.setColorScheme('fan', true) 221 + this.setTheme('fan', true) 214 222 } 215 223 } 216 224 217 - public setColorScheme = async (scheme: ColorScheme, doNotSavePreference = false) => { 218 - this.colorScheme.set(scheme) 219 - localStorage?.setItem('colorScheme', scheme) 225 + public setTheme = async (theme: Theme, doNotSavePreference = false) => { 226 + this.theme.set(theme) 227 + this.settingService.values.theme = theme 220 228 221 - // Forced theme 222 - if (colorSchemeData[scheme]?.compatibility === 'light') await this.setTheme('light') 223 - if (colorSchemeData[scheme]?.compatibility === 'dark') await this.setTheme('dark') 229 + // Forced lightDarkMode 230 + if (themeData[theme]?.compatibility === 'light') await this.setLightDarkMode('light') 231 + if (themeData[theme]?.compatibility === 'dark') await this.setLightDarkMode('dark') 224 232 225 - // User settings 226 - if (doNotSavePreference) return 227 - return await this.loginService.updateUserOptions([{ name: 'wafrn.colorScheme', value: scheme }]) 233 + this.settingService.forceUpdateValue('theme', !doNotSavePreference) 228 234 } 229 235 230 - public setTheme = async (theme: ColorTheme, doNotSavePreference = false) => { 231 - this.theme.set(theme) 232 - document.documentElement.setAttribute('data-theme', theme) 233 - localStorage?.setItem('theme', theme) 236 + public setLightDarkMode = async (lightDarkMode: LightDarkMode, doNotSavePreference = false) => { 237 + this.lightDarkMode.set(lightDarkMode) 238 + this.settingService.values.lightDarkMode = lightDarkMode 234 239 235 - // User settings 236 - if (doNotSavePreference) return 237 - return await this.loginService.updateUserOptions([{ name: 'wafrn.theme', value: theme }]) 240 + document.documentElement.setAttribute('data-theme', lightDarkMode) 241 + this.settingService.forceUpdateValue('lightDarkMode', !doNotSavePreference) 238 242 } 239 243 244 + // When setting additionalStyleMode either call the set method here or modify and call sync afterwards if modifying many settings at once. 240 245 public setAdditionalStyleMode = async (mode: AdditionalStyleMode, value: boolean, doNotSavePreference = false) => { 241 246 this.additionalStyleModes[mode].set(value) 242 - localStorage?.setItem(mode, value.toString()) 247 + this.syncAdditionalStyleModeSettings() 248 + 249 + this.settingService.forceUpdateValue('additionalStyleModes', !doNotSavePreference) 250 + } 251 + 252 + public syncAdditionalStyleMode() { 253 + this.syncAdditionalStyleModeSettings() 254 + 255 + this.settingService.forceUpdateValue('additionalStyleModes') 256 + } 257 + 258 + // Helper to sync up the settings service values and theme service values 259 + private syncAdditionalStyleModeSettings() { 260 + const modes = Object.entries(this.additionalStyleModes) 261 + .filter(([_, enabled]) => enabled()) 262 + .map(([val, _]) => val) 243 263 244 - // User settings 245 - if (doNotSavePreference) return 246 - return await this.loginService.updateUserOptions([{ name: `wafrn.${mode}`, value: value.toString() }]) 264 + this.settingService.values.additionalStyleModes = modes 247 265 } 248 266 249 267 public async toggleAdditionalStyleMode(mode: AdditionalStyleMode, doNotSavePreference = false) {
+5 -5
packages/frontend/src/app/theme-manager/theme-manager.component.ts
··· 1 1 import { Component, Signal, WritableSignal } from '@angular/core' 2 - import { AdditionalStyleMode, ColorScheme, ColorTheme, ThemeService } from '../services/theme.service' 2 + import { AdditionalStyleMode, Theme, LightDarkMode, ThemeService } from '../services/theme.service' 3 3 4 4 @Component({ 5 5 selector: 'app-theme-manager', ··· 8 8 styleUrl: './theme-manager.component.scss' 9 9 }) 10 10 export class ThemeManagerComponent { 11 - colorScheme: Signal<ColorScheme> 12 - theme: Signal<ColorTheme> 11 + colorScheme: Signal<Theme> 12 + theme: Signal<LightDarkMode> 13 13 additionalStyleModes: { [key in AdditionalStyleMode]: WritableSignal<boolean> } 14 14 15 15 constructor(themeService: ThemeService) { 16 - this.colorScheme = themeService.colorScheme 17 - this.theme = themeService.theme 16 + this.colorScheme = themeService.theme 17 + this.theme = themeService.lightDarkMode 18 18 this.additionalStyleModes = themeService.additionalStyleModes 19 19 } 20 20 }