diff --git a/source/options/components/exports.ts b/source/options/components/exports.ts index de6af4d..f065e5a 100644 --- a/source/options/components/exports.ts +++ b/source/options/components/exports.ts @@ -8,5 +8,6 @@ export {JumpToNewCommentSetting} from "./jump-to-new-comment.js"; export {MarkdownToolbarSetting} from "./markdown-toolbar.js"; export {MiscellaneousSetting} from "./miscellaneous.js"; export {ThemedLogoSetting} from "./themed-logo.js"; +export {ThemeSwitcherSetting} from "./theme-switcher.js"; export {UserLabelsSetting} from "./user-labels.js"; export {UsernameColorsSetting} from "./username-colors.js"; diff --git a/source/options/components/theme-switcher.tsx b/source/options/components/theme-switcher.tsx new file mode 100644 index 0000000..68e02d1 --- /dev/null +++ b/source/options/components/theme-switcher.tsx @@ -0,0 +1,129 @@ +import {Component} from "preact"; +import {type Value} from "@holllo/webextension-storage"; +import { + Data, + Feature, + fromStorage, + type ThemeSwitcherData, +} from "../../storage/exports.js"; +import {Setting, type SettingProps} from "./index.js"; + +type State = { + data: Value | undefined; + hasUnsavedChanges: boolean; + themesList: Array<[string, string]>; +}; + +export class ThemeSwitcherSetting extends Component { + constructor(props: SettingProps) { + super(props); + + this.state = { + data: undefined, + hasUnsavedChanges: false, + themesList: [], + }; + } + + async componentDidMount() { + const themesList = await fromStorage(Data.ThemesList); + this.setState({ + data: await fromStorage(Feature.ThemeSwitcher), + themesList: themesList.value, + }); + } + + onChange = (event: Event, key: keyof ThemeSwitcherData) => { + const {data} = this.state; + const target = event.target as HTMLInputElement | HTMLSelectElement; + + if (data === undefined) { + return; + } + + data.value[key] = target.value; + this.setState({data, hasUnsavedChanges: true}); + }; + + save = async () => { + const {data} = this.state; + if (data === undefined) { + return; + } + + await data.save(); + this.setState({hasUnsavedChanges: false}); + }; + + render() { + const {data, hasUnsavedChanges, themesList} = this.state; + if (data === undefined) { + return; + } + + const themeOptions = themesList.map(([value, text]) => ( + + )); + + const unsavedChanges = hasUnsavedChanges ? "unsaved-changes" : ""; + + return ( + +

+ Automatically switch between two themes at certain times of the day. +

+ + + +

+ Switch to{" "} + {" "} + at{" "} + { + this.onChange(event, "hourA"); + }} + type="text" + value={data.value.hourA} + /> +

+ +

+ Switch to{" "} + {" "} + at{" "} + { + this.onChange(event, "hourB"); + }} + type="text" + value={data.value.hourB} + /> +

+
+ ); + } +} diff --git a/source/options/features.ts b/source/options/features.ts index 404ba66..d35bbd0 100644 --- a/source/options/features.ts +++ b/source/options/features.ts @@ -9,6 +9,7 @@ import { JumpToNewCommentSetting, MarkdownToolbarSetting, MiscellaneousSetting, + ThemeSwitcherSetting, ThemedLogoSetting, UserLabelsSetting, UsernameColorsSetting, @@ -83,6 +84,13 @@ export const features: FeatureData[] = [ title: "Miscellaneous", component: MiscellaneousSetting, }, + { + availableSince: new Date("2023-12-24"), + index: 0, + key: Feature.ThemeSwitcher, + title: "Theme Switcher", + component: ThemeSwitcherSetting, + }, { availableSince: new Date("2022-02-27"), index: 0, diff --git a/source/scss/_button.scss b/source/scss/_button.scss index a5b94fb..cec0b90 100644 --- a/source/scss/_button.scss +++ b/source/scss/_button.scss @@ -1,5 +1,5 @@ @mixin button { - --button-color: var(--blue); + --button-color: var(--save-status-color, var(--blue)); --button-color-alt: var(--dark-blue); background-color: var(--button-color); diff --git a/source/scss/_settings.scss b/source/scss/_settings.scss index 6a63ede..802798d 100644 --- a/source/scss/_settings.scss +++ b/source/scss/_settings.scss @@ -157,10 +157,15 @@ } } + .styled-text-input, .styled-select { background-color: var(--background-primary); border: 1px solid var(--save-status-color, var(--blue)); color: var(--foreground); padding: 8px; } + + .margin-bottom-8 { + margin-bottom: 8px; + } } diff --git a/source/storage/enums.ts b/source/storage/enums.ts index b3822bd..975f628 100644 --- a/source/storage/enums.ts +++ b/source/storage/enums.ts @@ -12,6 +12,7 @@ export enum Feature { MarkdownToolbar = "markdown-toolbar", Miscellaneous = "miscellaneous-features", ThemedLogo = "themed-logo", + ThemeSwitcher = "theme-switcher", UserLabels = "user-labels", UsernameColors = "username-colors", } diff --git a/source/storage/exports.ts b/source/storage/exports.ts index 3cdd655..2a0c669 100644 --- a/source/storage/exports.ts +++ b/source/storage/exports.ts @@ -27,6 +27,23 @@ export type HideVotesData = { ownTopics: boolean; }; +/** + * The data stored for the Theme Switcher feature. + */ +export type ThemeSwitcherData = { + /** The hour to switch to theme A. */ + hourA: string; + + /** The hour to switch to theme B. */ + hourB: string; + + /** The value of the theme from the theme selector for the first theme. */ + themeA: string; + + /** The value of the theme from the theme selector for the second theme. */ + themeB: string; +}; + /** * All storage {@link Value}s stored in WebExtension storage. */ @@ -134,6 +151,18 @@ export const storageValues = { }, storage: browser.storage.sync, }), + [Feature.ThemeSwitcher]: createValue({ + deserialize: (input) => JSON.parse(input) as ThemeSwitcherData, + serialize: (input) => JSON.stringify(input), + key: Feature.ThemeSwitcher, + value: { + hourA: "09:00", + hourB: "21:00", + themeA: "white", + themeB: "black", + }, + storage: browser.storage.sync, + }), [Feature.UserLabels]: collectUserLabels(), [Feature.UsernameColors]: collectUsernameColors(), };