mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-05-18 13:16:11 +02:00
desktop: add custom titlebar
This commit is contained in:
committed by
Abdullah Atta
parent
be41cf1252
commit
23bd4a564f
@@ -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({});
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
83
apps/desktop/src/api/window.ts
Normal file
83
apps/desktop/src/api/window.ts
Normal 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
|
||||
);
|
||||
};
|
||||
});
|
||||
})
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -41,7 +41,10 @@ function setTheme(theme: Theme) {
|
||||
}
|
||||
|
||||
function getBackgroundColor() {
|
||||
return nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff";
|
||||
return (
|
||||
config.backgroundColor ||
|
||||
(nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff")
|
||||
);
|
||||
}
|
||||
|
||||
function getSystemTheme() {
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -45,7 +45,6 @@ export function ErrorComponent({ error, resetErrorBoundary }: FallbackProps) {
|
||||
return (
|
||||
<BaseThemeProvider
|
||||
onRender={() => document.getElementById("splash")?.remove()}
|
||||
addGlobalStyles
|
||||
sx={{
|
||||
height: "100%",
|
||||
bg: "background",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -232,6 +232,10 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
flexDirection: "column",
|
||||
display: "flex"
|
||||
}}
|
||||
trackStyle={() => ({
|
||||
width: 3
|
||||
})}
|
||||
thumbStyle={() => ({ width: 3 })}
|
||||
suppressScrollX={true}
|
||||
>
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
6
apps/web/src/global.d.ts
vendored
6
apps/web/src/global.d.ts
vendored
@@ -62,4 +62,10 @@ declare global {
|
||||
mode: "read-only" | "readwrite" | "readwrite-unsafe";
|
||||
}): Promise<FileSystemSyncAccessHandle>;
|
||||
}
|
||||
interface Navigator {
|
||||
windowControlsOverlay?: {
|
||||
getTitlebarAreaRect(): DOMRect;
|
||||
visible: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
66
apps/web/src/hooks/use-window-controls.ts
Normal file
66
apps/web/src/hooks/use-window-controls.ts
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -45,7 +45,6 @@ async function renderApp() {
|
||||
<ErrorBoundary>
|
||||
<BaseThemeProvider
|
||||
onRender={() => document.getElementById("splash")?.remove()}
|
||||
addGlobalStyles
|
||||
sx={{ height: "100%", bg: "background" }}
|
||||
>
|
||||
<AppLock>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
20
apps/web/src/theme-loader.ts
Normal file
20
apps/web/src/theme-loader.ts
Normal 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");
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user