desktop: fix custom title bar on linux

This commit is contained in:
Abdullah Atta
2024-04-01 17:17:28 +05:00
parent f99d4bc013
commit b43d51aa72
10 changed files with 289 additions and 197 deletions

View File

@@ -84,6 +84,7 @@ async function createWindow() {
}), }),
titleBarStyle: "hidden", titleBarStyle: "hidden",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: { titleBarOverlay: {
height: 37, height: 37,
color: "#00000000", color: "#00000000",

View File

@@ -4,10 +4,12 @@
} }
.tabsScroll, .tabsScroll,
.titlebarLogo { .titlebarLogo,
.theme-scope-titleBar {
-webkit-app-region: drag; -webkit-app-region: drag;
} }
.theme-scope-titleBar button,
.tabsScroll .tab { .tabsScroll .tab {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }

View File

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

View File

@@ -35,11 +35,7 @@ import {
Search, Search,
TableOfContents, TableOfContents,
Trash, Trash,
Unlock, Unlock
WindowClose,
WindowMaximize,
WindowMinimize,
WindowRestore
} from "../icons"; } from "../icons";
import { ScrollContainer } from "@notesnook/ui"; import { ScrollContainer } from "@notesnook/ui";
import { import {
@@ -68,16 +64,12 @@ import {
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import { AppEventManager, AppEvents } from "../../common/app-events"; import { AppEventManager, AppEvents } from "../../common/app-events";
import { desktop } from "../../common/desktop-bridge";
import { useWindowControls } from "../../hooks/use-window-controls"; import { useWindowControls } from "../../hooks/use-window-controls";
import { ScopedThemeProvider } from "../theme-provider";
import { getPlatform } from "../../utils/platform";
export function EditorActionBar() { export function EditorActionBar() {
const editorMargins = useEditorStore((store) => store.editorMargins); const editorMargins = useEditorStore((store) => store.editorMargins);
const isFocusMode = useAppStore((store) => store.isFocusMode); const isFocusMode = useAppStore((store) => store.isFocusMode);
const { isMaximized, isFullscreen, hasNativeWindowControls } = const { isFullscreen } = useWindowControls();
useWindowControls();
const activeSession = useEditorStore((store) => const activeSession = useEditorStore((store) =>
store.activeSessionId ? store.getSession(store.activeSessionId) : undefined store.activeSessionId ? store.getSession(store.activeSessionId) : undefined
); );
@@ -153,100 +145,42 @@ export function EditorActionBar() {
activeSession.type !== "conflicted" && activeSession.type !== "conflicted" &&
!isFocusMode, !isFocusMode,
onClick: () => useEditorStore.getState().toggleProperties() 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 ( return (
<ScopedThemeProvider scope="titleBar" injectCssVars> <>
<Flex <TabStrip />
sx={{ {tools.map((tool) => (
gap: 2, <Button
borderBottom: IS_DESKTOP_APP ? "1px solid var(--border)" : "none", data-test-id={tool.title}
px: 1, disabled={!tool.enabled}
...(IS_DESKTOP_APP && !isFullscreen && hasNativeWindowControls variant={tool.title === "Close" ? "error" : "secondary"}
? getPlatform() === "darwin" title={tool.title}
? { pl: "calc(100vw - env(titlebar-area-width))" } key={tool.title}
: { pr: "calc(100vw - env(titlebar-area-width))" }
: {})
}}
>
{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={{ sx={{
height: "100%",
alignItems: "center", alignItems: "center",
justifyContent: "flex-end" 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}
> >
{tools.map((tool) => ( <tool.icon size={18} />
<Button </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>
</ScopedThemeProvider>
); );
} }

View File

@@ -91,7 +91,17 @@ export default function TabsView() {
return ( return (
<> <>
{IS_DESKTOP_APP ? null : <EditorActionBar />} {IS_DESKTOP_APP ? (
ReactDOM.createPortal(
<EditorActionBar />,
document.getElementById("titlebar-portal-container")!
)
) : (
<Flex>
<EditorActionBar />
</Flex>
)}
<ScopedThemeProvider <ScopedThemeProvider
scope="editor" scope="editor"
ref={dropRef} ref={dropRef}

View File

@@ -49,98 +49,105 @@ export function ErrorComponent({ error, resetErrorBoundary }: FallbackProps) {
height: "100%", height: "100%",
bg: "background", bg: "background",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column"
justifyContent: "center",
alignItems: "center",
overflowY: "auto"
}} }}
> >
<Flex <Flex
sx={{ sx={{
width: ["95%", "50%"], width: "100%",
flexDirection: "column" flexDirection: "column",
alignItems: "center",
overflowY: "auto",
py: 5
}} }}
> >
<Image <Flex
src={colorScheme === "dark" ? LogoDark : Logo} sx={{
sx={{ borderRadius: "default", width: 60, alignSelf: "start" }} width: ["95%", "50%"],
mb={4} flexDirection: "column"
/> }}
<Text
variant="heading"
sx={{ borderBottom: "1px solid var(--border)", pb: 1 }}
> >
Something went wrong <Image
</Text> src={colorScheme === "dark" ? LogoDark : Logo}
<ErrorText error={error} /> sx={{ borderRadius: "default", width: 60, alignSelf: "start" }}
{help ? ( mb={4}
<> />
<Text variant="subtitle" sx={{ mt: 2 }}> <Text
What went wrong? variant="heading"
</Text> sx={{ borderBottom: "1px solid var(--border)", pb: 1 }}
<Text variant="body">{help.explanation}</Text> >
<Text variant="subtitle" sx={{ mt: 1 }}> Something went wrong
How to fix it? </Text>
</Text> <ErrorText error={error} />
<Text variant="body">{help.action}</Text>
</>
) : null}
<Flex sx={{ gap: 1 }}>
{help ? ( {help ? (
<Button <>
variant="error" <Text variant="subtitle" sx={{ mt: 2 }}>
sx={{ alignSelf: "start", px: 30, mt: 1 }} What went wrong?
onClick={() => </Text>
help.fix().catch((e) => { <Text variant="body">{help.explanation}</Text>
console.error(e); <Text variant="subtitle" sx={{ mt: 1 }}>
alert(errorToString(e)); How to fix it?
}) </Text>
} <Text variant="body">{help.action}</Text>
> </>
Fix it ) : null}
</Button> <Flex sx={{ gap: 1 }}>
) : ( {help ? (
<Button <Button
variant="error" variant="error"
sx={{ alignSelf: "start", px: 30, mt: 1 }} sx={{ alignSelf: "start", px: 30, mt: 1 }}
onClick={() => window.location.reload()} onClick={() =>
> help.fix().catch((e) => {
Reload app console.error(e);
</Button> alert(errorToString(e));
)} })
<> }
<Button >
variant="secondary" Fix it
sx={{ alignSelf: "start", px: 30, mt: 1 }} </Button>
onClick={async () => { ) : (
navigator.clipboard.writeText(errorToString(error)); <Button
}} variant="error"
> sx={{ alignSelf: "start", px: 30, mt: 1 }}
Copy onClick={() => window.location.reload()}
</Button> >
<Button Reload app
variant="secondary" </Button>
sx={{ alignSelf: "start", px: 30, mt: 1 }} )}
onClick={async () => { <>
const { getDeviceInfo } = await import( <Button
"../../dialogs/issue-dialog" variant="secondary"
); sx={{ alignSelf: "start", px: 30, mt: 1 }}
const mailto = new URL("mailto:support@streetwriters.co"); onClick={async () => {
mailto.searchParams.set( navigator.clipboard.writeText(errorToString(error));
"body", }}
`${errorToString(error)} >
Copy
</Button>
<Button
variant="secondary"
sx={{ alignSelf: "start", px: 30, mt: 1 }}
onClick={async () => {
const { getDeviceInfo } = await import(
"../../dialogs/issue-dialog"
);
const mailto = new URL("mailto:support@streetwriters.co");
mailto.searchParams.set(
"body",
`${errorToString(error)}
--- ---
Device information: Device information:
${getDeviceInfo()}` ${getDeviceInfo()}`
); );
window.open(mailto.toString(), "_blank"); window.open(mailto.toString(), "_blank");
}} }}
> >
Contact support Contact support
</Button> </Button>
</> </>
</Flex>
</Flex> </Flex>
</Flex> </Flex>
</BaseThemeProvider> </BaseThemeProvider>

View File

@@ -0,0 +1,128 @@
/*
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 { Button } from "@theme-ui/components";
import { desktop } from "../../common/desktop-bridge";
import { useWindowControls } from "../../hooks/use-window-controls";
import { getPlatform } from "../../utils/platform";
import {
WindowClose,
WindowMaximize,
WindowMinimize,
WindowRestore
} from "../icons";
import { BaseThemeProvider } from "../theme-provider";
export function TitleBar() {
const { isMaximized, isFullscreen, hasNativeWindowControls } =
useWindowControls();
const tools = [
{
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 (
<BaseThemeProvider
scope="titleBar"
sx={{
background: "background",
height: 37.8,
display: "flex",
borderBottom: "1px solid var(--border)",
...(!isFullscreen && hasNativeWindowControls
? getPlatform() === "darwin"
? { pl: "calc(100vw - env(titlebar-area-width))" }
: { pr: "calc(100vw - env(titlebar-area-width))" }
: { pl: 2, pr: 0 })
}}
injectCssVars
>
{getPlatform() !== "darwin" || isFullscreen ? (
<svg
className="titlebarLogo"
style={{
alignSelf: "center",
height: 24,
width: 24,
marginRight: 10
}}
>
<use href="#themed-logo" />
</svg>
) : null}
<div
id="titlebar-portal-container"
style={{
flex: 1,
display: "flex",
overflow: "hidden"
}}
/>
{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.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>
))}
</BaseThemeProvider>
);
}

View File

@@ -130,6 +130,11 @@
overflow: hidden; overflow: hidden;
} }
#root {
display: flex;
flex-direction: column;
}
@keyframes fadeUp { @keyframes fadeUp {
0% { 0% {
transform: translateY(500px); transform: translateY(500px);

View File

@@ -25,6 +25,7 @@ import { BaseThemeProvider } from "./components/theme-provider";
import { register } from "./utils/stream-saver/mitm"; import { register } from "./utils/stream-saver/mitm";
import { getServiceWorkerVersion } from "./utils/version"; import { getServiceWorkerVersion } from "./utils/version";
import { ErrorBoundary, ErrorComponent } from "./components/error-boundary"; import { ErrorBoundary, ErrorComponent } from "./components/error-boundary";
import { TitleBar } from "./components/title-bar";
renderApp(); renderApp();
@@ -36,29 +37,38 @@ async function renderApp() {
try { try {
const { component, props, path } = await init(); const { component, props, path } = await init();
const { useKeyStore } = await import("./interfaces/key-store");
await useKeyStore.getState().init();
if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker(); if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker();
const { default: Component } = await component(); const { default: Component } = await component();
const { default: AppLock } = await import("./views/app-lock"); const { default: AppLock } = await import("./views/app-lock");
root.render( root.render(
<ErrorBoundary> <>
<BaseThemeProvider {IS_DESKTOP_APP ? <TitleBar /> : null}
onRender={() => document.getElementById("splash")?.remove()} <ErrorBoundary>
sx={{ height: "100%", bg: "background" }} <BaseThemeProvider
> onRender={() => document.getElementById("splash")?.remove()}
<AppLock> sx={{ bg: "background", flex: 1 }}
<Component route={props?.route || "login:email"} /> >
</AppLock> <AppLock>
</BaseThemeProvider> <Component route={props?.route || "login:email"} />
</ErrorBoundary> </AppLock>
</BaseThemeProvider>
</ErrorBoundary>
</>
); );
} catch (e) { } catch (e) {
root.render( root.render(
<ErrorComponent <>
error={e} {IS_DESKTOP_APP ? <TitleBar /> : null}
resetErrorBoundary={() => window.location.reload()} <ErrorComponent
/> error={e}
resetErrorBoundary={() => window.location.reload()}
/>
</>
); );
} }
} }

View File

@@ -38,7 +38,6 @@ import { getDocumentTitle, setDocumentTitle } from "../utils/dom";
import { CredentialWithoutSecret, useKeyStore } from "../interfaces/key-store"; import { CredentialWithoutSecret, useKeyStore } from "../interfaces/key-store";
export default function AppLock(props: PropsWithChildren<unknown>) { export default function AppLock(props: PropsWithChildren<unknown>) {
const init = usePromise(() => useKeyStore.getState().init());
const credentials = useKeyStore((store) => store.activeCredentials()); const credentials = useKeyStore((store) => store.activeCredentials());
const isLocked = useKeyStore((store) => store.isLocked); const isLocked = useKeyStore((store) => store.isLocked);
const _lockAfter = useKeyStore((store) => store.secrets.lockAfter); const _lockAfter = useKeyStore((store) => store.secrets.lockAfter);
@@ -120,8 +119,6 @@ export default function AppLock(props: PropsWithChildren<unknown>) {
} }
}, [lockAfter, credentials]); }, [lockAfter, credentials]);
if (init.status !== "fulfilled") return null;
if (isLocked) if (isLocked)
return ( return (
<Flex <Flex