Skip to content

Commit

Permalink
fix: Respect app light/dark mode over OS preference #228
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Aug 23, 2024
1 parent 338803a commit ecef3e9
Show file tree
Hide file tree
Showing 15 changed files with 322 additions and 158 deletions.
8 changes: 7 additions & 1 deletion src/main/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import path from "node:path"

import { registerIpcMain } from "@egoist/tipc/main"
import { APP_PROTOCOL } from "@shared/constants"
import { app } from "electron"
import { app, nativeTheme } from "electron"

import { getIconPath } from "./helper"
import { store } from "./lib/store"
import { registerAppMenu } from "./menu"
import { initializeSentry } from "./sentry"
import { router } from "./tipc"
Expand Down Expand Up @@ -39,6 +40,11 @@ export const initializeApp = () => {
app.dock.setIcon(getIconPath())
}

// store.set("appearance", input);
const appearance = store.get("appearance")
if (appearance && ["light", "dark", "system"].includes(appearance)) {
nativeTheme.themeSource = appearance
}
// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.
registerAppMenu()
Expand Down
12 changes: 4 additions & 8 deletions src/main/tipc/setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createRequire } from "node:module"
import { app, nativeTheme } from "electron"

import { setDockCount } from "../lib/dock"
import { store } from "../lib/store"
import { createSettingWindow } from "../window"
import { t } from "./_instance"

Expand Down Expand Up @@ -31,20 +32,15 @@ export const settingRoute = {
})
}),
),
getAppearance: t.procedure.action(async () => nativeTheme.themeSource),
setAppearance: t.procedure
.input<"light" | "dark" | "system">()
.action(async ({ input }) => {
// NOTE: Temporarily changing to system to get the color mode that system is in at the moment may cause a bit of a problem.
// On macos, there is a bug, traffic lights flicker
nativeTheme.themeSource = "system"
const systemColorMode = nativeTheme.shouldUseDarkColors ?
"dark" :
"light"
nativeTheme.themeSource = input

nativeTheme.themeSource = systemColorMode === input ? "system" : input
store.set("appearance", input)
}),
setDockBadge: t.procedure.input<number>().action(async ({ input }) => {
setDockCount(input)
}),

}
14 changes: 0 additions & 14 deletions src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,6 @@
<!-- favicon -->
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
<title>Follow</title>

<script>
(function () {
var e =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches,
t = localStorage.getItem("theme") || '"system"';
t && t !== '"system"' && (document.documentElement.dataset.theme = t);
t &&
t === '"system"' &&
e &&
(document.documentElement.dataset.theme = "dark");
})();
</script>
</head>
<body>
<div id="root">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useDark } from "@renderer/hooks/common"
import { useIsDark } from "@renderer/hooks/common"

export const useShikiDefaultTheme = () => {
const isDark = useDark()
const isDark = useIsDark()

return isDark ? "github-dark" : "github-light"
}
6 changes: 2 additions & 4 deletions src/renderer/src/components/ui/media/preview-media.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { m } from "@renderer/components/common/Motion"
import { COPY_MAP, isElectronBuild } from "@renderer/constants"
import { COPY_MAP } from "@renderer/constants"
import { tipcClient } from "@renderer/lib/client"
import { stopPropagation } from "@renderer/lib/dom"
import { replaceImgUrlIfNeed } from "@renderer/lib/img-proxy"
Expand All @@ -23,9 +23,7 @@ const Wrapper: Component<{

return (
<div className="center relative size-full p-12" onClick={dismiss}>
{isElectronBuild && (
<div className="drag-region fixed inset-x-0 top-0 h-8" />
)}

<m.div
className="center size-full"
initial={{ scale: 0.94, opacity: 0 }}
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/src/components/ui/modal/stacked/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AppErrorBoundary } from "@renderer/components/common/AppErrorBoundary"
import { SafeFragment } from "@renderer/components/common/Fragment"
import { m } from "@renderer/components/common/Motion"
import { ErrorComponentType } from "@renderer/components/errors"
import { isElectronBuild } from "@renderer/constants"
import { useSwitchHotKeyScope } from "@renderer/hooks/common/useSwitchHotkeyScope"
import { nextFrame, stopPropagation } from "@renderer/lib/dom"
import { cn } from "@renderer/lib/utils"
Expand Down Expand Up @@ -238,6 +239,7 @@ export const ModalInternal = memo(
{title}
</Dialog.DialogTitle>
<Dialog.Content asChild onFocusCapture={stopPropagation}>

<div
ref={edgeElementRef}
className={cn(
Expand All @@ -257,6 +259,9 @@ export const ModalInternal = memo(
}
style={zIndexStyle}
>
{isElectronBuild && (
<div className="drag-region fixed inset-x-0 top-0 h-8" />
)}
<div
className={cn("contents", modalClassName)}
onClick={stopPropagation}
Expand Down Expand Up @@ -297,6 +302,10 @@ export const ModalInternal = memo(
undefined
}
>
{isElectronBuild && (
<div className="drag-region fixed inset-x-0 top-0 h-8" />
)}

<m.div
ref={modalElementRef}
style={modalStyle}
Expand Down
10 changes: 10 additions & 0 deletions src/renderer/src/components/ui/segement/ctx.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createContext } from "use-context-selector"

export interface SegmentGroupContextValue {
value: string
setValue: (value: string) => void
componentId: string
}
export const SegmentGroupContext = createContext<SegmentGroupContextValue>(
null!,
)
88 changes: 88 additions & 0 deletions src/renderer/src/components/ui/segement/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { cn } from "@renderer/lib/utils"
import { m } from "framer-motion"
import type { ReactNode } from "react"
import { useId, useMemo, useState } from "react"
import { useContextSelector } from "use-context-selector"

import { SegmentGroupContext } from "./ctx"

interface SegmentGroupProps {
value?: string
onValueChanged?: (value: string) => void
}
export const SegmentGroup = (props: ComponentType<SegmentGroupProps>) => {
const { onValueChanged, value, className } = props

const [currentValue, setCurrentValue] = useState(value || "")
const componentId = useId()

return (
<SegmentGroupContext.Provider
value={useMemo(
() => ({
value: currentValue,
setValue: (value) => {
setCurrentValue(value)
onValueChanged?.(value)
},
componentId,
}),
[componentId, currentValue, onValueChanged],
)}
>
<div
role="tablist"
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground outline-none",
className,
)}
tabIndex={0}
data-orientation="horizontal"
>
{props.children}
</div>
</SegmentGroupContext.Provider>
)
}

export const SegmentItem: Component<{
value: string
label: ReactNode
}> = ({ label, value, className }) => {
const isActive = useContextSelector(
SegmentGroupContext,
(v) => v.value === value,
)
const setValue = useContextSelector(SegmentGroupContext, (v) => v.setValue)
const layoutId = useContextSelector(
SegmentGroupContext,
(v) => v.componentId,
)
return (
<button
type="button"
role="tab"
className={cn(
"relative inline-flex items-center justify-center whitespace-nowrap px-3 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground",
"h-full rounded-md focus-visible:ring-accent/30",
className,
)}
tabIndex={-1}
data-orientation="horizontal"
onClick={() => {
setValue(value)
}}
data-state={isActive ? "active" : "inactive"}
>
<span className="z-[1]">{label}</span>

{isActive && (
<m.span
layout
layoutId={layoutId}
className="absolute inset-0 z-0 rounded-md bg-background shadow"
/>
)}
</button>
)
}
4 changes: 2 additions & 2 deletions src/renderer/src/components/ui/sonner.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useDark } from "@renderer/hooks/common"
import { useIsDark } from "@renderer/hooks/common"
import { Toaster as Sonner } from "sonner"

type ToasterProps = React.ComponentProps<typeof Sonner>

const Toaster = ({ ...props }: ToasterProps) => (
<Sonner
theme={useDark() ? "dark" : "light"}
theme={useIsDark() ? "dark" : "light"}
className="toaster group"
toastOptions={{
classNames: {
Expand Down
56 changes: 32 additions & 24 deletions src/renderer/src/hooks/common/useDark.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { tipcClient } from "@renderer/lib/client"
import { nextFrame } from "@renderer/lib/dom"
import { jotaiStore } from "@renderer/lib/jotai"
import { getStorageNS } from "@renderer/lib/ns"
Expand All @@ -8,7 +9,7 @@ import { useMediaQuery } from "usehooks-ts"

const useDarkQuery = () => useMediaQuery("(prefers-color-scheme: dark)")
type ColorMode = "light" | "dark" | "system"
const darkAtom = !window.electron ?
const themeAtom = !window.electron ?
atomWithStorage(
getStorageNS("color-mode"),
"system" as ColorMode,
Expand All @@ -19,28 +20,38 @@ const darkAtom = !window.electron ?
) :
atom("system" as ColorMode)
function useDarkElectron() {
return useAtomValue(darkAtom) === "dark"
return useAtomValue(themeAtom) === "dark"
}
function useDarkWebApp() {
const systemIsDark = useDarkQuery()
const mode = useAtomValue(darkAtom)
const mode = useAtomValue(themeAtom)
return mode === "dark" || (mode === "system" && systemIsDark)
}
export const useDark = window.electron ? useDarkElectron : useDarkWebApp
export const useIsDark = window.electron ? useDarkElectron : useDarkWebApp

const useSyncDarkElectron = () => {
export const useThemeAtomValue = () => useAtomValue(themeAtom)

const useSyncThemeElectron = () => {
const appIsDark = useDarkQuery()

useLayoutEffect(() => {
document.documentElement.dataset.theme = appIsDark ? "dark" : "light"
disableTransition(["[role=switch]>*"])()
let isMounted = true
tipcClient?.getAppearance().then((appearance) => {
if (!isMounted) return
jotaiStore.set(themeAtom, appearance)
disableTransition(["[role=switch]>*"])()

jotaiStore.set(darkAtom, appIsDark ? "dark" : "light")
document.documentElement.dataset.theme =
appearance === "system" ? (appIsDark ? "dark" : "light") : appearance
})
return () => {
isMounted = false
}
}, [appIsDark])
}

const useSyncDarkWebApp = () => {
const colorMode = useAtomValue(darkAtom)
const useSyncThemeWebApp = () => {
const colorMode = useAtomValue(themeAtom)
const systemIsDark = useDarkQuery()
useLayoutEffect(() => {
const realColorMode: Exclude<ColorMode, "system"> =
Expand All @@ -50,21 +61,18 @@ const useSyncDarkWebApp = () => {
}, [colorMode, systemIsDark])
}

export const useSyncDark = window.electron ?
useSyncDarkElectron :
useSyncDarkWebApp
export const useSyncThemeark = window.electron ?
useSyncThemeElectron :
useSyncThemeWebApp

export const useSetDarkInWebApp = () => {
const systemColorMode = useDarkQuery() ? "dark" : "light"
return useCallback(
(colorMode: Exclude<ColorMode, "system">) =>
jotaiStore.set(
darkAtom,
colorMode === systemColorMode ? "system" : colorMode,
),
[systemColorMode],
)
}
export const useSetTheme = () =>
useCallback((colorMode: ColorMode) => {
jotaiStore.set(themeAtom, colorMode)

if (window.electron) {
tipcClient?.setAppearance(colorMode)
}
}, [])

function disableTransition(disableTransitionExclude: string[] = []) {
const css = document.createElement("style")
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/src/modules/entry-content/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export function EntryHeader({

const entryTitleMeta = useEntryTitleMeta()
const isAtTop = useEntryContentScrollToTop()
const shouldShowMeta = !isAtTop && entryTitleMeta

const shouldShowMeta = !isAtTop && !!entryTitleMeta?.title

if (!entry?.entries) return null

return (
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/modules/entry-content/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export const EntryContentRender: Component<{ entryId: string }> = ({
"h-0 min-w-0 grow overflow-y-auto @container",
className,
)}
scrollbarClassName="mr-1"
scrollbarClassName="mr-[1.5px]"
viewportClassName="p-5"
ref={scrollerRef}
>
Expand Down Expand Up @@ -278,7 +278,7 @@ const TitleMetaHandler: Component<{

const atTop = useIsSoFWrappedElement()
useEffect(() => {
setEntryContentScrollToTop(false)
setEntryContentScrollToTop(true)
}, [entryId])
useLayoutEffect(() => {
setEntryContentScrollToTop(atTop)
Expand Down
Loading

0 comments on commit ecef3e9

Please sign in to comment.