Skip to content

Commit

Permalink
feat(client): support theme mode
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed May 23, 2023
1 parent ee73c78 commit 677d71d
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 101 deletions.
38 changes: 28 additions & 10 deletions packages/client/client/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RemovableRef, useDark, useLocalStorage } from '@vueuse/core'
import { reactive, watch } from 'vue'
import { RemovableRef, useLocalStorage, usePreferredDark } from '@vueuse/core'
import { reactive, watchEffect } from 'vue'

export let useStorage = <T extends object>(key: string, version: number, fallback?: () => T): RemovableRef<T> => {
const initial = fallback ? fallback() : {} as T
Expand Down Expand Up @@ -32,23 +32,41 @@ export function createStorage<T extends object>(key: string, version: number, fa
}

export interface Config {
theme?: string
theme: Config.Theme
locale?: string
}

const isDark = useDark()
export namespace Config {
export interface Theme {
mode: 'auto' | 'dark' | 'light'
dark: string
light: string
}
}

export const config = useStorage<Config>('config', 1, () => ({
theme: isDark.value ? 'default-dark' : 'default-light',
export const config = useStorage<Config>('config', 1.1, () => ({
theme: {
mode: 'auto',
dark: 'default-dark',
light: 'default-light',
},
locale: 'zh-CN',
}))

watch(() => config.value.theme, (theme) => {
const isDark = usePreferredDark()

function resolveThemeMode(theme: Config.Theme) {
if (theme.mode !== 'auto') return theme.mode
return isDark.value ? 'dark' : 'light'
}

watchEffect(() => {
const root = window.document.querySelector('html')
root.setAttribute('theme', theme)
if (theme.endsWith('-dark')) {
const mode = resolveThemeMode(config.value.theme)
root.setAttribute('theme', config.value.theme[mode])
if (mode === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}, { flush: 'post', immediate: true })
}, { flush: 'post' })
13 changes: 3 additions & 10 deletions packages/client/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createRouter, createWebHistory, START_LOCATION } from 'vue-router'
import { global } from './data'
import install from './components'
import Overlay from './components/chat/overlay.vue'
import Settings from './settings/index.vue'
import internal from './settings'
import { config } from './config'
import { initTask } from './loader'
import { Context } from './context'
Expand Down Expand Up @@ -36,20 +36,13 @@ root.app.use(install)
root.app.use(i18n)
root.app.use(router)

root.plugin(internal)

root.slot({
type: 'global',
component: Overlay,
})

root.page({
path: '/settings/:name*',
name: '用户设置',
icon: 'activity:settings',
position: 'bottom',
order: -100,
component: Settings,
})

root.on('activity', data => !data)

router.beforeEach(async (to, from) => {
Expand Down
28 changes: 28 additions & 0 deletions packages/client/client/settings/appearance.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<k-content>
<k-form v-model="config" :initial="config" :schema="schema"></k-form>
</k-content>
</template>

<script lang="ts" setup>
import { config } from '..'
import { Schema } from '@koishijs/components'
const schema = Schema.object({
theme: Schema.object({
mode: Schema.union([
Schema.const('auto').description('跟随系统'),
Schema.const('dark').description('深色'),
Schema.const('light').description('浅色'),
]).description('主题偏好。'),
dark: Schema.string().role('theme', { mode: 'dark' }).description('深色主题。'),
light: Schema.string().role('theme', { mode: 'light' }).description('浅色主题。'),
}).description('主题设置'),
})
</script>

<style lang="scss" scoped>
</style>
21 changes: 21 additions & 0 deletions packages/client/client/settings/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Context } from '..'
import components from '@koishijs/components'
import Settings from './index.vue'
import Theme from './theme.vue'

components.extensions.add({
type: 'string',
role: 'theme',
component: Theme,
})

export default function (ctx: Context) {
ctx.page({
path: '/settings/:name*',
name: '用户设置',
icon: 'activity:settings',
position: 'bottom',
order: -100,
component: Settings,
})
}
4 changes: 2 additions & 2 deletions packages/client/client/settings/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import { useRoute, useRouter } from 'vue-router'
import { computed } from 'vue'
import General from './general.vue'
import Theme from './theme.vue'
import Appearance from './appearance.vue'
const route = useRoute()
const router = useRouter()
Expand All @@ -36,7 +36,7 @@ const components = {
},
appearance: {
label: '外观设置',
component: Theme,
component: Appearance,
},
}
Expand Down
162 changes: 83 additions & 79 deletions packages/client/client/settings/theme.vue
Original file line number Diff line number Diff line change
@@ -1,109 +1,113 @@
<template>
<k-content>
<div class="theme-gallery">
<template v-for="(_, key) in themes" :key="key">
<div class="theme-item" @click="config.theme = key">
<div class="theme-container">
<schema-base>
<template #title><slot name="title"></slot></template>
<template #desc><slot name="desc"></slot></template>
<template #menu><slot name="menu"></slot></template>
<template #prefix><slot name="prefix"></slot></template>
<template #suffix><slot name="suffix"></slot></template>
<template #control>
<el-select popper-class="theme-select" v-model="model">
<template v-for="(_, key) in themes" :key="key">
<el-option :value="key" v-if="key.endsWith('-' + schema.meta.extra.mode)">
<div class="theme-root" :class="key.endsWith('-dark') ? 'dark' : 'light'" :theme="key">
<div class="theme-block-1"></div>
<div class="theme-block-2"></div>
<div class="theme-block-3"></div>
<div class="theme-content">
<span class="active">Koi</span>
<span class="normal">shi</span>
<div class="theme-title">
{{ tt(themes[key].name) }}
</div>
</div>
</div>
<el-radio :model-value="config.theme" :label="key">
{{ tt(themes[key].name) }}
</el-radio>
</div>
</template>
</div>
</k-content>
</el-option>
</template>
</el-select>
</template>
</schema-base>
</template>

<script lang="ts" setup>
<script setup lang="ts">
import { config, themes, useI18nText } from '..'
import { PropType, computed } from 'vue'
import { themes, useI18nText } from '..'
import { Schema, SchemaBase, useConfig } from '@koishijs/components'
const tt = useI18nText()
defineProps({
schema: {} as PropType<Schema>,
modelValue: {} as PropType<any[]>,
disabled: {} as PropType<boolean>,
prefix: {} as PropType<string>,
initial: {} as PropType<{}>,
})
</script>
const emit = defineEmits(['update:modelValue'])
<style lang="scss" scoped>
const tt = useI18nText()
.theme-gallery {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
}
const config = useConfig()
.theme-item {
display: flex;
flex-direction: column;
justify-content: center;
cursor: pointer;
gap: 0.5rem;
const model = computed({
get() {
return tt(themes[config.value].name)
},
set(value) {
emit('update:modelValue', value)
},
})
.el-radio {
justify-content: center;
}
}
.theme-container {
border: 1px solid var(--k-color-border);
}
</script>

.theme-root {
width: 240px;
height: 180px;
position: relative;
<style lang="scss">
.theme-block-1 {
position: absolute;
width: 33%;
height: 100%;
background-color: var(--bg1);
}
.el-select-dropdown.theme-select {
overflow: hidden;
.theme-block-2 {
position: absolute;
left: 33%;
width: 34%;
height: 100%;
background-color: var(--bg2);
.el-select-dropdown__list {
margin: 0 !important;
}
.theme-block-3 {
position: absolute;
left: 67%;
width: 33%;
height: 100%;
background-color: var(--bg3);
.el-select-dropdown__item {
padding: 0;
}
.theme-content {
position: absolute;
left: 0;
.theme-root {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-size: 2rem;
z-index: 100;
font-weight: 500;
letter-spacing: 1px;
font-family: var(--font-family);
span.normal {
color: var(--k-text-dark);
position: relative;
.theme-block-1 {
position: absolute;
width: 33%;
height: 100%;
background-color: var(--bg1);
}
.theme-block-2 {
position: absolute;
left: 33%;
width: 34%;
height: 100%;
background-color: var(--bg2);
}
.theme-block-3 {
position: absolute;
left: 67%;
width: 33%;
height: 100%;
background-color: var(--bg3);
}
span.active {
.theme-title {
position: absolute;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-size: 1em;
z-index: 100;
font-family: var(--font-family);
color: var(--k-color-primary);
}
}
Expand Down

0 comments on commit 677d71d

Please sign in to comment.