refs #4887 Switch dark/light mode

This commit is contained in:
AkiraFukushima 2024-03-12 23:02:18 +09:00
parent bffa45e331
commit d058a81be4
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
7 changed files with 83 additions and 10 deletions

View File

@ -167,6 +167,9 @@
"title": "Settings", "title": "Settings",
"language": "Language", "language": "Language",
"font_size": "Font size", "font_size": "Font size",
"mode": "Color mode",
"dark_mode": "Dark mode",
"light_mode": "Light mode",
"theme": "Color theme" "theme": "Color theme"
}, },
"thirdparty": { "thirdparty": {

View File

@ -1,5 +1,5 @@
import { localeType } from '@/provider/i18n' import { localeType } from '@/provider/i18n'
import { Dialog, DialogBody, DialogHeader, Input, Option, Select, Typography } from '@material-tailwind/react' import { Dialog, DialogBody, DialogHeader, Input, Option, Radio, Select, Typography } from '@material-tailwind/react'
import { ChangeEvent, useEffect, useState } from 'react' import { ChangeEvent, useEffect, useState } from 'react'
import { FormattedMessage } from 'react-intl' import { FormattedMessage } from 'react-intl'
@ -55,6 +55,7 @@ export default function Settings(props: Props) {
const [language, setLanguage] = useState<localeType>('en') const [language, setLanguage] = useState<localeType>('en')
const [fontSize, setFontSize] = useState<number>(16) const [fontSize, setFontSize] = useState<number>(16)
const [theme, setTheme] = useState<string>('theme-blue') const [theme, setTheme] = useState<string>('theme-blue')
const [isDark, setIsDark] = useState(false)
useEffect(() => { useEffect(() => {
if (typeof localStorage !== 'undefined') { if (typeof localStorage !== 'undefined') {
@ -64,6 +65,12 @@ export default function Settings(props: Props) {
} else { } else {
setLanguage('en') setLanguage('en')
} }
const dark = localStorage.getItem('color-mode')
if (dark === 'dark') {
setIsDark(true)
} else {
setIsDark(false)
}
} }
}, []) }, [])
@ -91,6 +98,18 @@ export default function Settings(props: Props) {
props.reloadSettings() props.reloadSettings()
} }
const modeChanged = (isDark: boolean) => {
setIsDark(isDark)
if (typeof localStorage !== 'undefined') {
if (isDark) {
localStorage.setItem('color-mode', 'dark')
} else {
localStorage.setItem('color-mode', 'light')
}
}
props.reloadSettings()
}
return ( return (
<Dialog open={props.opened} handler={props.close} size="sm"> <Dialog open={props.opened} handler={props.close} size="sm">
<DialogHeader> <DialogHeader>
@ -124,6 +143,27 @@ export default function Settings(props: Props) {
</Select> </Select>
</div> </div>
</div> </div>
<div>
<div className="mb-2">
<Typography>
<FormattedMessage id="settings.mode" />
</Typography>
</div>
<div>
<Radio
name="mode"
label={<FormattedMessage id="settings.dark_mode" />}
onClick={() => modeChanged(true)}
defaultChecked={isDark}
/>
<Radio
name="mode"
label={<FormattedMessage id="settings.light_mode" />}
onClick={() => modeChanged(false)}
defaultChecked={!isDark}
/>
</div>
</div>
<div> <div>
<div className="mb-2"> <div className="mb-2">
<Typography> <Typography>

View File

@ -34,6 +34,7 @@ export default function Layout({ children }: LayoutProps) {
const [style, setStyle] = useState<CSSProperties>({}) const [style, setStyle] = useState<CSSProperties>({})
const [openPopover, setOpenPopover] = useState(false) const [openPopover, setOpenPopover] = useState(false)
const [theme, setTheme] = useState('theme-blue') const [theme, setTheme] = useState('theme-blue')
const [isDark, setIsDark] = useState(false)
const { switchLang } = useContext(Context) const { switchLang } = useContext(Context)
const router = useRouter() const router = useRouter()
@ -69,6 +70,11 @@ export default function Layout({ children }: LayoutProps) {
} }
}, []) }, [])
useEffect(() => {
console.log('isDark', isDark)
document.body.className = isDark ? 'dark' : 'light'
}, [isDark])
const closeNewModal = async () => { const closeNewModal = async () => {
const acct = await db.accounts.toArray() const acct = await db.accounts.toArray()
setAccounts(acct) setAccounts(acct)
@ -121,12 +127,19 @@ export default function Layout({ children }: LayoutProps) {
if (t && t.length > 0) { if (t && t.length > 0) {
setTheme(t) setTheme(t)
} }
const dark = localStorage.getItem('color-mode')
console.log(dark)
if (dark && dark === 'dark') {
setIsDark(true)
} else {
setIsDark(false)
}
} }
} }
return ( return (
<div className={`app flex flex-col min-h-screen ${theme}`} style={style}> <div className={`app flex flex-col min-h-screen ${theme}`} style={style}>
<main className="flex w-full box-border my-0 mx-auto min-h-screen"> <main className="flex w-full box-border my-0 mx-auto min-h-screen bg-white dark:bg-gray-900">
<aside className="w-16 theme-account-bg flex flex-col justify-between"> <aside className="w-16 theme-account-bg flex flex-col justify-between">
<div> <div>
{accounts.map(account => ( {accounts.map(account => (

View File

@ -12,7 +12,7 @@ export default function Card(props: Props) {
return ( return (
<div <div
className="flex border-inherit border border-solid rounded-md w-full cursor-pointer overflow-hidden text-ellipsis mb-1" className="flex border-inherit border border-solid border-gray-200 dark:border-gray-800 rounded-md w-full cursor-pointer overflow-hidden text-ellipsis mb-1"
onClick={openCard} onClick={openCard}
> >
<div style={{ height: '60px', width: '60px' }}> <div style={{ height: '60px', width: '60px' }}>

View File

@ -79,7 +79,7 @@ export default function Status(props: Props) {
props.filters.map(f => f.phrase).filter(keyword => props.status.content.toLowerCase().includes(keyword.toLowerCase())).length > 0 props.filters.map(f => f.phrase).filter(keyword => props.status.content.toLowerCase().includes(keyword.toLowerCase())).length > 0
) { ) {
return ( return (
<div className="border-b mr-2 py-2 text-center"> <div className="border-b border-gray-200 dark:border-gray-800 mr-2 py-2 text-center">
<FormattedMessage id="timeline.status.filtered" /> <FormattedMessage id="timeline.status.filtered" />
<span className="theme-text-subtle cursor-pointer pl-4" onClick={() => setIgnoreFilter(true)}> <span className="theme-text-subtle cursor-pointer pl-4" onClick={() => setIgnoreFilter(true)}>
<FormattedMessage id="timeline.status.show_anyway" /> <FormattedMessage id="timeline.status.show_anyway" />
@ -89,7 +89,7 @@ export default function Status(props: Props) {
} }
return ( return (
<div className="border-b mr-2 py-1"> <div className="border-b border-gray-200 dark:border-gray-800 mr-2 py-1">
{rebloggedHeader( {rebloggedHeader(
props.status, props.status,
formatMessage( formatMessage(
@ -114,16 +114,16 @@ export default function Status(props: Props) {
)} )}
/> />
</div> </div>
<div className="text-gray-950 break-all overflow-hidden" style={{ width: 'calc(100% - 56px)' }}> <div className="text-gray-950 dark:text-gray-300 break-all overflow-hidden" style={{ width: 'calc(100% - 56px)' }}>
<div className="flex justify-between"> <div className="flex justify-between">
<div className="flex cursor-pointer" onClick={() => openUser(status.account.id)}> <div className="flex cursor-pointer" onClick={() => openUser(status.account.id)}>
<span <span
className="text-gray-950 text-ellipsis break-all overflow-hidden" className="text-gray-950 dark:text-gray-300 text-ellipsis break-all overflow-hidden"
dangerouslySetInnerHTML={{ __html: emojify(status.account.display_name, status.account.emojis) }} dangerouslySetInnerHTML={{ __html: emojify(status.account.display_name, status.account.emojis) }}
></span> ></span>
<span className="text-gray-600 text-ellipsis break-all overflow-hidden">@{status.account.acct}</span> <span className="text-gray-600 dark:text-gray-500 text-ellipsis break-all overflow-hidden">@{status.account.acct}</span>
</div> </div>
<div className="text-gray-600 text-right cursor-pointer" onClick={openStatus}> <div className="text-gray-600 dark:text-gray-500 text-right cursor-pointer" onClick={openStatus}>
<time dateTime={status.created_at}>{dayjs(status.created_at).format('YYYY-MM-DD HH:mm:ss')}</time> <time dateTime={status.created_at}>{dayjs(status.created_at).format('YYYY-MM-DD HH:mm:ss')}</time>
</div> </div>
</div> </div>
@ -164,7 +164,7 @@ const originalStatus = (status: Entity.Status) => {
const rebloggedHeader = (status: Entity.Status, alt: string) => { const rebloggedHeader = (status: Entity.Status, alt: string) => {
if (status.reblog && !status.quote) { if (status.reblog && !status.quote) {
return ( return (
<div className="flex text-gray-600"> <div className="flex text-gray-600 dark:text-gray-500">
<div className="grid justify-items-end pr-2" style={{ width: '56px' }}> <div className="grid justify-items-end pr-2" style={{ width: '56px' }}>
<Avatar src={status.account.avatar} size="xs" variant="rounded" alt={alt} /> <Avatar src={status.account.avatar} size="xs" variant="rounded" alt={alt} />
</div> </div>

View File

@ -0,0 +1,16 @@
import Document, { Html, Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
render() {
const pageProps = this.props?.__NEXT_DATA__?.props?.pageProps
return (
<Html>
<Head />
<body className={pageProps.isDark ? 'dark' : 'light'}>
<Main />
<NextScript />
</body>
</Html>
)
}
}

View File

@ -1,6 +1,7 @@
const withMT = require('@material-tailwind/react/utils/withMT') const withMT = require('@material-tailwind/react/utils/withMT')
module.exports = withMT({ module.exports = withMT({
darkMode: 'selector',
content: ['./renderer/pages/**/*.{js,ts,jsx,tsx}', './renderer/components/**/*.{js,ts,jsx,tsx}'], content: ['./renderer/pages/**/*.{js,ts,jsx,tsx}', './renderer/components/**/*.{js,ts,jsx,tsx}'],
theme: { theme: {
extend: { extend: {