mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add dark mode to user preferences
Summary: Adds initial implementation of dark mode. Preferences for dark mode are available on the account settings page. Dark mode is currently a beta feature as there are still some small bugs to squash and a few remaining UI elements to style. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Subscribers: paulfitz, jarek Differential Revision: https://phab.getgrist.com/D3587
This commit is contained in:
@@ -5,6 +5,7 @@ import {urlState} from 'app/client/models/gristUrlState';
|
||||
import {Notifier} from 'app/client/models/NotifyModel';
|
||||
import {getFlavor, ProductFlavor} from 'app/client/ui/CustomThemes';
|
||||
import {buildNewSiteModal, buildUpgradeModal} from 'app/client/ui/ProductUpgrades';
|
||||
import {attachCssThemeVars, prefersDarkModeObs} from 'app/client/ui2018/cssVars';
|
||||
import {OrgUsageSummary} from 'app/common/DocUsage';
|
||||
import {Features, isLegacyPlan, Product} from 'app/common/Features';
|
||||
import {GristLoadConfig} from 'app/common/gristUrls';
|
||||
@@ -13,6 +14,9 @@ import {LocalPlugin} from 'app/common/plugin';
|
||||
import {UserPrefs} from 'app/common/Prefs';
|
||||
import {isOwner} from 'app/common/roles';
|
||||
import {getTagManagerScript} from 'app/common/tagManager';
|
||||
import {getDefaultThemePrefs, Theme, ThemeAppearance, ThemeColors, ThemePrefs,
|
||||
ThemePrefsChecker} from 'app/common/ThemePrefs';
|
||||
import {getThemeColors} from 'app/common/Themes';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {getOrgName, Organization, OrgError, SUPPORT_EMAIL, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
|
||||
import {getUserPrefObs, getUserPrefsObs} from 'app/client/models/UserPrefs';
|
||||
@@ -75,7 +79,10 @@ export interface AppModel {
|
||||
|
||||
currentProduct: Product|null; // The current org's product.
|
||||
currentFeatures: Features; // Features of the current org's product.
|
||||
|
||||
userPrefsObs: Observable<UserPrefs>;
|
||||
themePrefs: Observable<ThemePrefs>;
|
||||
currentTheme: Computed<Theme>;
|
||||
|
||||
pageType: Observable<PageType>;
|
||||
|
||||
@@ -209,6 +216,11 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
public readonly isLegacySite = Boolean(this.currentProduct && isLegacyPlan(this.currentProduct.name));
|
||||
|
||||
public readonly userPrefsObs = getUserPrefsObs(this);
|
||||
public readonly themePrefs = getUserPrefObs(this.userPrefsObs, 'theme', {
|
||||
defaultValue: getDefaultThemePrefs(),
|
||||
checker: ThemePrefsChecker,
|
||||
}) as Observable<ThemePrefs>;
|
||||
public readonly currentTheme = this._getCurrentThemeObs();
|
||||
|
||||
// Get the current PageType from the URL.
|
||||
public readonly pageType: Observable<PageType> = Computed.create(this, urlState().state,
|
||||
@@ -223,6 +235,10 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
public readonly orgError?: OrgError,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._applyTheme();
|
||||
this.autoDispose(this.currentTheme.addListener(() => this._applyTheme()));
|
||||
|
||||
this._recordSignUpIfIsNewUser();
|
||||
|
||||
const state = urlState().state.get();
|
||||
@@ -311,6 +327,39 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
dataLayer.push({event: 'new-sign-up'});
|
||||
getUserPrefObs(this.userPrefsObs, 'recordSignUpEvent').set(undefined);
|
||||
}
|
||||
|
||||
private _getCurrentThemeObs() {
|
||||
return Computed.create(this, this.themePrefs, prefersDarkModeObs(),
|
||||
(_use, themePrefs, prefersDarkMode) => {
|
||||
let appearance: ThemeAppearance;
|
||||
if (!themePrefs.syncWithOS) {
|
||||
appearance = themePrefs.appearance;
|
||||
} else {
|
||||
appearance = prefersDarkMode ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
const nameOrColors = themePrefs.colors[appearance];
|
||||
let colors: ThemeColors;
|
||||
if (typeof nameOrColors === 'string') {
|
||||
colors = getThemeColors(nameOrColors);
|
||||
} else {
|
||||
colors = nameOrColors;
|
||||
}
|
||||
|
||||
return {appearance, colors};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a theme based on the user's current theme preferences.
|
||||
*/
|
||||
private _applyTheme() {
|
||||
// Custom CSS is incompatible with custom themes.
|
||||
if (getGristConfig().enableCustomCss) { return; }
|
||||
|
||||
attachCssThemeVars(this.currentTheme.get());
|
||||
}
|
||||
}
|
||||
|
||||
export function getHomeUrl(): string {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {localStorageObs} from 'app/client/lib/localStorageObs';
|
||||
import {AppModel} from 'app/client/models/AppModel';
|
||||
import {UserOrgPrefs, UserPrefs} from 'app/common/Prefs';
|
||||
import {Computed, Observable} from 'grainjs';
|
||||
import {CheckerT} from 'ts-interface-checker';
|
||||
|
||||
interface PrefsTypes {
|
||||
userOrgPrefs: UserOrgPrefs;
|
||||
@@ -12,15 +13,14 @@ function makePrefFunctions<P extends keyof PrefsTypes>(prefsTypeName: P) {
|
||||
type PrefsType = PrefsTypes[P];
|
||||
|
||||
/**
|
||||
* Creates an observable that returns UserOrgPrefs, and which stores them when set.
|
||||
* Creates an observable that returns a PrefsType, and which stores changes when set.
|
||||
*
|
||||
* For anon user, the prefs live in localStorage. Note that the observable isn't actually watching
|
||||
* for changes on the server, it will only change when set.
|
||||
*/
|
||||
function getPrefsObs(appModel: AppModel): Observable<PrefsType> {
|
||||
const savedPrefs = appModel.currentValidUser ? appModel.currentOrg?.[prefsTypeName] : undefined;
|
||||
if (savedPrefs) {
|
||||
const prefsObs = Observable.create<PrefsType>(null, savedPrefs!);
|
||||
if (appModel.currentValidUser) {
|
||||
const prefsObs = Observable.create<PrefsType>(null, appModel.currentOrg?.[prefsTypeName] ?? {});
|
||||
return Computed.create(null, (use) => use(prefsObs))
|
||||
.onWrite(prefs => {
|
||||
prefsObs.set(prefs);
|
||||
@@ -41,10 +41,30 @@ function makePrefFunctions<P extends keyof PrefsTypes>(prefsTypeName: P) {
|
||||
* stores it when set.
|
||||
*/
|
||||
function getPrefObs<Name extends keyof PrefsType>(
|
||||
prefsObs: Observable<PrefsType>, prefName: Name
|
||||
): Observable<PrefsType[Name]> {
|
||||
return Computed.create(null, (use) => use(prefsObs)[prefName])
|
||||
.onWrite(value => prefsObs.set({...prefsObs.get(), [prefName]: value}));
|
||||
prefsObs: Observable<PrefsType>,
|
||||
prefName: Name,
|
||||
options: {
|
||||
defaultValue?: Exclude<PrefsType[Name], undefined>;
|
||||
checker?: CheckerT<PrefsType[Name]>;
|
||||
} = {}
|
||||
): Observable<PrefsType[Name] | undefined> {
|
||||
const {defaultValue, checker} = options;
|
||||
return Computed.create(null, (use) => {
|
||||
const prefs = use(prefsObs);
|
||||
if (!(prefName in prefs)) { return defaultValue; }
|
||||
|
||||
const value = prefs[prefName];
|
||||
if (checker) {
|
||||
try {
|
||||
checker.check(value);
|
||||
} catch (e) {
|
||||
console.error(`getPrefObs: preference ${prefName.toString()} has value of invalid type`, e);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}).onWrite(value => prefsObs.set({...prefsObs.get(), [prefName]: value}));
|
||||
}
|
||||
|
||||
return {getPrefsObs, getPrefObs};
|
||||
|
||||
Reference in New Issue
Block a user