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,68 +145,12 @@ 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
sx={{
gap: 2,
borderBottom: IS_DESKTOP_APP ? "1px solid var(--border)" : "none",
px: 1,
...(IS_DESKTOP_APP && !isFullscreen && hasNativeWindowControls
? getPlatform() === "darwin"
? { pl: "calc(100vw - env(titlebar-area-width))" }
: { 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 /> <TabStrip />
<Flex
bg="transparent"
sx={{
alignItems: "center",
justifyContent: "flex-end"
}}
>
{tools.map((tool) => ( {tools.map((tool) => (
<Button <Button
data-test-id={tool.title} data-test-id={tool.title}
@@ -244,9 +180,7 @@ export function EditorActionBar() {
<tool.icon size={18} /> <tool.icon size={18} />
</Button> </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,10 +49,16 @@ export function ErrorComponent({ error, resetErrorBoundary }: FallbackProps) {
height: "100%", height: "100%",
bg: "background", bg: "background",
display: "flex", display: "flex",
flexDirection: "column"
}}
>
<Flex
sx={{
width: "100%",
flexDirection: "column", flexDirection: "column",
justifyContent: "center",
alignItems: "center", alignItems: "center",
overflowY: "auto" overflowY: "auto",
py: 5
}} }}
> >
<Flex <Flex
@@ -143,6 +149,7 @@ ${getDeviceInfo()}`
</> </>
</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(
<>
{IS_DESKTOP_APP ? <TitleBar /> : null}
<ErrorBoundary> <ErrorBoundary>
<BaseThemeProvider <BaseThemeProvider
onRender={() => document.getElementById("splash")?.remove()} onRender={() => document.getElementById("splash")?.remove()}
sx={{ height: "100%", bg: "background" }} sx={{ bg: "background", flex: 1 }}
> >
<AppLock> <AppLock>
<Component route={props?.route || "login:email"} /> <Component route={props?.route || "login:email"} />
</AppLock> </AppLock>
</BaseThemeProvider> </BaseThemeProvider>
</ErrorBoundary> </ErrorBoundary>
</>
); );
} catch (e) { } catch (e) {
root.render( root.render(
<>
{IS_DESKTOP_APP ? <TitleBar /> : null}
<ErrorComponent <ErrorComponent
error={e} error={e}
resetErrorBoundary={() => window.location.reload()} 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