forked from Mirrors/elk
fix: rework setup to improve SSR compatibility
This commit is contained in:
parent
fd7d30a38a
commit
d8d163dbd0
22 changed files with 137 additions and 73 deletions
1
app.vue
1
app.vue
|
@ -1,6 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
setupI18n()
|
setupI18n()
|
||||||
setupFontSize()
|
|
||||||
setupPageHeader()
|
setupPageHeader()
|
||||||
setupEmojis()
|
setupEmojis()
|
||||||
provideGlobalCommands()
|
provideGlobalCommands()
|
||||||
|
|
|
@ -5,6 +5,7 @@ defineProps<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const dropdown = $ref<any>()
|
const dropdown = $ref<any>()
|
||||||
|
const colorMode = useColorModeRef()
|
||||||
|
|
||||||
provide(dropdownContextKey, {
|
provide(dropdownContextKey, {
|
||||||
hide: () => dropdown.hide(),
|
hide: () => dropdown.hide(),
|
||||||
|
@ -12,7 +13,7 @@ provide(dropdownContextKey, {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VDropdown v-bind="$attrs" ref="dropdown" :class="{ dark: isDark }" :placement="placement || 'auto'">
|
<VDropdown v-bind="$attrs" ref="dropdown" :class="colorMode" :placement="placement || 'auto'">
|
||||||
<slot />
|
<slot />
|
||||||
<template #popper="scope">
|
<template #popper="scope">
|
||||||
<slot name="popper" v-bind="scope" />
|
<slot name="popper" v-bind="scope" />
|
||||||
|
|
|
@ -12,7 +12,7 @@ defineProps<{
|
||||||
>
|
>
|
||||||
<div flex justify-between px5 py4>
|
<div flex justify-between px5 py4>
|
||||||
<div flex gap-3 items-center overflow-hidden>
|
<div flex gap-3 items-center overflow-hidden>
|
||||||
<NuxtLink v-if="back" flex="~ gap1" items-center btn-text p-0 @click="$router.go(-1)">
|
<NuxtLink v-show="back" flex="~ gap1" items-center btn-text p-0 @click="$router.go(-1)">
|
||||||
<div i-ri:arrow-left-line />
|
<div i-ri:arrow-left-line />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<div truncate>
|
<div truncate>
|
||||||
|
|
|
@ -6,6 +6,7 @@ const emits = defineEmits<{
|
||||||
(event: 'update:modelValue', value: boolean): void
|
(event: 'update:modelValue', value: boolean): void
|
||||||
}>()
|
}>()
|
||||||
const visible = useVModel(props, 'modelValue', emits, { passive: true })
|
const visible = useVModel(props, 'modelValue', emits, { passive: true })
|
||||||
|
const colorMode = useColorModeRef()
|
||||||
|
|
||||||
function changeShow() {
|
function changeShow() {
|
||||||
visible.value = !visible.value
|
visible.value = !visible.value
|
||||||
|
@ -22,6 +23,10 @@ function clickEvent(mouse: MouseEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleDark() {
|
||||||
|
colorMode.value = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||||
|
}
|
||||||
|
|
||||||
watch(visible, (val) => {
|
watch(visible, (val) => {
|
||||||
if (val && typeof document !== 'undefined')
|
if (val && typeof document !== 'undefined')
|
||||||
document.addEventListener('click', clickEvent)
|
document.addEventListener('click', clickEvent)
|
||||||
|
@ -79,7 +84,7 @@ onBeforeUnmount(() => {
|
||||||
@click="toggleDark()"
|
@click="toggleDark()"
|
||||||
>
|
>
|
||||||
<span class="i-ri:sun-line dark:i-ri:moon-line flex-shrink-0 text-xl mr-4 !align-middle" />
|
<span class="i-ri:sun-line dark:i-ri:moon-line flex-shrink-0 text-xl mr-4 !align-middle" />
|
||||||
{{ !isDark ? $t('menu.toggle_theme.dark') : $t('menu.toggle_theme.light') }}
|
{{ colorMode === 'light' ? $t('menu.toggle_theme.dark') : $t('menu.toggle_theme.light') }}
|
||||||
</button>
|
</button>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
flex flex-row items-center
|
flex flex-row items-center
|
||||||
|
|
|
@ -5,6 +5,11 @@ const timeAgoOptions = useTimeAgoOptions()
|
||||||
|
|
||||||
const buildTimeDate = new Date(buildInfo.time)
|
const buildTimeDate = new Date(buildInfo.time)
|
||||||
const buildTimeAgo = useTimeAgo(buildTimeDate, timeAgoOptions)
|
const buildTimeAgo = useTimeAgo(buildTimeDate, timeAgoOptions)
|
||||||
|
|
||||||
|
const colorMode = useColorModeRef()
|
||||||
|
function toggleDark() {
|
||||||
|
colorMode.value = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -9,12 +9,13 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const el = $ref<HTMLElement>()
|
const el = $ref<HTMLElement>()
|
||||||
let picker = $ref<Picker>()
|
let picker = $ref<Picker>()
|
||||||
|
const colorMode = useColorModeRef()
|
||||||
|
|
||||||
async function openEmojiPicker() {
|
async function openEmojiPicker() {
|
||||||
await updateCustomEmojis()
|
await updateCustomEmojis()
|
||||||
if (picker) {
|
if (picker) {
|
||||||
picker.update({
|
picker.update({
|
||||||
theme: isDark.value ? 'dark' : 'light',
|
theme: colorMode.value,
|
||||||
custom: customEmojisData.value,
|
custom: customEmojisData.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -28,7 +29,7 @@ async function openEmojiPicker() {
|
||||||
? emit('select', native)
|
? emit('select', native)
|
||||||
: emit('selectCustom', { src, alt, 'data-emoji-id': name })
|
: emit('selectCustom', { src, alt, 'data-emoji-id': name })
|
||||||
},
|
},
|
||||||
theme: isDark.value ? 'dark' : 'light',
|
theme: colorMode.value,
|
||||||
custom: customEmojisData.value,
|
custom: customEmojisData.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
function setDark(v: boolean) {
|
import type { ColorMode } from '~/types'
|
||||||
isDark.value = v
|
|
||||||
|
const colorMode = useColorModeRef()
|
||||||
|
|
||||||
|
function setColorMode(mode: ColorMode) {
|
||||||
|
colorMode.value = mode
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -8,16 +12,16 @@ function setDark(v: boolean) {
|
||||||
<div flex="~ gap4" w-full>
|
<div flex="~ gap4" w-full>
|
||||||
<button
|
<button
|
||||||
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base
|
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base
|
||||||
:class="isDark ? 'pointer-events-none' : 'filter-saturate-0'"
|
:class="colorMode === 'dark' ? 'pointer-events-none' : 'filter-saturate-0'"
|
||||||
@click="setDark(true)"
|
@click="setColorMode('dark')"
|
||||||
>
|
>
|
||||||
<div i-ri:moon-line />
|
<div i-ri:moon-line />
|
||||||
Dark Mode
|
Dark Mode
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base
|
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base
|
||||||
:class="!isDark ? 'pointer-events-none' : 'filter-saturate-0'"
|
:class="colorMode === 'light' ? 'pointer-events-none' : 'filter-saturate-0'"
|
||||||
@click="setDark(false)"
|
@click="setColorMode('light')"
|
||||||
>
|
>
|
||||||
<div i-ri:sun-line />
|
<div i-ri:sun-line />
|
||||||
Light Mode
|
Light Mode
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { FontSize } from '~/composables/fontSize'
|
import type { FontSize } from '~/types'
|
||||||
|
|
||||||
const sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as FontSize[]
|
const sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as FontSize[]
|
||||||
const fontSize = getFontSize()
|
const fontSize = useFontSizeRef()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -121,13 +121,13 @@ onMounted(async () => {
|
||||||
text-left z-10 shadow of-auto
|
text-left z-10 shadow of-auto
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="server, idx in filteredServers"
|
v-for="name, idx in filteredServers"
|
||||||
:key="server"
|
:key="name"
|
||||||
:value="server"
|
:value="name"
|
||||||
px-2 py1 font-mono
|
px-2 py1 font-mono
|
||||||
:class="autocompleteIndex === idx ? 'text-primary font-bold' : null"
|
:class="autocompleteIndex === idx ? 'text-primary font-bold' : null"
|
||||||
>
|
>
|
||||||
{{ server }}
|
{{ name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -206,6 +206,7 @@ export const provideGlobalCommands = () => {
|
||||||
const { locales } = useI18n() as { locales: ComputedRef<LocaleObject[]> }
|
const { locales } = useI18n() as { locales: ComputedRef<LocaleObject[]> }
|
||||||
const users = useUsers()
|
const users = useUsers()
|
||||||
const masto = useMasto()
|
const masto = useMasto()
|
||||||
|
const colorMode = useColorModeRef()
|
||||||
|
|
||||||
useCommand({
|
useCommand({
|
||||||
scope: 'Actions',
|
scope: 'Actions',
|
||||||
|
@ -225,10 +226,10 @@ export const provideGlobalCommands = () => {
|
||||||
scope: 'Preferences',
|
scope: 'Preferences',
|
||||||
|
|
||||||
name: () => t('command.toggle_dark_mode'),
|
name: () => t('command.toggle_dark_mode'),
|
||||||
icon: () => isDark.value ? 'i-ri:sun-line' : 'i-ri:moon-line',
|
icon: () => colorMode.value === 'light' ? 'i-ri:sun-line' : 'i-ri:moon-line',
|
||||||
|
|
||||||
onActivate() {
|
onActivate() {
|
||||||
toggleDark()
|
colorMode.value = colorMode.value === 'light' ? 'dark' : 'light'
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
export const isDark = useDark()
|
|
||||||
export const toggleDark = useToggle(isDark)
|
|
|
@ -1,39 +0,0 @@
|
||||||
import type { InjectionKey, Ref } from 'vue'
|
|
||||||
import { STORAGE_KEY_FONT_SIZE } from '~/constants'
|
|
||||||
|
|
||||||
const InjectionKeyFontSize = Symbol('fontSize') as InjectionKey<Ref<FontSize>>
|
|
||||||
const DEFAULT = 'md'
|
|
||||||
|
|
||||||
export type FontSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
||||||
|
|
||||||
export function getFontSize() {
|
|
||||||
return inject(InjectionKeyFontSize)!
|
|
||||||
}
|
|
||||||
|
|
||||||
const fontSizeMap = {
|
|
||||||
xs: '13px',
|
|
||||||
sm: '14px',
|
|
||||||
md: '15px',
|
|
||||||
lg: '16px',
|
|
||||||
xl: '17px',
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setupFontSize() {
|
|
||||||
const fontSize = useCookie<FontSize>(STORAGE_KEY_FONT_SIZE, { default: () => DEFAULT })
|
|
||||||
getCurrentInstance()?.appContext.app.provide(InjectionKeyFontSize, fontSize)
|
|
||||||
|
|
||||||
if (!process.server) {
|
|
||||||
watchEffect(() => {
|
|
||||||
document.documentElement.style.setProperty('--font-size', fontSizeMap[fontSize.value || DEFAULT])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
useHead({
|
|
||||||
style: [
|
|
||||||
{
|
|
||||||
innerHTML: `:root { --font-size: ${fontSizeMap[fontSize.value || DEFAULT]}; }`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
14
composables/injections.ts
Normal file
14
composables/injections.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { InjectionKeyColorMode, InjectionKeyFontSize } from '~/constants/symbols'
|
||||||
|
|
||||||
|
export function useFontSizeRef() {
|
||||||
|
return inject(InjectionKeyFontSize)!
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useColorModeRef() {
|
||||||
|
return inject(InjectionKeyColorMode)!
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleColorMode() {
|
||||||
|
const colorMode = useColorModeRef()
|
||||||
|
colorMode.value = colorMode.value === 'light' ? 'dark' : 'light'
|
||||||
|
}
|
|
@ -1,15 +1,10 @@
|
||||||
import { useRegisterSW } from 'virtual:pwa-register/vue'
|
import { useRegisterSW } from 'virtual:pwa-register/vue'
|
||||||
|
|
||||||
export const usePWA = () => {
|
export function usePWA() {
|
||||||
const online = useOnline()
|
const online = useOnline()
|
||||||
|
|
||||||
useHead({
|
|
||||||
meta: [{ id: 'theme-color', name: 'theme-color', content: computed(() => isDark.value ? '#111111' : '#ffffff') }],
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
needRefresh,
|
needRefresh, updateServiceWorker,
|
||||||
updateServiceWorker,
|
|
||||||
} = useRegisterSW({
|
} = useRegisterSW({
|
||||||
immediate: true,
|
immediate: true,
|
||||||
onRegisteredSW(swUrl, r) {
|
onRegisteredSW(swUrl, r) {
|
||||||
|
|
|
@ -45,9 +45,6 @@ export function setupPageHeader() {
|
||||||
titleTemplate: title => `${title ? `${title} | ` : ''}${APP_NAME}${isDev ? ' (dev)' : isPreview ? ' (preview)' : ''}`,
|
titleTemplate: title => `${title ? `${title} | ` : ''}${APP_NAME}${isDev ? ' (dev)' : isPreview ? ' (preview)' : ''}`,
|
||||||
link,
|
link,
|
||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-expressions
|
|
||||||
isDark.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupI18n() {
|
export async function setupI18n() {
|
||||||
|
|
|
@ -44,7 +44,7 @@ export function useHightlighter(lang: Lang) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useShikiTheme() {
|
export function useShikiTheme() {
|
||||||
return isDark.value ? 'vitesse-dark' : 'vitesse-light'
|
return useColorModeRef().value ? 'vitesse-dark' : 'vitesse-light'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function highlightCode(code: string, lang: Lang) {
|
export function highlightCode(code: string, lang: Lang) {
|
||||||
|
|
|
@ -11,7 +11,6 @@ export const STORAGE_KEY_NOTIFY_TAB = 'elk-notify-tab'
|
||||||
export const STORAGE_KEY_FIRST_VISIT = 'elk-first-visit'
|
export const STORAGE_KEY_FIRST_VISIT = 'elk-first-visit'
|
||||||
export const STORAGE_KEY_ZEN_MODE = 'elk-zenmode'
|
export const STORAGE_KEY_ZEN_MODE = 'elk-zenmode'
|
||||||
export const STORAGE_KEY_LANG = 'elk-lang'
|
export const STORAGE_KEY_LANG = 'elk-lang'
|
||||||
export const STORAGE_KEY_FONT_SIZE = 'elk-font-size'
|
|
||||||
export const STORAGE_KEY_FEATURE_FLAGS = 'elk-feature-flags'
|
export const STORAGE_KEY_FEATURE_FLAGS = 'elk-feature-flags'
|
||||||
export const STORAGE_KEY_CUSTOM_EMOJIS = 'elk-custom-emojis'
|
export const STORAGE_KEY_CUSTOM_EMOJIS = 'elk-custom-emojis'
|
||||||
export const STORAGE_KEY_HIDE_EXPLORE_POSTS_TIPS = 'elk-hide-explore-posts-tips'
|
export const STORAGE_KEY_HIDE_EXPLORE_POSTS_TIPS = 'elk-hide-explore-posts-tips'
|
||||||
|
@ -20,4 +19,7 @@ export const STORAGE_KEY_HIDE_EXPLORE_TAGS_TIPS = 'elk-hide-explore-tags-tips'
|
||||||
export const STORAGE_KEY_NOTIFICATION = 'elk-notification'
|
export const STORAGE_KEY_NOTIFICATION = 'elk-notification'
|
||||||
export const STORAGE_KEY_NOTIFICATION_POLICY = 'elk-notification-policy'
|
export const STORAGE_KEY_NOTIFICATION_POLICY = 'elk-notification-policy'
|
||||||
|
|
||||||
|
export const COOKIE_KEY_FONT_SIZE = 'elk-font-size'
|
||||||
|
export const COOKIE_KEY_COLOR_MODE = 'elk-color-mode'
|
||||||
|
|
||||||
export const HANDLED_MASTO_URLS = /^(https?:\/\/)?([\w\d-]+\.)+\w+\/(@[@\w\d-\.]+)(\/objects)?(\/\d+)?$/
|
export const HANDLED_MASTO_URLS = /^(https?:\/\/)?([\w\d-]+\.)+\w+\/(@[@\w\d-\.]+)(\/objects)?(\/\d+)?$/
|
||||||
|
|
7
constants/options.ts
Normal file
7
constants/options.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const fontSizeMap = {
|
||||||
|
xs: '13px',
|
||||||
|
sm: '14px',
|
||||||
|
md: '15px',
|
||||||
|
lg: '16px',
|
||||||
|
xl: '17px',
|
||||||
|
}
|
5
constants/symbols.ts
Normal file
5
constants/symbols.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import type { InjectionKey, Ref } from 'vue'
|
||||||
|
import type { ColorMode, FontSize } from '~/types'
|
||||||
|
|
||||||
|
export const InjectionKeyFontSize = Symbol('font-size') as InjectionKey<Ref<FontSize>>
|
||||||
|
export const InjectionKeyColorMode = Symbol('color-mode') as InjectionKey<Ref<ColorMode>>
|
41
plugins/setup-color-mode.ts
Normal file
41
plugins/setup-color-mode.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import type { ColorMode } from '~/types'
|
||||||
|
import { InjectionKeyColorMode } from '~/constants/symbols'
|
||||||
|
import { COOKIE_KEY_COLOR_MODE } from '~/constants'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxt) => {
|
||||||
|
const cookieColorMode = useCookie<ColorMode | null>(COOKIE_KEY_COLOR_MODE, { default: () => null })
|
||||||
|
|
||||||
|
const preferColorMode = process.server ? computed(() => 'light') : usePreferredColorScheme()
|
||||||
|
const colorMode = computed<ColorMode>({
|
||||||
|
get() {
|
||||||
|
return cookieColorMode.value || preferColorMode.value as ColorMode
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
cookieColorMode.value = value
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
nuxt.vueApp.provide(InjectionKeyColorMode, colorMode)
|
||||||
|
|
||||||
|
if (process.server) {
|
||||||
|
useHead({
|
||||||
|
htmlAttrs: {
|
||||||
|
class: colorMode,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
watchEffect(() => {
|
||||||
|
document.documentElement.classList.toggle('dark', colorMode.value === 'dark')
|
||||||
|
document.documentElement.classList.toggle('light', colorMode.value === 'light')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
meta: [{
|
||||||
|
id: 'theme-color',
|
||||||
|
name: 'theme-color',
|
||||||
|
content: computed(() => colorMode.value === 'dark' ? '#111111' : '#ffffff'),
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
})
|
25
plugins/setup-font-size.ts
Normal file
25
plugins/setup-font-size.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import type { FontSize } from '~/types'
|
||||||
|
import { InjectionKeyFontSize } from '~/constants/symbols'
|
||||||
|
import { COOKIE_KEY_FONT_SIZE } from '~/constants'
|
||||||
|
import { fontSizeMap } from '~/constants/options'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxt) => {
|
||||||
|
const DEFAULT = 'md'
|
||||||
|
const cookieFontSize = useCookie<FontSize>(COOKIE_KEY_FONT_SIZE, { default: () => DEFAULT })
|
||||||
|
nuxt.vueApp.provide(InjectionKeyFontSize, cookieFontSize)
|
||||||
|
|
||||||
|
if (!process.server) {
|
||||||
|
watchEffect(() => {
|
||||||
|
document.documentElement.style.setProperty('--font-size', fontSizeMap[cookieFontSize.value || DEFAULT])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
useHead({
|
||||||
|
style: [
|
||||||
|
{
|
||||||
|
innerHTML: `:root { --font-size: ${fontSizeMap[cookieFontSize.value || DEFAULT]}; }`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
|
@ -72,3 +72,6 @@ export interface BuildInfo {
|
||||||
time: number
|
time: number
|
||||||
branch: string
|
branch: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FontSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||||
|
export type ColorMode = 'light' | 'dark'
|
||||||
|
|
Loading…
Reference in a new issue