refactor: migrate to nuxt compatibilityVersion: 4 (#3298)
This commit is contained in:
parent
46e4433e1c
commit
a3fbc056a9
342 changed files with 1200 additions and 2932 deletions
140
app/components/settings/SettingsBottomNav.vue
Normal file
140
app/components/settings/SettingsBottomNav.vue
Normal 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>
|
49
app/components/settings/SettingsColorMode.vue
Normal file
49
app/components/settings/SettingsColorMode.vue
Normal 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>
|
87
app/components/settings/SettingsFontSize.vue
Normal file
87
app/components/settings/SettingsFontSize.vue
Normal 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>
|
90
app/components/settings/SettingsItem.vue
Normal file
90
app/components/settings/SettingsItem.vue
Normal 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>
|
16
app/components/settings/SettingsLanguage.vue
Normal file
16
app/components/settings/SettingsLanguage.vue
Normal 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>
|
79
app/components/settings/SettingsProfileMetadata.vue
Normal file
79
app/components/settings/SettingsProfileMetadata.vue
Normal 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>
|
31
app/components/settings/SettingsSponsorsList.vue
Normal file
31
app/components/settings/SettingsSponsorsList.vue
Normal file
File diff suppressed because one or more lines are too long
69
app/components/settings/SettingsThemeColors.vue
Normal file
69
app/components/settings/SettingsThemeColors.vue
Normal 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>
|
48
app/components/settings/SettingsToggleItem.vue
Normal file
48
app/components/settings/SettingsToggleItem.vue
Normal 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>
|
63
app/components/settings/SettingsTranslations.vue
Normal file
63
app/components/settings/SettingsTranslations.vue
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue