refactor: dark mode and support system theme (#908)
This commit is contained in:
parent
90e51a5a26
commit
1da7bbc280
|
|
@ -97,6 +97,7 @@
|
|||
"mermaid": "10.3.1",
|
||||
"nanoid": "4.0.2",
|
||||
"next": "13.4.12",
|
||||
"next-themes": "0.2.1",
|
||||
"node-id3": "0.2.6",
|
||||
"open-graph-scraper": "^6.2.2",
|
||||
"pangu": "4.0.7",
|
||||
|
|
|
|||
|
|
@ -206,6 +206,9 @@ dependencies:
|
|||
next:
|
||||
specifier: 13.4.12
|
||||
version: 13.4.12(@babel/core@7.22.10)(react-dom@18.2.0)(react@18.2.0)
|
||||
next-themes:
|
||||
specifier: 0.2.1
|
||||
version: 0.2.1(next@13.4.12)(react-dom@18.2.0)(react@18.2.0)
|
||||
node-id3:
|
||||
specifier: 0.2.6
|
||||
version: 0.2.6
|
||||
|
|
@ -9214,6 +9217,18 @@ packages:
|
|||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||
dev: true
|
||||
|
||||
/next-themes@0.2.1(next@13.4.12)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
|
||||
peerDependencies:
|
||||
next: '*'
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
dependencies:
|
||||
next: 13.4.12(@babel/core@7.22.10)(react-dom@18.2.0)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/next@13.4.12(@babel/core@7.22.10)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-eHfnru9x6NRmTMcjQp6Nz0J4XH9OubmzOa7CkWL+AUrUxpibub3vWwttjduu9No16dug1kq04hiUUpo7J3m3Xw==}
|
||||
engines: {node: '>=16.8.0'}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,16 @@
|
|||
"use client"
|
||||
|
||||
import { useServerInsertedHTML } from "next/navigation"
|
||||
|
||||
import {
|
||||
COLOR_SCHEME_DARK,
|
||||
COLOR_SCHEME_LIGHT,
|
||||
DARK_MODE_STORAGE_KEY,
|
||||
} from "~/lib/constants"
|
||||
import { DARK_MODE_STORAGE_KEY } from "~/lib/constants"
|
||||
|
||||
export const ColorSchemeInjector = () => {
|
||||
useServerInsertedHTML(() => {
|
||||
return (
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(() => {
|
||||
return (
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(() => {
|
||||
let DARK_MODE_STORAGE_KEY = "${DARK_MODE_STORAGE_KEY}";
|
||||
let namespace = "xlog"
|
||||
let getStorage = (key) => {
|
||||
let data = {}
|
||||
try {
|
||||
data = JSON.parse(localStorage.getItem(namespace) || "{}")
|
||||
} catch (error) {}
|
||||
return data[key]
|
||||
}
|
||||
let COLOR_SCHEME_LIGHT = "${COLOR_SCHEME_LIGHT}";
|
||||
let COLOR_SCHEME_DARK = "${COLOR_SCHEME_DARK}";
|
||||
|
||||
let data = {}
|
||||
const isDark = getStorage(DARK_MODE_STORAGE_KEY)
|
||||
const isDark = localStorage.getItem(DARK_MODE_STORAGE_KEY)
|
||||
if (typeof isDark === "undefined") {
|
||||
const currentColorScheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
|
|
@ -40,9 +23,7 @@ if (typeof isDark === "undefined") {
|
|||
)
|
||||
}
|
||||
})();`,
|
||||
}}
|
||||
></script>
|
||||
)
|
||||
})
|
||||
return null
|
||||
}}
|
||||
></script>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,9 @@ export default function RootLayout({
|
|||
className={colorScheme}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<ColorSchemeInjector />
|
||||
<head>
|
||||
<ColorSchemeInjector />
|
||||
</head>
|
||||
<body>
|
||||
<Providers lang={lang}>
|
||||
{modal}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { ThemeProvider } from "next-themes"
|
||||
import { useState } from "react"
|
||||
import { WagmiConfig } from "wagmi"
|
||||
|
||||
|
|
@ -11,10 +12,11 @@ import {
|
|||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
|
||||
import { ModalStackProvider } from "~/components/ui/ModalStack"
|
||||
import { useDarkModeListener } from "~/hooks/useDarkMode"
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { useDarkMode } from "~/hooks/useDarkMode"
|
||||
import { useMobileLayout } from "~/hooks/useMobileLayout"
|
||||
import { useNProgress } from "~/hooks/useNProgress"
|
||||
import { DARK_MODE_STORAGE_KEY } from "~/lib/constants"
|
||||
import { APP_NAME, WALLET_CONNECT_V2_PROJECT_ID } from "~/lib/env"
|
||||
import { filterNotificationCharacter } from "~/lib/filter-character"
|
||||
import { toGateway } from "~/lib/ipfs-parser"
|
||||
|
|
@ -42,31 +44,37 @@ export default function Providers({
|
|||
children: React.ReactNode
|
||||
lang: string
|
||||
}) {
|
||||
useDarkMode()
|
||||
useMobileLayout()
|
||||
useNProgress()
|
||||
useDarkModeListener()
|
||||
|
||||
const [queryClient] = useState(createQueryClient)
|
||||
|
||||
return (
|
||||
<WagmiConfig config={wagmiConfig}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConnectKitProvider
|
||||
ipfsLinkToHttpLink={toGateway}
|
||||
urlComposer={urlComposer}
|
||||
signInStrategy="simple"
|
||||
ignoreWalletDisconnectEvent={true}
|
||||
>
|
||||
<LangProvider lang={lang}>
|
||||
<ModalStackProvider>{children}</ModalStackProvider>
|
||||
</LangProvider>
|
||||
<ThemeProvider
|
||||
disableTransitionOnChange
|
||||
storageKey={DARK_MODE_STORAGE_KEY}
|
||||
attribute="class"
|
||||
>
|
||||
<WagmiConfig config={wagmiConfig}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConnectKitProvider
|
||||
ipfsLinkToHttpLink={toGateway}
|
||||
urlComposer={urlComposer}
|
||||
signInStrategy="simple"
|
||||
ignoreWalletDisconnectEvent={true}
|
||||
>
|
||||
<LangProvider lang={lang}>
|
||||
<ModalStackProvider>{children}</ModalStackProvider>
|
||||
</LangProvider>
|
||||
|
||||
<NotificationModal
|
||||
colorScheme={colorScheme}
|
||||
filter={filterNotificationCharacter}
|
||||
/>
|
||||
</ConnectKitProvider>
|
||||
</QueryClientProvider>
|
||||
</WagmiConfig>
|
||||
<NotificationModal
|
||||
colorScheme={colorScheme}
|
||||
filter={filterNotificationCharacter}
|
||||
/>
|
||||
</ConnectKitProvider>
|
||||
</QueryClientProvider>
|
||||
</WagmiConfig>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,130 @@
|
|||
"use client"
|
||||
|
||||
import { Switch } from "@headlessui/react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { MouseEvent } from "react"
|
||||
|
||||
import { useDarkModeSwitch, useIsDark } from "~/hooks/useDarkMode"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { useIsClient } from "~/hooks/useClient"
|
||||
|
||||
export const DarkModeSwitch = () => {
|
||||
const toggle = useDarkModeSwitch()
|
||||
const isDark = useIsDark()
|
||||
const styles = {
|
||||
base: " rounded-inherit inline-flex h-[32px] w-[32px] items-center justify-center border-0 text-current",
|
||||
}
|
||||
|
||||
const ThemeIndicator = () => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const isClient = useIsClient()
|
||||
|
||||
if (!isClient) return null
|
||||
if (!theme) return null
|
||||
return (
|
||||
<Switch
|
||||
checked={isDark || false}
|
||||
onClick={(e) => toggle(e)}
|
||||
className={cn(
|
||||
`${isDark ? "bg-accent" : "bg-gray-200"}`,
|
||||
` relative inline-flex h-6 w-11 items-center rounded-full text-base dark:text-always-gray-200 text-always-gray-700 align-middle`,
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Switch Dark Mode</span>
|
||||
<span
|
||||
className={`${
|
||||
isDark ? "translate-x-6" : "translate-x-1"
|
||||
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
|
||||
/>
|
||||
|
||||
{isDark ? (
|
||||
<i className="icon-[mingcute--moon-line] translate-x-2 scale-75" />
|
||||
) : (
|
||||
<i className="icon-[mingcute--sun-line] scale-75 -translate-x-3" />
|
||||
)}
|
||||
</Switch>
|
||||
<div
|
||||
className="absolute top-[3px] z-[-1] h-[32px] w-[32px] rounded-full bg-zinc-200/80 shadow-[0_1px_2px_0_rgba(127.5,127.5,127.5,.2),_0_1px_3px_0_rgba(127.5,127.5,127.5,.1)] duration-200 bg-blend-multiply transition-[left]"
|
||||
style={{
|
||||
left: { light: 4, system: 36, dark: 68 }[theme],
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ThemeSwitcher = () => {
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
const buildThemeTransition = (
|
||||
e: MouseEvent,
|
||||
theme: "light" | "dark" | "system",
|
||||
) => {
|
||||
return setTheme(theme)
|
||||
// disable in https://github.com/Crossbell-Box/xLog/commit/06713c901e90b57de6f5f5a0252d2c9dd874335f
|
||||
if (
|
||||
!("startViewTransition" in document) ||
|
||||
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
|
||||
) {
|
||||
setTheme(theme)
|
||||
return
|
||||
}
|
||||
|
||||
const $document = document.documentElement
|
||||
|
||||
const { x, y } = e.currentTarget.getBoundingClientRect()
|
||||
|
||||
const endRadius = Math.hypot(
|
||||
Math.max(x, window.innerWidth - x),
|
||||
Math.max(y, window.innerHeight - y),
|
||||
)
|
||||
|
||||
document
|
||||
.startViewTransition(() => {
|
||||
return Promise.resolve()
|
||||
})
|
||||
?.ready.then(() => {
|
||||
setTheme(theme)
|
||||
|
||||
if (x === 0) return
|
||||
const clipPath = [
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${endRadius}px at ${x}px ${y}px)`,
|
||||
]
|
||||
|
||||
$document.animate(
|
||||
{
|
||||
clipPath,
|
||||
},
|
||||
{
|
||||
duration: 300,
|
||||
easing: "ease-in",
|
||||
pseudoElement: "::view-transition-new(root)",
|
||||
// pseudoElement: darkMode
|
||||
// ? "::view-transition-old(root)"
|
||||
// : "::view-transition-new(root)",
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
className="w-fit-content inline-flex rounded-full border border-slate-200 p-[3px]"
|
||||
>
|
||||
<button
|
||||
aria-label="Switch to light theme"
|
||||
type="button"
|
||||
className={styles.base}
|
||||
onClick={(e) => {
|
||||
buildThemeTransition(e, "light")
|
||||
}}
|
||||
>
|
||||
<i className="icon-[mingcute--sun-line] scale-75" />
|
||||
</button>
|
||||
<button
|
||||
aria-label="Switch to system theme"
|
||||
className={styles.base}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
buildThemeTransition(e, "system")
|
||||
}}
|
||||
>
|
||||
<i className="icon-[mingcute--computer-line] scale-75" />
|
||||
</button>
|
||||
<button
|
||||
aria-label="Switch to dark theme"
|
||||
className={styles.base}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
buildThemeTransition(e, "dark")
|
||||
}}
|
||||
>
|
||||
<i className="icon-[mingcute--moon-line] scale-75" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const DarkModeSwitch = () => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<ThemeSwitcher />
|
||||
<ThemeIndicator />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,231 +1,31 @@
|
|||
import { MouseEvent, useEffect, useRef, useState } from "react"
|
||||
import { create } from "zustand"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useEffect } from "react"
|
||||
|
||||
import {
|
||||
COLOR_SCHEME_DARK,
|
||||
COLOR_SCHEME_LIGHT,
|
||||
DARK_MODE_STORAGE_KEY,
|
||||
DEFAULT_COLOR_SCHEME,
|
||||
IS_DEV,
|
||||
} from "~/lib/constants"
|
||||
import { COLOR_SCHEME_DARK, COLOR_SCHEME_LIGHT, IS_DEV } from "~/lib/constants"
|
||||
import { OUR_DOMAIN } from "~/lib/env"
|
||||
import { noop } from "~/lib/noop"
|
||||
import { delStorage, getStorage, setStorage } from "~/lib/storage"
|
||||
import { isServerSide } from "~/lib/utils"
|
||||
|
||||
import { useGetState } from "./useGetState"
|
||||
|
||||
interface IMediaStore {
|
||||
isDark: boolean
|
||||
toggle: (e: MouseEvent) => void
|
||||
}
|
||||
|
||||
const useMediaStore = create<IMediaStore>(() => {
|
||||
return {
|
||||
isDark: DEFAULT_COLOR_SCHEME === COLOR_SCHEME_DARK,
|
||||
toggle: () => void 0,
|
||||
}
|
||||
})
|
||||
|
||||
interface DarkModeConfig {
|
||||
classNameDark?: string // A className to set "dark mode". Default = "dark".
|
||||
classNameLight?: string // A className to set "light mode". Default = "light".
|
||||
element?: HTMLElement | undefined | null // The element to apply the className. Default = `document.body`.
|
||||
transition?: ViewTransition | undefined // Specify the `animate` when switching the mode. Only Chromium >= 111 etc.
|
||||
}
|
||||
|
||||
const useDarkModeInternal = (
|
||||
initialState: boolean | undefined,
|
||||
options: DarkModeConfig,
|
||||
) => {
|
||||
const {
|
||||
classNameDark = COLOR_SCHEME_DARK,
|
||||
classNameLight = COLOR_SCHEME_LIGHT,
|
||||
element,
|
||||
transition,
|
||||
} = options
|
||||
|
||||
const [darkMode, setDarkMode] = useState(initialState)
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
|
||||
|
||||
export const useDarkModeListener = () => {
|
||||
const { theme, systemTheme } = useTheme()
|
||||
useEffect(() => {
|
||||
const presentedDarkMode =
|
||||
!isServerSide() && getStorage(DARK_MODE_STORAGE_KEY)
|
||||
if (theme === "system") {
|
||||
// delete cookie
|
||||
document.cookie = IS_DEV
|
||||
? `color_scheme=;`
|
||||
: `color_scheme=; Domain=.${OUR_DOMAIN}; Path=/; Secure; expires=Thu, 01 Jan 1970 00:00:00 GMT`
|
||||
|
||||
if (presentedDarkMode !== undefined) {
|
||||
setDarkMode(presentedDarkMode === "true")
|
||||
} else if (typeof initialState === "undefined") {
|
||||
setDarkMode(window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MediaQueryListEvent) => {
|
||||
const storageValue = getStorage(DARK_MODE_STORAGE_KEY)
|
||||
const parseStorageValueAsBool = storageValue === "true"
|
||||
setDarkMode(e.matches)
|
||||
|
||||
// reset dark mode, follow system
|
||||
if (parseStorageValueAsBool === e.matches) {
|
||||
delStorage(DARK_MODE_STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
const storageHandler = () => {
|
||||
const storageValue = getStorage(DARK_MODE_STORAGE_KEY, true)
|
||||
// if not storage color mode, switch to follow system
|
||||
if (storageValue === undefined) {
|
||||
setDarkMode(window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
} else {
|
||||
// make multiple pages to switch to dark mode together.
|
||||
setDarkMode(storageValue === "true")
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("storage", storageHandler)
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", handler)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", storageHandler)
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.removeEventListener("change", handler)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getDarkMode = useGetState(darkMode)
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
// if set color mode follow system, del storage
|
||||
if (
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches ===
|
||||
getDarkMode()
|
||||
) {
|
||||
delStorage(DARK_MODE_STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
window.addEventListener("beforeunload", handler)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handler)
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (isServerSide() || typeof darkMode === "undefined") {
|
||||
return
|
||||
}
|
||||
const $document = element || document.documentElement
|
||||
const setDarkModeClass = () => {
|
||||
if (darkMode) {
|
||||
$document.classList.remove(classNameLight)
|
||||
$document.classList.add(classNameDark)
|
||||
} else {
|
||||
$document.classList.remove(classNameDark)
|
||||
$document.classList.add(classNameLight)
|
||||
}
|
||||
}
|
||||
|
||||
const { x, y } = mousePosition
|
||||
const endRadius = Math.hypot(
|
||||
Math.max(x, innerWidth - x),
|
||||
Math.max(y, innerHeight - y),
|
||||
)
|
||||
transition?.ready.then(() => {
|
||||
setDarkModeClass()
|
||||
if (mousePosition.x === 0) return
|
||||
const clipPath = [
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${endRadius}px at ${x}px ${y}px)`,
|
||||
]
|
||||
$document.animate(
|
||||
{
|
||||
clipPath: !darkMode ? clipPath : [...clipPath].reverse(),
|
||||
},
|
||||
{
|
||||
duration: 400,
|
||||
easing: "ease-in-out",
|
||||
pseudoElement: darkMode
|
||||
? "::view-transition-old(root)"
|
||||
: "::view-transition-new(root)",
|
||||
},
|
||||
)
|
||||
}) ?? setDarkModeClass()
|
||||
}, [classNameDark, classNameLight, darkMode, element])
|
||||
|
||||
if (isServerSide()) {
|
||||
return {
|
||||
toggle: () => {},
|
||||
value: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value: darkMode,
|
||||
toggle: (e: MouseEvent) => {
|
||||
setDarkMode((d) => {
|
||||
if (DARK_MODE_STORAGE_KEY && !isServerSide()) {
|
||||
setStorage(DARK_MODE_STORAGE_KEY, String(!d))
|
||||
setMousePosition({ x: e.clientX, y: e.clientY })
|
||||
}
|
||||
return !d
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const mockElement = {
|
||||
classList: {
|
||||
add: noop,
|
||||
remove: noop,
|
||||
},
|
||||
}
|
||||
|
||||
export const useDarkMode = () => {
|
||||
const { toggle, value } = useDarkModeInternal(
|
||||
getStorage(DARK_MODE_STORAGE_KEY) === undefined
|
||||
? undefined
|
||||
: getStorage(DARK_MODE_STORAGE_KEY) === "true",
|
||||
{
|
||||
classNameDark: COLOR_SCHEME_DARK,
|
||||
classNameLight: COLOR_SCHEME_LIGHT,
|
||||
element: (globalThis.document && document.documentElement) || mockElement,
|
||||
// transition:
|
||||
// !isServerSide() &&
|
||||
// !!document.startViewTransition &&
|
||||
// !window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
|
||||
// ? document.startViewTransition()
|
||||
// : undefined,
|
||||
},
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
useMediaStore.setState({
|
||||
isDark: value,
|
||||
})
|
||||
const colorScheme = value ? COLOR_SCHEME_DARK : COLOR_SCHEME_LIGHT
|
||||
const isDarkMode = theme === "dark"
|
||||
const colorScheme = isDarkMode ? COLOR_SCHEME_DARK : COLOR_SCHEME_LIGHT
|
||||
const date = new Date()
|
||||
date.setMonth(date.getMonth() + 1)
|
||||
document.cookie = IS_DEV
|
||||
? `color_scheme=${colorScheme};`
|
||||
: `color_scheme=${colorScheme}; Domain=.${OUR_DOMAIN}; Path=/; Secure; expires=${date.toUTCString()}`
|
||||
}, [value])
|
||||
|
||||
const onceRef = useRef(false)
|
||||
if (!onceRef.current) {
|
||||
onceRef.current = true
|
||||
useMediaStore.setState({ toggle })
|
||||
}
|
||||
|
||||
return {
|
||||
toggle,
|
||||
value,
|
||||
}
|
||||
}, [theme, systemTheme])
|
||||
}
|
||||
|
||||
export const useIsDark = () => useMediaStore((state) => state.isDark)
|
||||
|
||||
export const useDarkModeSwitch = () => {
|
||||
return useMediaStore((state) => state.toggle)
|
||||
export const useIsDark = () => {
|
||||
const { theme, systemTheme } = useTheme()
|
||||
return theme === "dark" || (theme === "system" && systemTheme === "dark")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue