refactor: migrate to nuxt compatibilityVersion: 4 (#3298)

This commit is contained in:
Daniel Roe 2025-05-20 15:05:01 +01:00 committed by GitHub
parent 46e4433e1c
commit a3fbc056a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
342 changed files with 1200 additions and 2932 deletions

View file

@ -0,0 +1,140 @@
<script setup lang="ts">
import type { NavButtonName } from '~/composables/settings'
import { STORAGE_KEY_BOTTOM_NAV_BUTTONS } from '~/constants'
interface NavButton {
name: NavButtonName
label: string
icon: string
}
const availableNavButtons: NavButton[] = [
{ name: 'home', label: 'nav.home', icon: 'i-ri:home-5-line' },
{ name: 'search', label: 'nav.search', icon: 'i-ri:search-line' },
{ name: 'notification', label: 'nav.notifications', icon: 'i-ri:notification-4-line' },
{ name: 'mention', label: 'nav.conversations', icon: 'i-ri:at-line' },
{ name: 'favorite', label: 'nav.favourites', icon: 'i-ri:heart-line' },
{ name: 'bookmark', label: 'nav.bookmarks', icon: 'i-ri:bookmark-line' },
{ name: 'compose', label: 'nav.compose', icon: 'i-ri:quill-pen-line' },
{ name: 'explore', label: 'nav.explore', icon: 'i-ri:compass-3-line' },
{ name: 'local', label: 'nav.local', icon: 'i-ri:group-2-line' },
{ name: 'federated', label: 'nav.federated', icon: 'i-ri:earth-line' },
{ name: 'list', label: 'nav.lists', icon: 'i-ri:list-check' },
{ name: 'hashtag', label: 'nav.hashtags', icon: 'i-ri:hashtag' },
{ name: 'moreMenu', label: 'nav.more_menu', icon: 'i-ri:more-fill' },
] as const
const defaultSelectedNavButtonNames = computed<NavButtonName[]>(() =>
currentUser.value
? ['home', 'search', 'notification', 'mention', 'moreMenu']
: ['explore', 'local', 'federated', 'moreMenu'],
)
const navButtonNamesSetting = useLocalStorage<NavButtonName[]>(STORAGE_KEY_BOTTOM_NAV_BUTTONS, defaultSelectedNavButtonNames.value)
const selectedNavButtonNames = ref<NavButtonName[]>(navButtonNamesSetting.value)
const selectedNavButtons = computed<NavButton[]>(() =>
selectedNavButtonNames.value.map(name =>
availableNavButtons.find(navButton => navButton.name === name)!,
),
)
const canSave = computed(() =>
selectedNavButtonNames.value.length > 0
&& selectedNavButtonNames.value.includes('moreMenu')
&& JSON.stringify(selectedNavButtonNames.value) !== JSON.stringify(navButtonNamesSetting.value),
)
function isAdded(name: NavButtonName) {
return selectedNavButtonNames.value.includes(name)
}
function append(navButtonName: NavButtonName) {
const maxButtonNumber = 5
if (selectedNavButtonNames.value.length < maxButtonNumber)
selectedNavButtonNames.value = [...selectedNavButtonNames.value, navButtonName]
}
function remove(navButtonName: NavButtonName) {
selectedNavButtonNames.value = selectedNavButtonNames.value.filter(name => name !== navButtonName)
}
function clear() {
selectedNavButtonNames.value = []
}
function reset() {
selectedNavButtonNames.value = defaultSelectedNavButtonNames.value
}
function save() {
navButtonNamesSetting.value = selectedNavButtonNames.value
}
</script>
<template>
<section space-y-2>
<h2 id="interface-bn" font-medium>
{{ $t('settings.interface.bottom_nav') }}
</h2>
<form aria-labelledby="interface-bn" aria-describedby="interface-bn-desc" @submit.prevent="save">
<p id="interface-bn-desc" pb-2>
{{ $t('settings.interface.bottom_nav_instructions') }}
</p>
<!-- preview -->
<div aria-hidden="true" flex="~ gap4 wrap" items-center select-settings h-14>
<nav
v-for="availableNavButton in selectedNavButtons" :key="availableNavButton.name"
flex="~ 1" items-center justify-center text-xl
scrollbar-hide overscroll-none
>
<span :class="availableNavButton.icon" />
</nav>
</div>
<!-- button selection -->
<div flex="~ gap4 wrap" py4>
<button
v-for="{ name, label, icon } in availableNavButtons"
:key="name"
btn-text flex="~ gap-2" items-center p2 border="~ base rounded" bg-base ws-nowrap
:class="isAdded(name) ? 'text-secondary hover:text-second bg-auto' : ''"
type="button"
role="switch"
:aria-checked="isAdded(name)"
@click="isAdded(name) ? remove(name) : append(name)"
>
<span :class="icon" />
{{ label ? $t(label) : 'More menu' }}
</button>
</div>
<div flex="~ col" gap-y-4 gap-x-2 py-1 sm="~ justify-end flex-row">
<button
btn-outline font-bold py2 full-w sm-wa flex="~ gap2 center"
type="button"
:disabled="selectedNavButtonNames.length === 0"
:class="selectedNavButtonNames.length === 0 ? 'border-none' : undefined"
@click="clear"
>
<span aria-hidden="true" class="block i-ri:delete-bin-line" />
{{ $t('action.clear') }}
</button>
<button
btn-outline font-bold py2 full-w sm-wa flex="~ gap2 center"
type="reset"
@click="reset"
>
<span aria-hidden="true" class="block i-ri:repeat-line" />
{{ $t('action.reset') }}
</button>
<button
btn-solid font-bold py2 full-w sm-wa flex="~ gap2 center"
:disabled="!canSave"
>
<span aria-hidden="true" i-ri:save-2-fill />
{{ $t('action.save') }}
</button>
</div>
</form>
</section>
</template>

View file

@ -0,0 +1,49 @@
<script setup lang="ts">
import type { ColorMode } from '~/composables/settings'
const colorMode = useColorMode()
function setColorMode(mode: ColorMode) {
colorMode.preference = mode
}
const modes = [
{
icon: 'i-ri-moon-line',
label: 'settings.interface.dark_mode',
mode: 'dark',
},
{
icon: 'i-ri-sun-line',
label: 'settings.interface.light_mode',
mode: 'light',
},
{
icon: 'i-ri-computer-line',
label: 'settings.interface.system_mode',
mode: 'system',
},
] as const
</script>
<template>
<section space-y-2>
<h2 id="interface-cm" font-medium>
{{ $t('settings.interface.color_mode') }}
</h2>
<div flex="~ gap4 wrap" w-full role="group" aria-labelledby="interface-cm">
<button
v-for="{ icon, label, mode } in modes"
:key="mode"
type="button"
btn-text flex-1 flex="~ gap-1 center" p4 border="~ base rounded" bg-base ws-nowrap
:aria-pressed="colorMode.preference === mode ? 'true' : 'false'"
:class="colorMode.preference === mode ? 'pointer-events-none' : 'filter-saturate-0'"
@click="setColorMode(mode)"
>
<span :class="`${icon}`" />
{{ $t(label) }}
</button>
</div>
</section>
</template>

View file

@ -0,0 +1,87 @@
<script setup lang="ts">
import type { FontSize } from '~/composables/settings'
import { DEFAULT_FONT_SIZE } from '~/constants'
const userSettings = useUserSettings()
const sizes = (Array.from({ length: 11 })).fill(0).map((x, i) => `${10 + i}px`) as FontSize[]
function setFontSize(e: Event) {
if (e.target && 'valueAsNumber' in e.target)
userSettings.value.fontSize = sizes[e.target.valueAsNumber as number]
}
</script>
<template>
<section space-y-2>
<h2 id="interface-fs" font-medium>
{{ $t('settings.interface.font_size') }}
</h2>
<div flex items-center space-x-4 select-settings>
<span text-xs text-secondary>Aa</span>
<div flex-1 relative flex items-center>
<input
aria-labelledby="interface-fs"
:value="sizes.indexOf(userSettings.fontSize)"
:aria-valuetext="`${userSettings.fontSize}${userSettings.fontSize === DEFAULT_FONT_SIZE ? ` ${$t('settings.interface.default')}` : ''}`"
:min="0"
:max="sizes.length - 1"
:step="1"
type="range"
focus:outline-none
appearance-none bg-transparent
w-full cursor-pointer
@change="setFontSize"
>
<div flex items-center justify-between absolute w-full pointer-events-none>
<div
v-for="i in sizes.length" :key="i"
class="container-marker"
h-3 w-3
rounded-full bg-secondary-light
relative
>
<div
v-if="(sizes.indexOf(userSettings.fontSize)) === i - 1"
absolute rounded-full class="-top-1 -left-1"
bg-primary h-5 w-5
/>
</div>
</div>
</div>
<span text-xl text-secondary>Aa</span>
</div>
</section>
</template>
<style>
input:focus + div .container-marker:has(> div)::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2rem;
height: 2rem;
border: 2px solid var(--c-primary);
border-radius: 50%;
}
input[type=range]::-webkit-slider-runnable-track {
--at-apply: bg-secondary-light rounded-full h1 op60;
}
input[type=range]:focus::-webkit-slider-runnable-track {
--at-apply: outline-2 outline-red;
}
input[type=range]::-webkit-slider-thumb {
--at-apply: w3 h3 bg-primary -mt-1 outline outline-3 outline-primary rounded-full cursor-pointer appearance-none;
}
input[type=range]::-moz-range-track {
--at-apply: bg-secondary-light rounded-full h1 op60;
}
input[type=range]:focus::-moz-range-track {
--at-apply: outline-2 outline-red;
}
input[type=range]::-moz-range-thumb {
--at-apply: w3 h3 bg-primary -mt-1 outline outline-3 outline-primary rounded-full cursor-pointer appearance-none border-none;
}
</style>

View file

@ -0,0 +1,90 @@
<script setup lang="ts">
const { text, description, icon, to, command, external, target } = defineProps<{
text?: string
content?: string
description?: string
icon?: string
to?: string | Record<string, string>
command?: boolean
disabled?: boolean
external?: true
large?: true
match?: boolean
target?: string
}>()
const router = useRouter()
const scrollOnClick = computed(() => to && !(target === '_blank' || external))
useCommand({
scope: 'Settings',
name: () => text
?? (to
? typeof to === 'string'
? to
: to.name
: ''
),
description: () => description,
icon: () => icon || '',
visible: () => command && to,
onActivate() {
router.push(to!)
},
})
</script>
<template>
<NuxtLink
:disabled="disabled"
:to="to"
:external="external"
:target="target"
exact-active-class="text-primary"
:class="disabled ? 'op25 pointer-events-none ' : match ? 'text-primary' : ''"
block w-full group focus:outline-none
:tabindex="disabled ? -1 : null"
@click="scrollOnClick ? $scrollToTop() : undefined"
>
<div
w-full flex px5 py3 md:gap2 gap4 items-center
transition-250 group-hover:bg-active
group-focus-visible:ring="2 current"
>
<div flex-1 flex items-center md:gap2 gap4>
<div
v-if="$slots.icon || icon"
flex items-center justify-center flex-shrink-0
:class="$slots.description ? 'w-12 h-12' : ''"
>
<slot name="icon">
<div
v-if="icon"
:class="[icon, large ? 'text-xl mr-1' : 'text-xl md:text-size-inherit']"
/>
</slot>
</div>
<div flex="~ col gap-0.5">
<p>
<slot>
<span>{{ text }}</span>
</slot>
</p>
<p v-if="$slots.description || description" text-sm text-secondary>
<slot name="description">
{{ description }}
</slot>
</p>
</div>
</div>
<p v-if="$slots.content || content" text-sm text-secondary>
<slot name="content">
{{ content }}
</slot>
</p>
<div v-if="to" :class="!external ? 'i-ri:arrow-right-s-line' : 'i-ri:external-link-line'" text-xl text-secondary-light class="rtl-flip" />
</div>
</NuxtLink>
</template>

