refactor: unify user settings (#821)

Co-authored-by: 三咲智子 <sxzz@sxzz.moe>
This commit is contained in:
Anthony Fu 2023-01-06 18:39:37 +01:00 committed by GitHub
parent 35c9a871be
commit 1aa118283e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 105 additions and 61 deletions

View file

@ -22,9 +22,9 @@ function toggleDark() {
<button <button
flex flex
text-lg text-lg
:class="isZenMode ? 'i-ri:layout-right-2-line' : 'i-ri:layout-right-line'" :class="userSettings.zenMode ? 'i-ri:layout-right-2-line' : 'i-ri:layout-right-line'"
:aria-label="$t('nav.zen_mode')" :aria-label="$t('nav.zen_mode')"
@click="toggleZenMode()" @click="userSettings.zenMode = !userSettings.zenMode"
/> />
</CommonTooltip> </CommonTooltip>
</div> </div>

View file

@ -126,7 +126,7 @@ async function editStatus() {
<template #popper> <template #popper>
<div flex="~ col"> <div flex="~ col">
<template v-if="isZenMode"> <template v-if="userSettings.zenMode">
<CommonDropdownItem <CommonDropdownItem
:text="$t('action.reply')" :text="$t('action.reply')"
icon="i-ri:chat-3-line" icon="i-ri:chat-3-line"

View file

@ -138,7 +138,7 @@ const isDM = $computed(() => status.visibility === 'direct')
<StatusReplyingTo :collapsed="true" :status="status" :class="faded ? 'text-secondary-light' : ''" /> <StatusReplyingTo :collapsed="true" :status="status" :class="faded ? 'text-secondary-light' : ''" />
</div> </div>
<div flex-auto /> <div flex-auto />
<div v-if="!isZenMode" text-sm text-secondary flex="~ row nowrap" hover:underline> <div v-if="!userSettings.zenMode" text-sm text-secondary flex="~ row nowrap" hover:underline>
<AccountBotIndicator v-if="status.account.bot" me-2 /> <AccountBotIndicator v-if="status.account.bot" me-2 />
<div flex> <div flex>
<CommonTooltip :content="createdAt"> <CommonTooltip :content="createdAt">
@ -155,7 +155,7 @@ const isDM = $computed(() => status.visibility === 'direct')
</div> </div>
<StatusContent :status="status" :context="context" mb2 :class="{ 'mt-2 mb1': isDM }" /> <StatusContent :status="status" :context="context" mb2 :class="{ 'mt-2 mb1': isDM }" />
<div> <div>
<StatusActions v-if="(actions !== false && !isZenMode)" :status="status" /> <StatusActions v-if="(actions !== false && !userSettings.zenMode)" :status="status" />
</div> </div>
</div> </div>
</div> </div>

View file

@ -21,7 +21,7 @@ const isSquare = $computed(() => (
)) ))
const providerName = $computed(() => props.card.providerName ? props.card.providerName : new URL(props.card.url).hostname) const providerName = $computed(() => props.card.providerName ? props.card.providerName : new URL(props.card.url).hostname)
const gitHubCards = $(computedEager(() => useFeatureFlags().experimentalGitHubCards)) const gitHubCards = $(useFeatureFlag('experimentalGitHubCards'))
// TODO: handle card.type: 'photo' | 'video' | 'rich'; // TODO: handle card.type: 'photo' | 'video' | 'rich';
const cardTypeIconMap: Record<CardType, string> = { const cardTypeIconMap: Record<CardType, string> = {

View file

@ -12,7 +12,7 @@ const { paginator, stream } = defineProps<{
}>() }>()
const { formatNumber } = useHumanReadableNumber() const { formatNumber } = useHumanReadableNumber()
const virtualScroller = $(computedEager(() => useFeatureFlags().experimentalVirtualScroll)) const virtualScroller = $(useFeatureFlag('experimentalVirtualScroll'))
</script> </script>
<template> <template>

View file

@ -271,10 +271,10 @@ export const provideGlobalCommands = () => {
scope: 'Preferences', scope: 'Preferences',
name: () => t('command.toggle_zen_mode'), name: () => t('command.toggle_zen_mode'),
icon: () => isZenMode.value ? 'i-ri:layout-right-2-line' : 'i-ri:layout-right-line', icon: () => userSettings.value.zenMode ? 'i-ri:layout-right-2-line' : 'i-ri:layout-right-line',
onActivate() { onActivate() {
toggleZenMode() userSettings.value.zenMode = !userSettings.value.zenMode
}, },
}) })

View file

@ -1,6 +1,6 @@
import type { Attachment, Status, StatusEdit } from 'masto' import type { Attachment, Status, StatusEdit } from 'masto'
import type { Draft } from '~/types' import type { Draft } from '~/types'
import { STORAGE_KEY_FIRST_VISIT, STORAGE_KEY_ZEN_MODE } from '~/constants' import { STORAGE_KEY_FIRST_VISIT } from '~/constants'
export const mediaPreviewList = ref<Attachment[]>([]) export const mediaPreviewList = ref<Attachment[]>([])
export const mediaPreviewIndex = ref(0) export const mediaPreviewIndex = ref(0)
@ -11,7 +11,6 @@ export const dialogDraftKey = ref<string>()
export const commandPanelInput = ref('') export const commandPanelInput = ref('')
export const isFirstVisit = useLocalStorage(STORAGE_KEY_FIRST_VISIT, !process.mock) export const isFirstVisit = useLocalStorage(STORAGE_KEY_FIRST_VISIT, !process.mock)
export const isZenMode = useLocalStorage(STORAGE_KEY_ZEN_MODE, false)
export const isSigninDialogOpen = ref(false) export const isSigninDialogOpen = ref(false)
export const isPublishDialogOpen = ref(false) export const isPublishDialogOpen = ref(false)
@ -22,8 +21,6 @@ export const isCommandPanelOpen = ref(false)
export const lastPublishDialogStatus = ref<Status | null>(null) export const lastPublishDialogStatus = ref<Status | null>(null)
export const toggleZenMode = useToggle(isZenMode)
export function openSigninDialog() { export function openSigninDialog() {
isSigninDialogOpen.value = true isSigninDialogOpen.value = true
} }

View file

@ -1,38 +0,0 @@
import { STORAGE_KEY_FEATURE_FLAGS } from '~/constants'
export interface FeatureFlags {
experimentalVirtualScroll: boolean
experimentalGitHubCards: boolean
experimentalUserPicker: boolean
}
export type FeatureFlagsMap = Record<string, FeatureFlags>
export function getDefaultFeatureFlags(): FeatureFlags {
return {
experimentalVirtualScroll: false,
experimentalGitHubCards: true,
experimentalUserPicker: true,
}
}
export const currentUserFeatureFlags = process.server
? computed(getDefaultFeatureFlags)
: useUserLocalStorage(STORAGE_KEY_FEATURE_FLAGS, getDefaultFeatureFlags)
export function useFeatureFlags() {
const featureFlags = currentUserFeatureFlags.value
return featureFlags
}
export function toggleFeatureFlag(key: keyof FeatureFlags) {
const featureFlags = currentUserFeatureFlags.value
if (featureFlags[key])
featureFlags[key] = !featureFlags[key]
else
featureFlags[key] = true
}
const userPicker = eagerComputed(() => useFeatureFlags().experimentalUserPicker)
export const showUserPicker = computed(() => useUsers().value.length > 1 && userPicker.value)

View file

@ -0,0 +1,36 @@
import type { Ref } from 'vue'
import { userSettings } from '.'
export interface FeatureFlags {
experimentalVirtualScroll: boolean
experimentalGitHubCards: boolean
experimentalUserPicker: boolean
}
export type FeatureFlagsMap = Record<string, FeatureFlags>
const DEFAULT_FEATURE_FLAGS: FeatureFlags = {
experimentalVirtualScroll: false,
experimentalGitHubCards: true,
experimentalUserPicker: true,
}
export function useFeatureFlag<T extends keyof FeatureFlags>(name: T): Ref<FeatureFlags[T]> {
return computed({
get() {
return getFeatureFlag(name)
},
set(value) {
if (userSettings.value)
userSettings.value.featureFlags[name] = value
},
})
}
export function getFeatureFlag<T extends keyof FeatureFlags>(name: T): FeatureFlags[T] {
return userSettings.value?.featureFlags?.[name] ?? DEFAULT_FEATURE_FLAGS[name]
}
export function toggleFeatureFlag(key: keyof FeatureFlags) {
const flag = useFeatureFlag(key)
flag.value = !flag.value
}

View file

@ -0,0 +1,21 @@
import type { FeatureFlags } from './featureFlags'
import type { ColorMode, FontSize } from '~/types'
import { STORAGE_KEY_SETTINGS } from '~/constants'
export interface UserSettings {
featureFlags: Partial<FeatureFlags>
colorMode?: ColorMode
fontSize?: FontSize
lang?: string
zenMode?: boolean
}
export function getDefaultUserSettings(): UserSettings {
return {
featureFlags: {},
}
}
export const userSettings = process.server
? computed(getDefaultUserSettings)
: useUserLocalStorage(STORAGE_KEY_SETTINGS, getDefaultUserSettings)

View file

@ -10,9 +10,7 @@ export const STORAGE_KEY_SERVERS = 'elk-servers'
export const STORAGE_KEY_CURRENT_USER = 'elk-current-user' export const STORAGE_KEY_CURRENT_USER = 'elk-current-user'
export const STORAGE_KEY_NOTIFY_TAB = 'elk-notify-tab' 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_SETTINGS = 'elk-settings'
export const STORAGE_KEY_LANG = 'elk-lang'
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'
export const STORAGE_KEY_HIDE_EXPLORE_NEWS_TIPS = 'elk-hide-explore-news-tips' export const STORAGE_KEY_HIDE_EXPLORE_NEWS_TIPS = 'elk-hide-explore-news-tips'
@ -24,6 +22,6 @@ export const COOKIE_MAX_AGE = 10 * 365 * 24 * 60 * 60 * 1000
export const COOKIE_KEY_FONT_SIZE = 'elk-font-size' export const COOKIE_KEY_FONT_SIZE = 'elk-font-size'
export const COOKIE_KEY_COLOR_MODE = 'elk-color-mode' export const COOKIE_KEY_COLOR_MODE = 'elk-color-mode'
export const COOKIE_KEY_LOCALE = 'elk-locale' export const COOKIE_KEY_LOCALE = 'elk-lang'
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+)?$/

View file

@ -1,11 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useFeatureFlag } from '~~/composables/settings/featureFlags'
const route = useRoute() const route = useRoute()
const wideLayout = computed(() => route.meta.wideLayout ?? false) const wideLayout = computed(() => route.meta.wideLayout ?? false)
const showUserPicker = logicAnd(
useFeatureFlag('experimentalUserPicker'),
() => useUsers().value.length > 1,
)
</script> </script>
<template> <template>
<div h-full :class="{ zen: isZenMode }"> <div h-full :class="{ zen: userSettings.zenMode }">
<main flex w-full mxa lg:max-w-80rem> <main flex w-full mxa lg:max-w-80rem>
<aside class="hidden sm:flex w-1/8 md:w-1/6 lg:w-1/5 xl:w-1/4 justify-end xl:me-4 zen-hide" relative> <aside class="hidden sm:flex w-1/8 md:w-1/6 lg:w-1/5 xl:w-1/4 justify-end xl:me-4 zen-hide" relative>
<div sticky top-0 w-20 xl:w-100 h-screen flex="~ col" lt-xl-items-center> <div sticky top-0 w-20 xl:w-100 h-screen flex="~ col" lt-xl-items-center>

View file

@ -50,6 +50,7 @@ export default defineNuxtConfig({
dirs: [ dirs: [
'./composables/push-notifications', './composables/push-notifications',
'./composables/tiptap', './composables/tiptap',
'./composables/settings',
], ],
}, },
vite: { vite: {

View file

@ -87,6 +87,7 @@
"@unocss/nuxt": "^0.48.0", "@unocss/nuxt": "^0.48.0",
"@vitejs/plugin-vue": "^3.2.0", "@vitejs/plugin-vue": "^3.2.0",
"@vue-macros/nuxt": "^0.2.8", "@vue-macros/nuxt": "^0.2.8",
"@vueuse/math": "^9.10.0",
"@vueuse/nuxt": "^9.9.0", "@vueuse/nuxt": "^9.9.0",
"bumpp": "^8.2.1", "bumpp": "^8.2.1",
"emoji-mart": "^5.4.0", "emoji-mart": "^5.4.0",

View file

@ -18,19 +18,19 @@ useHeadFixed({
{{ $t('settings.feature_flags.title') }} {{ $t('settings.feature_flags.title') }}
</h3> </h3>
<SettingsToggleItem <SettingsToggleItem
:checked="currentUserFeatureFlags.experimentalVirtualScroll" :checked="getFeatureFlag('experimentalVirtualScroll')"
@click="toggleFeatureFlag('experimentalVirtualScroll')" @click="toggleFeatureFlag('experimentalVirtualScroll')"
> >
{{ $t('settings.feature_flags.virtual_scroll') }} {{ $t('settings.feature_flags.virtual_scroll') }}
</SettingsToggleItem> </SettingsToggleItem>
<SettingsToggleItem <SettingsToggleItem
:checked="currentUserFeatureFlags.experimentalGitHubCards" :checked="getFeatureFlag('experimentalGitHubCards')"
@click="toggleFeatureFlag('experimentalGitHubCards')" @click="toggleFeatureFlag('experimentalGitHubCards')"
> >
{{ $t('settings.feature_flags.github_cards') }} {{ $t('settings.feature_flags.github_cards') }}
</SettingsToggleItem> </SettingsToggleItem>
<SettingsToggleItem <SettingsToggleItem
:checked="currentUserFeatureFlags.experimentalUserPicker" :checked="getFeatureFlag('experimentalUserPicker')"
@click="toggleFeatureFlag('experimentalUserPicker')" @click="toggleFeatureFlag('experimentalUserPicker')"
> >
{{ $t('settings.feature_flags.user_picker') }} {{ $t('settings.feature_flags.user_picker') }}

View file

@ -1,9 +1,9 @@
import { COOKIE_MAX_AGE, STORAGE_KEY_LANG } from '~/constants' import { COOKIE_KEY_LOCALE, COOKIE_MAX_AGE } from '~/constants'
export default defineNuxtPlugin(async (nuxt) => { export default defineNuxtPlugin(async (nuxt) => {
const i18n = nuxt.vueApp.config.globalProperties.$i18n const i18n = nuxt.vueApp.config.globalProperties.$i18n
const { setLocale, locales } = nuxt.vueApp.config.globalProperties.$i18n const { setLocale, locales } = nuxt.vueApp.config.globalProperties.$i18n
const cookieLocale = useCookie(STORAGE_KEY_LANG, { maxAge: COOKIE_MAX_AGE }) const cookieLocale = useCookie(COOKIE_KEY_LOCALE, { maxAge: COOKIE_MAX_AGE })
const isFirstVisit = cookieLocale.value == null const isFirstVisit = cookieLocale.value == null
if (process.client && isFirstVisit) { if (process.client && isFirstVisit) {

View file

@ -37,6 +37,7 @@ specifiers:
'@vueuse/core': ^9.9.0 '@vueuse/core': ^9.9.0
'@vueuse/gesture': 2.0.0-beta.1 '@vueuse/gesture': 2.0.0-beta.1
'@vueuse/integrations': ^9.9.0 '@vueuse/integrations': ^9.9.0
'@vueuse/math': ^9.10.0
'@vueuse/motion': 2.0.0-beta.12 '@vueuse/motion': 2.0.0-beta.12
'@vueuse/nuxt': ^9.9.0 '@vueuse/nuxt': ^9.9.0
blurhash: ^2.0.4 blurhash: ^2.0.4
@ -147,6 +148,7 @@ devDependencies:
'@unocss/nuxt': 0.48.0 '@unocss/nuxt': 0.48.0
'@vitejs/plugin-vue': 3.2.0 '@vitejs/plugin-vue': 3.2.0
'@vue-macros/nuxt': 0.2.8_3nbxte3dhogb6b7pemmre2vo4m '@vue-macros/nuxt': 0.2.8_3nbxte3dhogb6b7pemmre2vo4m
'@vueuse/math': 9.10.0
'@vueuse/nuxt': 9.9.0_nuxt@3.0.0 '@vueuse/nuxt': 9.9.0_nuxt@3.0.0
bumpp: 8.2.1 bumpp: 8.2.1
emoji-mart: 5.4.0 emoji-mart: 5.4.0
@ -3658,6 +3660,16 @@ packages:
- vue - vue
dev: false dev: false
/@vueuse/math/9.10.0:
resolution: {integrity: sha512-DvCyQFhXToPgZzSs841TCylIVwuab5gNErjdFCNWX3vs+T3rC8wMnlaBCYTXb5g4ruqm6K/hBnHyalRnCj/VbQ==}
dependencies:
'@vueuse/shared': 9.10.0
vue-demi: 0.13.11
transitivePeerDependencies:
- '@vue/composition-api'
- vue
dev: true
/@vueuse/metadata/8.9.4: /@vueuse/metadata/8.9.4:
resolution: {integrity: sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==} resolution: {integrity: sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==}
dev: false dev: false
@ -3714,6 +3726,15 @@ packages:
vue-demi: 0.13.11 vue-demi: 0.13.11
dev: false dev: false
/@vueuse/shared/9.10.0:
resolution: {integrity: sha512-vakHJ2ZRklAzqmcVBL38RS7BxdBA4+5poG9NsSyqJxrt9kz0zX3P5CXMy0Hm6LFbZXUgvKdqAS3pUH1zX/5qTQ==}
dependencies:
vue-demi: 0.13.11
transitivePeerDependencies:
- '@vue/composition-api'
- vue
dev: true
/@vueuse/shared/9.9.0: /@vueuse/shared/9.9.0:
resolution: {integrity: sha512-+D0XFwHG0T+uaIbCSlROBwm1wzs71B7n3KyDOxnvfEMMHDOzl09rYKwaE2AENmYwYPXfHPbSBRDD2gBVHbvTcg==} resolution: {integrity: sha512-+D0XFwHG0T+uaIbCSlROBwm1wzs71B7n3KyDOxnvfEMMHDOzl09rYKwaE2AENmYwYPXfHPbSBRDD2gBVHbvTcg==}
dependencies: dependencies: