desktop: add custom titlebar

This commit is contained in:
Abdullah Atta
2024-03-31 00:06:34 +05:00
committed by Abdullah Atta
parent be41cf1252
commit 23bd4a564f
25 changed files with 458 additions and 165 deletions

View File

@@ -24,6 +24,7 @@ import { spellCheckerRouter } from "./spell-checker";
import { updaterRouter } from "./updater";
import { bridgeRouter } from "./bridge";
import { safeStorageRouter } from "./safe-storage";
import { windowRouter } from "./window";
const t = initTRPC.create();
@@ -33,7 +34,8 @@ export const router = t.router({
spellChecker: spellCheckerRouter,
updater: updaterRouter,
bridge: bridgeRouter,
safeStorage: safeStorageRouter
safeStorage: safeStorageRouter,
window: windowRouter
});
export const api = router.createCaller({});

View File

@@ -170,16 +170,41 @@ export const osIntegrationRouter = t.router({
}),
bringToFront: t.procedure.query(() => bringToFront()),
changeTheme: t.procedure
.input(Theme)
.mutation(({ input }) => setTheme(input)),
.input(
z.object({
theme: Theme,
windowControlsIconColor: z.string().optional(),
backgroundColor: z.string().optional()
})
)
.mutation(
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
if (windowControlsIconColor) {
config.windowControlsIconColor = windowControlsIconColor;
globalThis.window?.setTitleBarOverlay({
symbolColor: windowControlsIconColor
});
}
if (backgroundColor) {
config.backgroundColor = backgroundColor;
}
setTheme(theme);
}
),
onThemeChanged: t.procedure.subscription(() =>
observable<"dark" | "light">((emit) => {
nativeTheme.on("updated", () => {
const updated = () => {
if (getTheme() === "system") {
emit.next(nativeTheme.shouldUseDarkColors ? "dark" : "light");
}
});
};
nativeTheme.on("updated", updated);
return () => {
nativeTheme.off("updated", updated);
};
})
)
});

View File

@@ -0,0 +1,83 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { initTRPC } from "@trpc/server";
import { observable } from "@trpc/server/observable";
const t = initTRPC.create();
export const windowRouter = t.router({
maximize: t.procedure.mutation(() => {
globalThis.window?.maximize();
}),
restore: t.procedure.mutation(() => {
globalThis.window?.restore();
}),
minimze: t.procedure.mutation(() => {
globalThis.window?.minimize();
}),
maximized: t.procedure.query(() => globalThis.window?.isMaximized()),
fullscreen: t.procedure.query(() => globalThis.window?.isFullScreen()),
onWindowStateChanged: t.procedure.subscription(() => {
return observable<{ maximized: boolean; fullscreen: boolean }>((emit) => {
function listener() {
emit.next({
maximized: !!globalThis.window?.isMaximized(),
fullscreen: !!globalThis.window?.isFullScreen()
});
}
function enterFullscreen() {
emit.next({
maximized: !!globalThis.window?.isMaximized(),
fullscreen: true
});
}
function leaveFullscreen() {
emit.next({
maximized: !!globalThis.window?.isMaximized(),
fullscreen: false
});
}
globalThis.window?.addListener("maximize", listener);
globalThis.window?.addListener("minimize", listener);
globalThis.window?.addListener("unmaximize", listener);
globalThis.window?.addListener("restore", listener);
globalThis.window?.addListener("enter-full-screen", enterFullscreen);
globalThis.window?.addListener("leave-full-screen", leaveFullscreen);
globalThis.window?.addListener("leave-html-full-screen", leaveFullscreen);
globalThis.window?.addListener("enter-html-full-screen", enterFullscreen);
return () => {
globalThis.window?.removeListener("maximize", listener);
globalThis.window?.removeListener("minimize", listener);
globalThis.window?.removeListener("unmaximize", listener);
globalThis.window?.removeListener("restore", listener);
globalThis.window?.removeListener("enter-full-screen", enterFullscreen);
globalThis.window?.removeListener("leave-full-screen", leaveFullscreen);
globalThis.window?.removeListener(
"leave-html-full-screen",
leaveFullscreen
);
globalThis.window?.removeListener(
"enter-html-full-screen",
enterFullscreen
);
};
});
})
});

View File

@@ -82,6 +82,14 @@ async function createWindow() {
size: 512,
format: process.platform === "win32" ? "ico" : "png"
}),
titleBarStyle: "hidden",
titleBarOverlay: {
height: 37,
color: "#00000000",
symbolColor: config.windowControlsIconColor
},
webPreferences: {
zoomFactor: config.zoomFactor,
nodeIntegration: true,
@@ -122,7 +130,7 @@ async function createWindow() {
setupJumplist();
if (isDevelopment())
mainWindow.webContents.openDevTools({ mode: "right", activate: true });
mainWindow.webContents.openDevTools({ mode: "bottom", activate: true });
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);

View File

@@ -42,7 +42,11 @@ export const config = {
zoomFactor: 1,
theme: nativeTheme.themeSource,
automaticUpdates: true,
proxyRules: ""
proxyRules: "",
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
windowControlsIconColor:
nativeTheme.themeSource === "dark" ? "#ffffff" : "#000000"
};
type ConfigKey = keyof typeof config;

View File

@@ -41,7 +41,10 @@ function setTheme(theme: Theme) {
}
function getBackgroundColor() {
return nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff";
return (
config.backgroundColor ||
(nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff")
);
}
function getSystemTheme() {

View File

@@ -25,12 +25,11 @@ import { App } from "./app";
renderApp();
async function renderApp() {
const { component, props, path } = await init();
const { component, props } = await init();
const { default: Component } = await component();
render(
<BaseThemeProvider
addGlobalStyles
sx={{
display: "flex",
"#app": { flex: 1, height: "unset" },

View File

@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect } from "react";
import { useStore } from "./stores/app-store";
import { useStore as useUserStore } from "./stores/user-store";
import { useStore as useThemeStore } from "./stores/theme-store";
import { useStore as useAttachmentStore } from "./stores/attachment-store";
import { useEditorStore } from "./stores/editor-store";
import { useStore as useAnnouncementStore } from "./stores/announcement-store";
@@ -37,7 +36,6 @@ import {
showFeatureDialog,
showOnboardingDialog
} from "./common/dialog-controller";
import useSystemTheme from "./hooks/use-system-theme";
import { updateStatus, removeStatus, getStatus } from "./hooks/use-status";
import { showToast } from "./utils/toast";
import { interruptedOnboarding } from "./dialogs/onboarding-dialog";
@@ -55,13 +53,10 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
const initStore = useStore((store) => store.init);
const initAttachments = useAttachmentStore((store) => store.init);
const setIsVaultCreated = useStore((store) => store.setIsVaultCreated);
const setColorScheme = useThemeStore((store) => store.setColorScheme);
const followSystemTheme = useThemeStore((store) => store.followSystemTheme);
const initEditorStore = useEditorStore((store) => store.init);
const dialogAnnouncements = useAnnouncementStore(
(store) => store.dialogAnnouncements
);
const isSystemThemeDark = useSystemTheme();
useEffect(
function initializeApp() {
@@ -233,11 +228,6 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
})();
}, [dialogAnnouncements]);
useEffect(() => {
if (!followSystemTheme) return;
setColorScheme(isSystemThemeDark ? "dark" : "light");
}, [isSystemThemeDark, followSystemTheme, setColorScheme]);
useEffect(() => {
const { unsubscribe } =
desktop?.bridge.onCreateItem.subscribe(undefined, {

View File

@@ -3,6 +3,15 @@
--sash-hover-size: 4px;
}
.tabsScroll,
.titlebarLogo {
-webkit-app-region: drag;
}
.tabsScroll .tab {
-webkit-app-region: no-drag;
}
.sash-module_sash__K-9lB.sash-module_hover__80W6I:before,
.sash-module_sash__K-9lB.sash-module_active__bJspD:before {
background: var(--accent);

View File

@@ -34,6 +34,7 @@ import { FlexScrollContainer } from "./components/scroll-container";
import CachedRouter from "./components/cached-router";
import { WebExtensionRelay } from "./utils/web-extension-relay";
import { usePersistentState } from "./hooks/use-persistent-state";
import { EditorActionBar } from "./components/editor/action-bar";
new WebExtensionRelay();
@@ -135,6 +136,7 @@ function DesktopAppContents({
return (
<>
{IS_DESKTOP_APP ? <EditorActionBar /> : null}
<Flex
variant="rowFill"
sx={{

View File

@@ -35,7 +35,11 @@ import {
Search,
TableOfContents,
Trash,
Unlock
Unlock,
WindowClose,
WindowMaximize,
WindowMinimize,
WindowRestore
} from "../icons";
import { ScrollContainer } from "@notesnook/ui";
import {
@@ -64,11 +68,16 @@ import {
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { AppEventManager, AppEvents } from "../../common/app-events";
import { desktop } from "../../common/desktop-bridge";
import { useWindowControls } from "../../hooks/use-window-controls";
import { ScopedThemeProvider } from "../theme-provider";
import { getPlatform } from "../../utils/platform";
export function EditorActionBar() {
const editorMargins = useEditorStore((store) => store.editorMargins);
const isFocusMode = useAppStore((store) => store.isFocusMode);
const [isFullscreen, setIsFullscreen] = useState(false);
const { isMaximized, isFullscreen, hasNativeWindowControls } =
useWindowControls();
const activeSession = useEditorStore((store) =>
store.activeSessionId ? store.getSession(store.activeSessionId) : undefined
);
@@ -95,7 +104,6 @@ export function EditorActionBar() {
} else {
enterFullscreen(document.documentElement);
}
setIsFullscreen((s) => !s);
}
},
{
@@ -145,44 +153,96 @@ export function EditorActionBar() {
activeSession.type !== "conflicted" &&
!isFocusMode,
onClick: () => useEditorStore.getState().toggleProperties()
},
{
title: "Minimize",
icon: WindowMinimize,
hidden: hasNativeWindowControls || isFullscreen,
enabled: true,
onClick: () => desktop?.window.minimze.mutate()
},
{
title: isMaximized ? "Restore" : "Maximize",
icon: isMaximized ? WindowRestore : WindowMaximize,
enabled: true,
hidden: hasNativeWindowControls || isFullscreen,
onClick: () =>
isMaximized
? desktop?.window.restore.mutate()
: desktop?.window.maximize.mutate()
},
{
title: "Close",
icon: WindowClose,
hidden: hasNativeWindowControls || isFullscreen,
enabled: true,
onClick: () => window.close()
}
];
return (
<Flex sx={{ mb: 1, gap: 2 }}>
<TabStrip />
<Flex
bg="background"
sx={{
borderRadius: "default",
overflow: "hidden",
alignItems: "center",
justifyContent: "flex-end",
mr: 2
}}
>
{tools.map((tool) => (
<Button
data-test-id={tool.title}
disabled={!tool.enabled}
variant="secondary"
title={tool.title}
key={tool.title}
sx={{
display: [
tool.hideOnMobile ? "none" : "flex",
tool.hidden ? "none" : "flex"
],
borderRadius: 0,
flexShrink: 0
}}
onClick={tool.onClick}
>
<tool.icon size={18} />
</Button>
))}
<ScopedThemeProvider scope="titleBar" injectCssVars>
<Flex sx={{ gap: 2, borderBottom: "1px solid var(--border)", pl: 2 }}>
{IS_DESKTOP_APP ? (
getPlatform() === "darwin" && !isFullscreen ? (
<></>
) : (
<svg
className="titlebarLogo"
style={{
alignSelf: "center",
height: 24,
width: 24
}}
>
<use href="#themed-logo" />
</svg>
)
) : null}
<TabStrip />
<Flex
bg="transparent"
sx={{
// borderRadius: "default",
// overflow: "hidden",
alignItems: "center",
justifyContent: "flex-end",
mr: IS_DESKTOP_APP ? `calc(100vw - env(titlebar-area-width))` : 2
}}
>
{tools.map((tool) => (
<Button
data-test-id={tool.title}
disabled={!tool.enabled}
variant={tool.title === "Close" ? "error" : "secondary"}
title={tool.title}
key={tool.title}
sx={{
height: "100%",
alignItems: "center",
bg: "transparent",
display: [
tool.hideOnMobile ? "none" : "flex",
tool.hidden ? "none" : "flex"
],
borderRadius: 0,
flexShrink: 0,
"&:hover svg path": {
fill:
tool.title === "Close"
? "var(--accentForeground-error) !important"
: "var(--icon)"
}
}}
onClick={tool.onClick}
>
<tool.icon size={18} />
</Button>
))}
</Flex>
</Flex>
</Flex>
</ScopedThemeProvider>
);
}
@@ -210,8 +270,7 @@ function TabStrip() {
<Flex
sx={{
flex: 1,
my: 1,
ml: 1,
my: "2.5px",
gap: 1,
height: 32
}}
@@ -392,12 +451,12 @@ function Tab(props: TabProps) {
transition,
visibility: active?.id === id ? "hidden" : "visible",
bg: isActive ? "background" : "background-secondary",
bg: isActive ? "background-selected" : "background-secondary",
// borderTopLeftRadius: "default",
// borderTopRightRadius: "default",
// borderBottom: isActive ? "none" : "1px solid var(--border)",
border: "1px solid",
borderColor: isActive ? "border" : "transparent",
borderColor: isActive ? "border-selected" : "transparent",
justifyContent: "space-between",
alignItems: "center",
flexShrink: 0,
@@ -405,7 +464,7 @@ function Tab(props: TabProps) {
"& .closeTabButton": {
visibility: "visible"
},
bg: isActive ? "background" : "hover"
bg: isActive ? "hover-selected" : "hover"
}
}}
onContextMenu={(e) => {
@@ -476,7 +535,7 @@ function Tab(props: TabProps) {
{...attributes}
>
<Flex mr={1}>
<Icon size={16} color={isActive ? "accent" : "icon"} />
<Icon size={16} color={isActive ? "accent-selected" : "icon"} />
<Text
variant="body"
sx={{
@@ -485,7 +544,8 @@ function Tab(props: TabProps) {
overflowX: "hidden",
pointerEvents: "none",
fontStyle: isTemporary ? "italic" : "normal",
maxWidth: 120
maxWidth: 120,
color: isActive ? "paragraph-selected" : "paragraph"
}}
ml={1}
>

View File

@@ -61,7 +61,6 @@ import { showToast } from "../../utils/toast";
import { Item, MaybeDeletedItem, isDeleted } from "@notesnook/core/dist/types";
import { debounce, debounceWithId } from "@notesnook/common";
import { Freeze } from "react-freeze";
import { EditorActionBar } from "./action-bar";
import { UnlockView } from "../unlock";
import DiffViewer from "../diff-viewer";
import TableOfContents from "./table-of-contents";
@@ -91,11 +90,12 @@ export default function TabsView() {
return (
<>
<EditorActionBar />
<ScopedThemeProvider
scope="editor"
ref={dropRef}
sx={{
bg: "background",
pt: 1,
flex: 1,
overflow: "hidden",
display: "flex",

View File

@@ -45,7 +45,6 @@ export function ErrorComponent({ error, resetErrorBoundary }: FallbackProps) {
return (
<BaseThemeProvider
onRender={() => document.getElementById("splash")?.remove()}
addGlobalStyles
sx={{
height: "100%",
bg: "background",

View File

@@ -211,7 +211,8 @@ import {
mdiDotsHorizontal,
mdiCalendarBlank,
mdiFormatListBulleted,
mdiLink
mdiLink,
mdiWindowClose
} from "@mdi/js";
import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme";
@@ -538,3 +539,10 @@ export const Legal = createIcon(mdiGavel);
export const Desktop = createIcon(mdiDesktopClassic);
export const Notification = createIcon(mdiBellBadgeOutline);
export const Calendar = createIcon(mdiCalendarBlank);
export const WindowMinimize = createIcon("M4 20v-2h16v2H4Z");
export const WindowMaximize = createIcon("M4 20V4h16v16Zm2-2h12V6H6ZM6 6v12Z");
export const WindowRestore = createIcon(
"M8 16V4h12v12Zm2-2h8V6h-8Zm-6 6V8.525h2V18h9.475v2Zm6-6V6v8Z"
);
export const WindowClose = createIcon(mdiWindowClose);

View File

@@ -232,6 +232,10 @@ function NavigationMenu(props: NavigationMenuProps) {
flexDirection: "column",
display: "flex"
}}
trackStyle={() => ({
width: 3
})}
thumbStyle={() => ({ width: 3 })}
suppressScrollX={true}
>
<Flex sx={{ flexDirection: "column" }}>

View File

@@ -19,38 +19,39 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
EmotionThemeProvider,
themeToCSS,
ThemeScopes,
themeToCSS,
useThemeEngineStore
} from "@notesnook/theme";
import { PropsWithChildren, useEffect, useMemo } from "react";
import { PropsWithChildren, useEffect } from "react";
import { BoxProps } from "@theme-ui/components";
import { Global, css } from "@emotion/react";
import { useStore as useThemeStore } from "../../stores/theme-store";
import { useSystemTheme } from "../../hooks/use-system-theme";
export function BaseThemeProvider(
props: PropsWithChildren<
{
injectCssVars?: boolean;
addGlobalStyles?: boolean;
scope?: keyof ThemeScopes;
onRender?: () => void;
} & Omit<BoxProps, "variant">
>
) {
const {
children,
addGlobalStyles = false,
scope = "base",
onRender,
...restProps
} = props;
const { children, scope = "base", onRender, ...restProps } = props;
const colorScheme = useThemeStore((store) => store.colorScheme);
const theme = useThemeStore((store) =>
colorScheme === "dark" ? store.darkTheme : store.lightTheme
);
const cssTheme = useMemo(() => themeToCSS(theme), [theme]);
// const cssTheme = useMemo(() => themeToCSS(theme), [theme]);
const isSystemThemeDark = useSystemTheme();
const setColorScheme = useThemeStore((store) => store.setColorScheme);
const followSystemTheme = useThemeStore((store) => store.followSystemTheme);
useEffect(() => {
if (!followSystemTheme) return;
setColorScheme(isSystemThemeDark ? "dark" : "light");
}, [isSystemThemeDark, followSystemTheme, setColorScheme]);
useEffect(() => {
if (IS_THEME_BUILDER) return;
@@ -71,6 +72,10 @@ export function BaseThemeProvider(
theme.scopes.base.primary.background
);
}
const css = themeToCSS(theme);
const stylesheet = document.getElementById("theme-colors");
if (stylesheet) stylesheet.innerHTML = css;
}, [theme]);
useEffect(() => {
@@ -79,14 +84,6 @@ export function BaseThemeProvider(
return (
<>
{addGlobalStyles && (
<Global
styles={css`
${cssTheme}
`}
/>
)}
<EmotionThemeProvider {...restProps} scope={scope}>
{children}
</EmotionThemeProvider>

View File

@@ -62,4 +62,10 @@ declare global {
mode: "read-only" | "readwrite" | "readwrite-unsafe";
}): Promise<FileSystemSyncAccessHandle>;
}
interface Navigator {
windowControlsOverlay?: {
getTitlebarAreaRect(): DOMRect;
visible: boolean;
};
}
}

View File

@@ -21,7 +21,7 @@ import { useEffect, useState } from "react";
import useMediaQuery from "./use-media-query";
import { desktop } from "../common/desktop-bridge";
function useSystemTheme() {
export function useSystemTheme() {
const isDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
const [systemTheme, setSystemTheme] = useState(isDarkMode ? "dark" : "light");
@@ -41,4 +41,3 @@ function useSystemTheme() {
return systemTheme === "dark";
}
export default useSystemTheme;

View File

@@ -0,0 +1,66 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from "react";
import { desktop } from "../common/desktop-bridge";
import { getPlatform } from "../utils/platform";
export function useWindowControls() {
const [isMaximized, setIsMaximized] = useState<boolean>();
const [isFullscreen, setIsFullscreen] = useState<boolean>();
useEffect(() => {
const event = desktop?.window.onWindowStateChanged.subscribe(undefined, {
onData(value) {
setIsMaximized(value.maximized);
setIsFullscreen(value.fullscreen);
}
});
desktop?.window.maximized.query().then((value) => setIsMaximized(value));
desktop?.window.fullscreen.query().then((value) => setIsFullscreen(value));
function onFullscreenChange() {
setIsFullscreen(!!document.fullscreenElement);
console.log(document.fullscreenElement);
}
document.addEventListener("fullscreenchange", onFullscreenChange);
return () => {
event?.unsubscribe();
document.removeEventListener("fullscreenchange", onFullscreenChange);
};
}, []);
return {
isMaximized,
isFullscreen,
hasNativeWindowControls:
IS_DESKTOP_APP || getPlatform() === "darwin" || getPlatform() === "win32"
};
}
function windowControlsMargin() {
if (
!window.navigator.windowControlsOverlay ||
!window.navigator.windowControlsOverlay.visible
)
return 0;
const overlayRect =
window.navigator.windowControlsOverlay.getTitlebarAreaRect();
return window.innerWidth - overlayRect.width + 5;
}

View File

@@ -40,12 +40,26 @@
<meta name="twitter:card" content="summary_large_image" />
<title>Notesnook</title>
<script nonce="7WIq8hRwApoXhctoGZZthMLYQLRNiprTwcPi6Azdf">
<style id="theme-colors">
#splash {
display: none !important;
}
</style>
<script type="module">
import { themeToCSS } from "@notesnook/theme";
const colorScheme = JSON.parse(
window.localStorage.getItem("colorScheme") || '"light"'
);
const root = document.querySelector("html");
if (root) root.setAttribute("data-theme", colorScheme);
const theme = window.localStorage.getItem(`theme:${colorScheme}`);
if (theme) {
const css = themeToCSS(JSON.parse(theme));
const stylesheet = document.getElementById("theme-colors");
if (stylesheet) stylesheet.innerHTML = css;
}
</script>
<script type="module" src="/index.tsx"></script>
<style>
@@ -53,21 +67,6 @@
overscroll-behavior: none;
}
html[data-theme="dark"] {
--bg: #0f0f0f;
--fg: #fff;
--three-bars-bg: #494949;
--gradient-stop-color: #111111;
--n-letter-color: #e1e1e1;
}
html[data-theme="light"] {
--bg: #fff;
--three-bars-bg: #bebebe;
--gradient-stop-color: #fff9f9;
--n-letter-color: #000;
}
html[data-theme="light"] .react-loading-skeleton {
--base-color: var(--background-secondary);
--highlight-color: #var(--background-secondary);
@@ -78,6 +77,19 @@
--highlight-color: var(--background-secondary);
}
#splash {
background-color: var(--background);
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#splash svg {
transform: scale(1);
animation: pulse 2s infinite;
@@ -161,28 +173,13 @@
</style>
</head>
<body>
<body class="theme-scope-base">
<noscript>You need to enable JavaScript to run this app.</noscript>
<!-- <script src="https://cdn.jsdelivr.net/npm/highlightjs@9.16.2/highlight.pack.min.js"></script>
-->
<div id="root"></div>
<div
id="splash"
style="
background-color: var(--bg);
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
"
>
<svg viewBox="0 0 339 339" style="width: 150px">
<defs />
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="themed-logo" viewBox="0 0 339 339">
<defs>
<linearGradient
xlink:href="#a"
@@ -199,7 +196,7 @@
<stop offset="0" />
<stop
offset="1"
stop-color="var(--gradient-stop-color)"
stop-color="var(--background-secondary)"
stop-opacity="0"
/>
</linearGradient>
@@ -223,7 +220,7 @@
d="M160 205V-35m0 240L18 249m142-44l154 41"
/>
<path
fill="var(--n-letter-color)"
fill="var(--paragraph)"
d="M84 109l35 54V98l21-7v91l-27 9-35-54v65l-21 6v-91z"
/>
<rect
@@ -231,15 +228,20 @@
height="12.6"
x="185"
y="97"
fill="var(--three-bars-bg)"
fill="var(--paragraph-secondary)"
ry="2.3"
transform="skewY(15) scale(.9669 1)"
/>
<path
fill="var(--three-bars-bg)"
fill="var(--paragraph-secondary)"
d="M181 169l99 26 2 3v8c0 1-1 2-2 1l-99-26-2-3v-7c0-2 1-2 2-2zm0-47l99 27 2 2v8l-2 2-99-27c-1 0-2-1-2-3v-7l2-2z"
/>
</g>
</symbol>
</svg>
<div id="splash" class="hidden">
<svg>
<use href="#themed-logo" />
</svg>
</div>
<div id="dialogContainer"></div>

View File

@@ -45,7 +45,6 @@ async function renderApp() {
<ErrorBoundary>
<BaseThemeProvider
onRender={() => document.getElementById("splash")?.remove()}
addGlobalStyles
sx={{ height: "100%", bg: "background" }}
>
<AppLock>

View File

@@ -24,34 +24,32 @@ import { desktop } from "../common/desktop-bridge";
import {
THEME_COMPATIBILITY_VERSION,
ThemeDark,
ThemeDefinition,
ThemeLight
} from "@notesnook/theme";
import { ThemesRouter } from "../common/themes-router";
/**
* @extends {BaseStore<ThemeStore>}
*/
class ThemeStore extends BaseStore {
/**
* @type {"dark" | "light"}
*/
colorScheme = Config.get("colorScheme", "light");
type ColorScheme = "dark" | "light";
class ThemeStore extends BaseStore<ThemeStore> {
colorScheme: "dark" | "light" = Config.get("colorScheme", "light");
darkTheme = getTheme("dark");
lightTheme = getTheme("light");
followSystemTheme = Config.get("followSystemTheme", false);
init = async () => {
const { darkTheme, lightTheme } = this.get();
const { darkTheme, lightTheme, colorScheme } = this.get();
await changeDesktopTheme(
colorScheme === "dark" ? darkTheme : lightTheme,
this.get().followSystemTheme
);
this.set({
darkTheme: await updateTheme(darkTheme),
lightTheme: await updateTheme(lightTheme)
});
};
/**
* @param {import("@notesnook/theme").ThemeDefinition} theme
*/
setTheme = (theme) => {
setTheme = (theme: ThemeDefinition) => {
changeDesktopTheme(theme, this.get().followSystemTheme);
Config.set(`theme:${theme.colorScheme}`, theme);
this.set({
[getKey(theme)]: theme,
@@ -59,21 +57,17 @@ class ThemeStore extends BaseStore {
});
};
setColorScheme = async (colorScheme) => {
if (!this.get().followSystemTheme)
await desktop?.integration.changeTheme.mutate(colorScheme);
setColorScheme = async (colorScheme: ColorScheme) => {
const theme = getTheme(colorScheme);
this.set({
colorScheme,
theme
});
Config.set("colorScheme", colorScheme);
this.set({ colorScheme, [getKey(theme)]: theme });
changeDesktopTheme(theme, this.get().followSystemTheme);
updateTheme(theme).then((theme) =>
this.set({
[getKey(theme)]: theme
})
);
updateTheme(theme).then((theme) => {
changeDesktopTheme(theme, this.get().followSystemTheme);
Config.set("colorScheme", colorScheme);
Config.set(`theme:${theme.colorScheme}`, theme);
this.set({ [getKey(theme)]: theme });
});
};
toggleColorScheme = () => {
@@ -81,12 +75,12 @@ class ThemeStore extends BaseStore {
this.setColorScheme(theme === "dark" ? "light" : "dark");
};
setFollowSystemTheme = async (followSystemTheme) => {
setFollowSystemTheme = async (followSystemTheme: boolean) => {
this.set({ followSystemTheme });
Config.set("followSystemTheme", followSystemTheme);
await desktop?.integration.changeTheme.mutate(
followSystemTheme ? "system" : "light"
);
await desktop?.integration.changeTheme.mutate({
theme: followSystemTheme ? "system" : this.get().colorScheme
});
};
toggleFollowSystemTheme = () => {
@@ -94,25 +88,27 @@ class ThemeStore extends BaseStore {
this.setFollowSystemTheme(!followSystemTheme);
};
isThemeCurrentlyApplied = (id) => {
isThemeCurrentlyApplied = (id: string) => {
return this.get().darkTheme.id === id || this.get().lightTheme.id === id;
};
}
const [useStore, store] = createStore((set, get) => new ThemeStore(set, get));
const [useStore, store] = createStore<ThemeStore>(
(set, get) => new ThemeStore(set, get)
);
export { useStore, store };
function getKey(theme) {
function getKey(theme: ThemeDefinition) {
return theme.colorScheme === "dark" ? "darkTheme" : "lightTheme";
}
function getTheme(colorScheme) {
function getTheme(colorScheme: ColorScheme) {
return colorScheme === "dark"
? Config.get("theme:dark", ThemeDark)
: Config.get("theme:light", ThemeLight);
}
async function updateTheme(theme) {
async function updateTheme(theme: ThemeDefinition) {
const { id, version } = theme;
try {
const updatedTheme = await ThemesRouter.updateTheme.query({
@@ -126,3 +122,11 @@ async function updateTheme(theme) {
return theme;
}
}
function changeDesktopTheme(theme: ThemeDefinition, system: boolean) {
return desktop?.integration.changeTheme.mutate({
theme: system ? "system" : theme.colorScheme,
backgroundColor: theme.scopes.base.primary.background,
windowControlsIconColor: theme.scopes.base.primary.icon
});
}

View File

@@ -0,0 +1,20 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
console.log("loading theme");

View File

@@ -114,6 +114,10 @@ export type ThemeScopes = {
* variant is missing in them.
*/
base: Variants<true>;
/**
* Scope for the title bar on Desktop & Web.
*/
titleBar?: PartialVariants;
/**
* Scope for the status bar on Desktop & mobile. On mobile, the status
* bar can be found at the bottom of the side navigation menu which

View File

@@ -74,12 +74,12 @@ export function themeToCSS(theme: ThemeDefinition) {
const scope = theme.scopes[scopeKey] || {};
const variants = buildVariants(scopeKey, theme, scope);
let scopeCss = `.theme-scope-${scopeKey} {`;
let scopeCss = `.theme-scope-${scopeKey} {\n\t`;
for (const variantKey in variants) {
const variant = variants[variantKey as keyof Variants];
if (!variant) continue;
css.push(`.theme-scope-${scopeKey}-${variant} {
css.push(`.theme-scope-${scopeKey}-${variantKey} {
${colorsToCSSVariables(variant, variantKey)}
}`);