View file

@ -0,0 +1,16 @@
<script setup lang="ts">
import type { LocaleObject } from '@nuxtjs/i18n'
import type { ComputedRef } from 'vue'
const userSettings = useUserSettings()
const { locales } = useI18n() as { locales: ComputedRef<LocaleObject[]> }
</script>
<template>
<select v-model="userSettings.language">
<option v-for="item in locales" :key="item.code" :value="item.code" :selected="userSettings.language === item.code">
{{ item.name }}
</option>
</select>
</template>

View file

@ -0,0 +1,79 @@
<script setup lang="ts">
import type { mastodon } from 'masto'
const form = defineModel<{
fieldsAttributes: NonNullable<mastodon.rest.v1.UpdateCredentialsParams['fieldsAttributes']>
}>({ required: true })
const dropdown = ref<any>()
const fieldIcons = computed(() =>
Array.from({ length: maxAccountFieldCount.value }, (_, i) =>
getAccountFieldIcon(form.value.fieldsAttributes[i].name)),
)
const fieldCount = computed(() => {
// find last non-empty field
const idx = [...form.value.fieldsAttributes].reverse().findIndex(f => f.name || f.value)
if (idx === -1)
return 1
return Math.min(
form.value.fieldsAttributes.length - idx + 1,
maxAccountFieldCount.value,
)
})
function chooseIcon(i: number, text: string) {
form.value.fieldsAttributes[i].name = text
dropdown.value[i]?.hide()
}
</script>
<template>
<div space-y-2>
<div font-medium>
{{ $t('settings.profile.appearance.profile_metadata') }}
</div>
<div text-sm text-secondary>
{{ $t('settings.profile.appearance.profile_metadata_desc', [maxAccountFieldCount]) }}
</div>
<div flex="~ col gap4">
<div v-for="i in fieldCount" :key="i" flex="~ gap3" items-center>
<CommonDropdown ref="dropdown" placement="left">
<CommonTooltip :content="$t('tooltip.pick_an_icon')">
<button type="button" btn-action-icon>
<div :class="fieldIcons[i - 1] || 'i-ri:question-mark'" />
</button>
</CommonTooltip>
<template #popper>
<div flex="~ wrap gap-1" max-w-60 m2 me1>
<CommonTooltip
v-for="(icon, text) in accountFieldIcons"
:key="icon"
:content="text"
>
<template v-if="text !== 'Joined'">
<button type="button" btn-action-icon @click="chooseIcon(i - 1, text)">
<div text-xl :class="icon" />
</button>
</template>
</CommonTooltip>
</div>
</template>
</CommonDropdown>
<input
v-model="form.fieldsAttributes[i - 1].name"
type="text" placeholder-text-secondary
:placeholder="$t('settings.profile.appearance.profile_metadata_label')"
input-base
>
<input
v-model="form.fieldsAttributes[i - 1].value"
type="text" placeholder-text-secondary
:placeholder="$t('settings.profile.appearance.profile_metadata_value')"
input-base
>
</div>
</div>
</div>
</template>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,69 @@
<script setup lang="ts">
import type { ThemeColors } from '~/composables/settings'
import { THEME_COLORS } from '~/constants'
const themes = await import('~/constants/themes.json').then((r) => {
const map = new Map<'dark' | 'light', [string, ThemeColors][]>([['dark', []], ['light', []]])
const themes = r.default as [string, ThemeColors][]
for (const [key, theme] of themes) {
map.get('dark')!.push([key, theme])
map.get('light')!.push([key, {
...theme,
'--c-primary': `color-mix(in srgb, ${theme['--c-primary']}, black 25%)`,
}])
}
return map
})
const settings = useUserSettings()
const media = useMediaQuery('(prefers-color-scheme: dark)')
const colorMode = useColorMode()
const useThemes = shallowRef<[string, ThemeColors][]>([])
watch(() => colorMode.preference, (cm) => {
const dark = cm === 'dark' || (cm === 'system' && media.value)
const newThemes = dark ? themes.get('dark')! : themes.get('light')!
const key = settings.value.themeColors?.['--theme-color-name'] || THEME_COLORS.defaultTheme
for (const [k, theme] of newThemes) {
if (k === key) {
settings.value.themeColors = theme
break
}
}
useThemes.value = newThemes
}, { immediate: true, flush: 'post' })
const currentTheme = computed(() => settings.value.themeColors?.['--theme-color-name'] || THEME_COLORS.defaultTheme)
function updateTheme(theme: ThemeColors) {
settings.value.themeColors = theme
}
</script>
<template>
<section space-y-2>
<h2 id="interface-tc" font-medium>
{{ $t('settings.interface.theme_color') }}
</h2>
<div flex="~ gap4 wrap" p2 role="group" aria-labelledby="interface-tc">
<button
v-for="[key, theme] in useThemes" :key="key"
:style="{
'--rgb-primary': theme['--rgb-primary'],
'background': theme['--c-primary'],
'--local-ring-color': theme['--c-primary'],
}"
type="button"
:class="currentTheme === theme['--theme-color-name'] ? 'ring-2' : 'scale-90'"
:aria-pressed="currentTheme === theme['--theme-color-name'] ? 'true' : 'false'"
:title="theme['--theme-color-name']"
w-8 h-8 rounded-full transition-all
ring="$local-ring-color offset-3 offset-$c-bg-base"
@click="updateTheme(theme)"
/>
</div>
</section>
</template>

View file

@ -0,0 +1,48 @@
<script setup lang="ts">
const { disabled = false } = defineProps<{
icon?: string
text?: string
checked: boolean
disabled?: boolean
}>()
</script>
<template>
<button
exact-active-class="text-primary"
block w-full group focus:outline-none text-start
role="checkbox" :aria-checked="checked"
:disabled="disabled"
:class="disabled ? 'opacity-50 cursor-not-allowed' : ''"
>
<span
w-full flex px5 py3 md:gap2 gap4 items-center
transition-250
:class="disabled ? '' : 'group-hover:bg-active'"
group-focus-visible:ring="2 current"
>
<span flex-1 flex items-center md:gap2 gap4>
<span
v-if="icon" flex items-center justify-center
flex-shrink-0
:class="$slots.description ? 'w-12 h-12' : ''"
>
<slot name="icon">
<span v-if="icon" :class="icon" md:text-size-inherit text-xl />
</slot>
</span>
<span space-y-1>
<span :class="checked ? 'text-base' : 'text-secondary'">
<slot>
<span>{{ text }}</span>
</slot>
</span>
<span v-if="$slots.description" block text-sm text-secondary>
<slot name="description" />
</span>
</span>
</span>
<span text-lg :class="checked ? 'i-ri-checkbox-line text-primary' : 'i-ri-checkbox-blank-line text-secondary'" />
</span>
</button>
</template>

View file

@ -0,0 +1,63 @@
<script setup lang="ts">
import ISO6391 from 'iso-639-1'
const supportedTranslationLanguages = ISO6391.getLanguages([...supportedTranslationCodes])
const userSettings = useUserSettings()
const language = ref<string | null>(null)
const availableOptions = computed(() => {
return Object.values(supportedTranslationLanguages).filter((value) => {
return !userSettings.value.disabledTranslationLanguages.includes(value.code)
})
})
function addDisabledTranslation() {
if (language.value) {
const uniqueValues = new Set(userSettings.value.disabledTranslationLanguages)
uniqueValues.add(language.value)
userSettings.value.disabledTranslationLanguages = [...uniqueValues]
language.value = null
}
}
function removeDisabledTranslation(code: string) {
const uniqueValues = new Set(userSettings.value.disabledTranslationLanguages)
uniqueValues.delete(code)
userSettings.value.disabledTranslationLanguages = [...uniqueValues]
}
</script>
<template>
<div>
<CommonCheckbox v-model="userSettings.preferences.hideTranslation" :label="$t('settings.preferences.hide_translation')" />
<div v-if="!userSettings.preferences.hideTranslation" class="mt-1 ms-2">
<p class=" mb-2">
{{ $t('settings.language.translations.hide_specific') }}
</p>
<div class="ms-4">
<ul>
<li v-for="langCode in userSettings.disabledTranslationLanguages" :key="langCode" class="flex items-center">
<div>{{ ISO6391.getNativeName(langCode) }}</div>
<button class="btn-text" type="button" :title="$t('settings.language.translations.remove')" @click.prevent="removeDisabledTranslation(langCode)">
<span class="block i-ri:close-line" aria-hidden="true" />
</button>
</li>
</ul>
<div class="flex items-center mt-2">
<select v-model="language" class="select-settings">
<option disabled selected :value="null">
{{ $t('settings.language.translations.choose_language') }}
</option>
<option v-for="availableOption in availableOptions" :key="availableOption.code" :value="availableOption.code">
{{ availableOption.nativeName }}
</option>
</select>
<button class="btn-text shrink-0" @click="addDisabledTranslation">
{{ $t('settings.language.translations.add') }}
</button>
</div>
</div>
</div>
</div>
</template>