feat: Wire up new preferences and remove old settings
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s

This commit is contained in:
Jesse Wierzbinski 2025-04-30 18:03:14 +02:00
parent 412e49dfe2
commit 3ce71dd4df
No known key found for this signature in database
32 changed files with 213 additions and 340 deletions

20
app.vue
View file

@ -1,7 +1,7 @@
<template> <template>
<TooltipProvider> <TooltipProvider>
<Component is="style"> <Component is="style">
{{ customCss.value }} {{ preferences.custom_css }}
</Component> </Component>
<NuxtPwaAssets /> <NuxtPwaAssets />
<NuxtLayout> <NuxtLayout>
@ -19,7 +19,6 @@ import ConfirmationModal from "./components/modals/confirm.vue";
import { Toaster } from "./components/ui/sonner"; import { Toaster } from "./components/ui/sonner";
import { TooltipProvider } from "./components/ui/tooltip"; import { TooltipProvider } from "./components/ui/tooltip";
import { overwriteGetLocale } from "./paraglide/runtime"; import { overwriteGetLocale } from "./paraglide/runtime";
import { type EnumSetting, SettingIds } from "./settings";
// Sin // Sin
//import "~/styles/mcdonalds.css"; //import "~/styles/mcdonalds.css";
@ -31,17 +30,16 @@ const origin = useRequestURL().searchParams.get("origin");
const appData = useAppData(); const appData = useAppData();
const instance = useInstance(); const instance = useInstance();
const description = useExtendedDescription(client); const description = useExtendedDescription(client);
const customCss = useSetting(SettingIds.CustomCSS);
const route = useRoute(); const route = useRoute();
// Theme switcher // Theme switcher
const theme = useSetting(SettingIds.Theme) as Ref<EnumSetting>;
const colorMode = useColorMode(); const colorMode = useColorMode();
const radius = useCssVar("--radius");
watch(theme.value, () => { watch(preferences.color_theme, (newVal) => {
// Add theme-changing class to html to trigger transition // Add theme-changing class to html to trigger transition
document.documentElement.classList.add("theme-changing"); document.documentElement.classList.add("theme-changing");
colorMode.preference = theme.value.value; colorMode.preference = newVal;
setTimeout(() => { setTimeout(() => {
// Remove theme-changing class after transition // Remove theme-changing class after transition
@ -49,6 +47,16 @@ watch(theme.value, () => {
}, 1000); }, 1000);
}); });
watch(
preferences.border_radius,
(newVal) => {
radius.value = `${newVal}rem`;
},
{
immediate: true,
},
);
useSeoMeta({ useSeoMeta({
titleTemplate: (titleChunk) => { titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} · Versia` : "Versia"; return titleChunk ? `${titleChunk} · Versia` : "Versia";

View file

@ -21,7 +21,8 @@
"noUnusedVariables": "off", "noUnusedVariables": "off",
"noUnusedImports": "off", "noUnusedImports": "off",
"noUndeclaredDependencies": "off", "noUndeclaredDependencies": "off",
"useImportExtensions": "off" "useImportExtensions": "off",
"useJsxKeyInIterable": "off"
}, },
"complexity": { "complexity": {
"noExcessiveCognitiveComplexity": "off" "noExcessiveCognitiveComplexity": "off"

View file

@ -159,7 +159,6 @@ import {
SelectTrigger, SelectTrigger,
} from "~/components/ui/select"; } from "~/components/ui/select";
import * as m from "~/paraglide/messages.js"; import * as m from "~/paraglide/messages.js";
import { SettingIds } from "~/settings";
import EditorContent from "../editor/content.vue"; import EditorContent from "../editor/content.vue";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { DialogFooter } from "../ui/dialog"; import { DialogFooter } from "../ui/dialog";
@ -169,13 +168,11 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import Files from "./files.vue"; import Files from "./files.vue";
const { Control_Enter, Command_Enter } = useMagicKeys(); const { Control_Enter, Command_Enter } = useMagicKeys();
const ctrlEnterSend = useSetting(SettingIds.CtrlEnterToSend);
const defaultVisibility = useSetting(SettingIds.DefaultVisibility);
const { play } = useAudio(); const { play } = useAudio();
const fileInput = ref<HTMLInputElement | null>(null); const fileInput = ref<HTMLInputElement | null>(null);
watch([Control_Enter, Command_Enter], () => { watch([Control_Enter, Command_Enter], () => {
if (sending.value || !ctrlEnterSend.value.value) { if (sending.value || !preferences.ctrl_enter_send.value) {
return; return;
} }
@ -220,9 +217,10 @@ const state = reactive({
sensitive: relation?.type === "edit" ? relation.note.sensitive : false, sensitive: relation?.type === "edit" ? relation.note.sensitive : false,
contentWarning: relation?.type === "edit" ? relation.note.spoiler_text : "", contentWarning: relation?.type === "edit" ? relation.note.spoiler_text : "",
contentType: "text/html" as "text/html" | "text/plain", contentType: "text/html" as "text/html" | "text/plain",
visibility: (relation?.type === "edit" visibility:
? relation.note.visibility relation?.type === "edit"
: (defaultVisibility.value.value ?? "public")) as Status["visibility"], ? relation.note.visibility
: preferences.default_visibility.value,
files: (relation?.type === "edit" files: (relation?.type === "edit"
? relation.note.media_attachments.map((a) => ({ ? relation.note.media_attachments.map((a) => ({
apiId: a.id, apiId: a.id,

View file

@ -19,9 +19,9 @@ defineProps<{
modalOptions: ConfirmModalOptions; modalOptions: ConfirmModalOptions;
}>(); }>();
defineEmits<{ const emit = defineEmits<{
confirm: (result: ConfirmModalResult) => void; confirm: [result: ConfirmModalResult];
cancel: () => void; cancel: [];
}>(); }>();
const inputValue = ref<string>(""); const inputValue = ref<string>("");
@ -55,10 +55,10 @@ const inputValue = ref<string>("");
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" @click="() => $emit('cancel')"> <Button variant="outline" @click="() => emit('cancel')">
{{ modalOptions.cancelText }} {{ modalOptions.cancelText }}
</Button> </Button>
<Button @click="() => $emit('confirm', { <Button @click="() => emit('confirm', {
confirmed: true, confirmed: true,
value: inputValue, value: inputValue,
})"> })">
@ -67,4 +67,4 @@ const inputValue = ref<string>("");
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</template> </template>

View file

@ -21,7 +21,6 @@ import { Ellipsis, Heart, Quote, Repeat, Reply } from "lucide-vue-next";
import { toast } from "vue-sonner"; import { toast } from "vue-sonner";
import * as m from "~/paraglide/messages.js"; import * as m from "~/paraglide/messages.js";
import { getLocale } from "~/paraglide/runtime"; import { getLocale } from "~/paraglide/runtime";
import { SettingIds } from "~/settings";
import { confirmModalService } from "../modals/composable"; import { confirmModalService } from "../modals/composable";
import ActionButton from "./action-button.vue"; import ActionButton from "./action-button.vue";
import Menu from "./menu.vue"; import Menu from "./menu.vue";
@ -48,11 +47,8 @@ const emit = defineEmits<{
}>(); }>();
const { play } = useAudio(); const { play } = useAudio();
const confirmLikes = useSetting(SettingIds.ConfirmLike);
const confirmReblogs = useSetting(SettingIds.ConfirmReblog);
const like = async () => { const like = async () => {
if (confirmLikes.value.value) { if (preferences.confirm_actions.value.includes("like")) {
const confirmation = await confirmModalService.confirm({ const confirmation = await confirmModalService.confirm({
title: m.slimy_least_ray_aid(), title: m.slimy_least_ray_aid(),
message: m.stale_new_ray_jolt(), message: m.stale_new_ray_jolt(),
@ -74,7 +70,7 @@ const like = async () => {
}; };
const unlike = async () => { const unlike = async () => {
if (confirmLikes.value.value) { if (preferences.confirm_actions.value.includes("like")) {
const confirmation = await confirmModalService.confirm({ const confirmation = await confirmModalService.confirm({
title: m.odd_strong_halibut_prosper(), title: m.odd_strong_halibut_prosper(),
message: m.slow_blue_parrot_savor(), message: m.slow_blue_parrot_savor(),
@ -95,7 +91,7 @@ const unlike = async () => {
}; };
const reblog = async () => { const reblog = async () => {
if (confirmReblogs.value.value) { if (preferences.confirm_actions.value.includes("reblog")) {
const confirmation = await confirmModalService.confirm({ const confirmation = await confirmModalService.confirm({
title: m.best_mellow_llama_surge(), title: m.best_mellow_llama_surge(),
message: m.salty_plain_mallard_gaze(), message: m.salty_plain_mallard_gaze(),
@ -116,7 +112,7 @@ const reblog = async () => {
}; };
const unreblog = async () => { const unreblog = async () => {
if (confirmReblogs.value.value) { if (preferences.confirm_actions.value.includes("reblog")) {
const confirmation = await confirmModalService.confirm({ const confirmation = await confirmModalService.confirm({
title: m.main_fancy_octopus_loop(), title: m.main_fancy_octopus_loop(),
message: m.odd_alive_swan_express(), message: m.odd_alive_swan_express(),

View file

@ -1,11 +1,11 @@
<template> <template>
<ContentWarning v-if="(sensitive || contentWarning) && showCw.value" :content-warning="contentWarning" v-model="blurred" /> <ContentWarning v-if="(sensitive || contentWarning) && preferences.show_content_warning" :content-warning="contentWarning" v-model="blurred" />
<OverflowGuard :character-count="characterCount" :class="(blurred && showCw.value) && 'blur-md'"> <OverflowGuard :character-count="characterCount" :class="(blurred && preferences.show_content_warning) && 'blur-md'">
<Prose v-html="content" v-render-emojis="emojis"></Prose> <Prose v-html="content" v-render-emojis="emojis"></Prose>
</OverflowGuard> </OverflowGuard>
<Attachments v-if="attachments.length > 0" :attachments="attachments" :class="(blurred && showCw.value) && 'blur-xl'" /> <Attachments v-if="attachments.length > 0" :attachments="attachments" :class="(blurred && preferences.show_content_warning) && 'blur-xl'" />
<div v-if="quote" class="mt-4 rounded border overflow-hidden"> <div v-if="quote" class="mt-4 rounded border overflow-hidden">
<Note :note="quote" :hide-actions="true" :small-layout="true" /> <Note :note="quote" :hide-actions="true" :small-layout="true" />
@ -14,7 +14,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Attachment, Emoji, Status } from "@versia/client/types"; import type { Attachment, Emoji, Status } from "@versia/client/types";
import { type BooleanSetting, SettingIds } from "~/settings";
import Attachments from "./attachments.vue"; import Attachments from "./attachments.vue";
import ContentWarning from "./content-warning.vue"; import ContentWarning from "./content-warning.vue";
import Note from "./note.vue"; import Note from "./note.vue";
@ -32,7 +31,6 @@ const { content, plainContent, sensitive, contentWarning } = defineProps<{
}>(); }>();
const blurred = ref(sensitive || !!contentWarning); const blurred = ref(sensitive || !!contentWarning);
const showCw = useSetting(SettingIds.ShowContentWarning) as Ref<BooleanSetting>;
const characterCount = plainContent?.length; const characterCount = plainContent?.length;
</script> </script>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="rounded flex flex-row items-center gap-3"> <div class="rounded flex flex-row items-center gap-3">
<HoverCard v-model:open="popupOpen" @update:open="() => { <HoverCard v-model:open="popupOpen" @update:open="() => {
if (!enableHoverCard.value) { if (!preferences.popup_avatar_hover) {
popupOpen = false; popupOpen = false;
} }
}" :open-delay="2000"> }" :open-delay="2000">
@ -51,7 +51,6 @@ import type {
} from "@vueuse/core"; } from "@vueuse/core";
import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next"; import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next";
import { getLocale } from "~/paraglide/runtime"; import { getLocale } from "~/paraglide/runtime";
import { SettingIds } from "~/settings";
import Avatar from "../profiles/avatar.vue"; import Avatar from "../profiles/avatar.vue";
import SmallCard from "../profiles/small-card.vue"; import SmallCard from "../profiles/small-card.vue";
import { import {
@ -59,7 +58,6 @@ import {
HoverCardContent, HoverCardContent,
HoverCardTrigger, HoverCardTrigger,
} from "../ui/hover-card"; } from "../ui/hover-card";
import CopyableText from "./copyable-text.vue";
const { createdAt, noteUrl, author, authorUrl } = defineProps<{ const { createdAt, noteUrl, author, authorUrl } = defineProps<{
cornerAvatar?: string; cornerAvatar?: string;
@ -94,7 +92,6 @@ const fullTime = new Intl.DateTimeFormat(getLocale(), {
dateStyle: "medium", dateStyle: "medium",
timeStyle: "short", timeStyle: "short",
}).format(createdAt); }).format(createdAt);
const enableHoverCard = useSetting(SettingIds.PopupAvatarHover);
const popupOpen = ref(false); const popupOpen = ref(false);
const visibilities = { const visibilities = {

View file

@ -21,7 +21,6 @@ import {
import { toast } from "vue-sonner"; import { toast } from "vue-sonner";
import { confirmModalService } from "~/components/modals/composable.ts"; import { confirmModalService } from "~/components/modals/composable.ts";
import * as m from "~/paraglide/messages.js"; import * as m from "~/paraglide/messages.js";
import { SettingIds } from "~/settings";
const { authorId, noteId } = defineProps<{ const { authorId, noteId } = defineProps<{
apiNoteString: string; apiNoteString: string;
@ -41,8 +40,6 @@ const { copy } = useClipboard();
const loggedIn = !!identity.value; const loggedIn = !!identity.value;
const authorIsMe = loggedIn && authorId === identity.value?.account.id; const authorIsMe = loggedIn && authorId === identity.value?.account.id;
const confirmDeletes = useSetting(SettingIds.ConfirmDelete);
const copyText = (text: string) => { const copyText = (text: string) => {
copy(text); copy(text);
toast.success(m.flat_nice_worm_dream()); toast.success(m.flat_nice_worm_dream());
@ -57,7 +54,7 @@ const blockUser = async (userId: string) => {
}; };
const _delete = async () => { const _delete = async () => {
if (confirmDeletes.value.value) { if (preferences.confirm_actions.value.includes("delete")) {
const confirmation = await confirmModalService.confirm({ const confirmation = await confirmModalService.confirm({
title: m.calm_icy_weasel_twirl(), title: m.calm_icy_weasel_twirl(),
message: m.gray_fun_toucan_slide(), message: m.gray_fun_toucan_slide(),

View file

@ -5,13 +5,13 @@
</CardTitle> </CardTitle>
<Card class="p-0 gap-0"> <Card class="p-0 gap-0">
<div v-for="preference of preferences" :key="preference"> <div v-for="preference of preferences" :key="preference">
<TextPreferenceVue v-if="(prefs[preference] instanceof TextPreference)" :pref="(prefs[preference] as TextPreference)" /> <TextPreferenceVue v-if="(prefs[preference] instanceof TextPreference)" :pref="(prefs[preference] as TextPreference)" :name="preference" />
<BooleanPreferenceVue v-else-if="(prefs[preference] instanceof BooleanPreference)" :pref="(prefs[preference] as BooleanPreference)" /> <BooleanPreferenceVue v-else-if="(prefs[preference] instanceof BooleanPreference)" :pref="(prefs[preference] as BooleanPreference)" :name="preference" />
<SelectPreferenceVue v-else-if="(prefs[preference] instanceof SelectPreference)" :pref="(prefs[preference] as SelectPreference<string>)" /> <SelectPreferenceVue v-else-if="(prefs[preference] instanceof SelectPreference)" :pref="(prefs[preference] as SelectPreference<string>)" :name="preference" />
<NumberPreferenceVue v-else-if="(prefs[preference] instanceof NumberPreference)" :pref="(prefs[preference] as NumberPreference)" /> <NumberPreferenceVue v-else-if="(prefs[preference] instanceof NumberPreference)" :pref="(prefs[preference] as NumberPreference)" :name="preference" />
<MultiSelectPreferenceVue v-else-if="(prefs[preference] instanceof MultiSelectPreference)" :pref="(prefs[preference] as MultiSelectPreference<string>)" /> <MultiSelectPreferenceVue v-else-if="(prefs[preference] instanceof MultiSelectPreference)" :pref="(prefs[preference] as MultiSelectPreference<string>)" :name="preference" />
<CodePreferenceVue v-else-if="(prefs[preference] instanceof CodePreference)" :pref="(prefs[preference] as CodePreference)" /> <CodePreferenceVue v-else-if="(prefs[preference] instanceof CodePreference)" :pref="(prefs[preference] as CodePreference)" :name="preference" />
<UrlPreferenceVue v-else-if="(prefs[preference] instanceof UrlPreference)" :pref="(prefs[preference] as UrlPreference)" /> <UrlPreferenceVue v-else-if="(prefs[preference] instanceof UrlPreference)" :pref="(prefs[preference] as UrlPreference)" :name="preference" />
</div> </div>
</Card> </Card>
</section> </section>

View file

@ -0,0 +1,60 @@
<template>
<Card class="grid gap-3 text-sm p-4">
<dl class="grid gap-3">
<div v-for="[key, value] of data" :key="key" class="flex flex-row items-baseline justify-between gap-4 truncate">
<dt class="text-muted-foreground">
{{ key }}
</dt>
<dd class="font-mono" v-if="typeof value === 'string'">{{ value }}</dd>
<dd class="font-mono" v-else>
<component :is="value" />
</dd>
</div>
</dl>
</Card>
</template>
<script lang="tsx" setup>
import type { VNode } from "vue";
import { toast } from "vue-sonner";
import { Button } from "../ui/button";
import { Card } from "../ui/card";
const copy = (data: string) => {
navigator.clipboard.writeText(data);
toast.success("Copied to clipboard");
};
const appData = useAppData();
const data: [string, string | VNode][] = [
["User ID", identity.value?.account.id ?? ""],
["Instance domain", identity.value?.instance.domain ?? ""],
["Instance version", identity.value?.instance.versia_version ?? ""],
["Client ID", appData.value?.client_id ?? ""],
[
"Client secret",
<Button
variant="outline"
class="font-sans"
size="sm"
// @ts-expect-error missing onClick types
onClick={() => copy(appData.value?.client_secret ?? "")}
>
Click to copy
</Button>,
],
[
"Access token",
<Button
variant="outline"
class="font-sans"
size="sm"
// @ts-expect-error missing onClick types
onClick={() => copy(identity.value?.tokens.access_token ?? "")}
>
Click to copy
</Button>,
],
];
</script>

View file

@ -21,6 +21,7 @@ import TinyCard from "../profiles/tiny-card.vue";
import { Separator } from "../ui/separator"; import { Separator } from "../ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import Category from "./category.vue"; import Category from "./category.vue";
import Developer from "./developer.vue";
import Emojis from "./emojis/index.vue"; import Emojis from "./emojis/index.vue";
import Page from "./page.vue"; import Page from "./page.vue";
import { preferences } from "./preferences"; import { preferences } from "./preferences";
@ -110,6 +111,11 @@ const { account: author3 } = useAccountFromAcct(
<Emojis /> <Emojis />
</Page> </Page>
</TabsContent> </TabsContent>
<TabsContent value="Developer" as-child>
<Page title="Developer">
<Developer />
</Page>
</TabsContent>
<TabsContent value="About" as-child> <TabsContent value="About" as-child>
<Page title="About"> <Page title="About">
<section class="space-y-4"> <section class="space-y-4">

View file

@ -25,7 +25,6 @@ const data: [string, string | VNode][] = [
["Author", pkg.author.name], ["Author", pkg.author.name],
[ [
"Repository", "Repository",
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<a <a
href={pkg.repository.url.replace("git+", "")} href={pkg.repository.url.replace("git+", "")}
target="_blank" target="_blank"

View file

@ -13,17 +13,23 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { preferences as prefs } from "../preferences";
import type { Preference } from "../types"; import type { Preference } from "../types";
const { pref } = defineProps<{ const { pref, name } = defineProps<{
pref: Preference<any>; pref: Preference<any>;
name: keyof typeof prefs;
}>(); }>();
const value = ref<any>(pref.options.defaultValue); const value = ref<any>(preferences[name].value);
const setValue = (newValue: MaybeRef<any>) => { const setValue = (newValue: MaybeRef<any>) => {
value.value = toValue(newValue); value.value = toValue(newValue);
}; };
watch(value, (newVal) => {
preferences[name].value = newVal;
});
defineSlots<{ defineSlots<{
default(props: { default(props: {
value: any; value: any;

View file

@ -1,15 +1,17 @@
<template> <template>
<Base :pref="pref" v-slot="{ setValue, value }"> <Base :pref="pref" :name="name" v-slot="{ setValue, value }">
<Switch @update:model-value="setValue" :model-value="value" /> <Switch @update:model-value="setValue" :model-value="value" />
</Base> </Base>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Switch } from "~/components/ui/switch"; import { Switch } from "~/components/ui/switch";
import type { preferences as prefs } from "../preferences";
import type { BooleanPreference } from "../types"; import type { BooleanPreference } from "../types";
import Base from "./base.vue"; import Base from "./base.vue";
const { pref } = defineProps<{ const { pref, name } = defineProps<{
pref: BooleanPreference; pref: BooleanPreference;
name: keyof typeof prefs;
}>(); }>();
</script> </script>

View file

@ -1,6 +1,6 @@
<template> <template>
<Collapsible as-child> <Collapsible as-child>
<Base :pref="pref"> <Base :name="name" :pref="pref">
<template #default> <template #default>
<CollapsibleTrigger as-child> <CollapsibleTrigger as-child>
<Button variant="outline"> <Button variant="outline">
@ -25,10 +25,12 @@ import {
CollapsibleTrigger, CollapsibleTrigger,
} from "~/components/ui/collapsible"; } from "~/components/ui/collapsible";
import { Textarea } from "~/components/ui/textarea"; import { Textarea } from "~/components/ui/textarea";
import type { preferences as prefs } from "../preferences";
import type { CodePreference } from "../types"; import type { CodePreference } from "../types";
import Base from "./base.vue"; import Base from "./base.vue";
const { pref } = defineProps<{ const { pref, name } = defineProps<{
pref: CodePreference; pref: CodePreference;
name: keyof typeof prefs;
}>(); }>();
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<Base :pref="pref" v-slot="{ setValue, value }"> <Base :pref="pref" :name="name" v-slot="{ setValue, value }">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger as-child> <DropdownMenuTrigger as-child>
<Button variant="outline"> <Button variant="outline">
@ -30,10 +30,12 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu"; } from "~/components/ui/dropdown-menu";
import type { preferences as prefs } from "../preferences";
import type { MultiSelectPreference } from "../types"; import type { MultiSelectPreference } from "../types";
import Base from "./base.vue"; import Base from "./base.vue";
const { pref } = defineProps<{ const { pref, name } = defineProps<{
pref: MultiSelectPreference<string>; pref: MultiSelectPreference<string>;
name: keyof typeof prefs;
}>(); }>();
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<Base :pref="pref" v-slot="{ setValue, value }"> <Base :pref="pref" :name="name" v-slot="{ setValue, value }">
<NumberField :model-value="value" @update:model-value="setValue" :min="pref.options.min" :max="pref.options.max" :step="pref.options.integer ? 1 : pref.options.step"> <NumberField :model-value="value" @update:model-value="setValue" :min="pref.options.min" :max="pref.options.max" :step="pref.options.integer ? 1 : pref.options.step">
<NumberFieldContent> <NumberFieldContent>
<NumberFieldDecrement /> <NumberFieldDecrement />
@ -18,10 +18,12 @@ import {
NumberFieldIncrement, NumberFieldIncrement,
NumberFieldInput, NumberFieldInput,
} from "~/components/ui/number-field"; } from "~/components/ui/number-field";
import type { preferences as prefs } from "../preferences";
import type { NumberPreference } from "../types"; import type { NumberPreference } from "../types";
import Base from "./base.vue"; import Base from "./base.vue";
const { pref } = defineProps<{ const { pref, name } = defineProps<{
pref: NumberPreference; pref: NumberPreference;
name: keyof typeof prefs;
}>(); }>();
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<Base :pref="pref" v-slot="{ setValue, value }"> <Base :pref="pref" :name="name" v-slot="{ setValue, value }">
<Select :model-value="value" @update:model-value="setValue"> <Select :model-value="value" @update:model-value="setValue">
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select an option" /> <SelectValue placeholder="Select an option" />
@ -24,10 +24,12 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "~/components/ui/select"; } from "~/components/ui/select";
import type { preferences as prefs } from "../preferences";
import type { SelectPreference } from "../types"; import type { SelectPreference } from "../types";
import Base from "./base.vue"; import Base from "./base.vue";
const { pref } = defineProps<{ const { pref, name } = defineProps<{
pref: SelectPreference<string>; pref: SelectPreference<string>;
name: keyof typeof prefs;
}>(); }>();
</script> </script>

View file

@ -1,15 +1,17 @@
<template> <template>
<Base :pref="pref" v-slot="{ setValue, value }"> <Base :pref="pref" :name="name" v-slot="{ setValue, value }">
<Input placeholder="Content here..." :model-value="value" @update:model-value="setValue" /> <Input placeholder="Content here..." :model-value="value" @update:model-value="setValue" />
</Base> </Base>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import type { preferences as prefs } from "../preferences";
import type { TextPreference } from "../types"; import type { TextPreference } from "../types";
import Base from "./base.vue"; import Base from "./base.vue";
const { pref } = defineProps<{ const { pref, name } = defineProps<{
pref: TextPreference; pref: TextPreference;
name: keyof typeof prefs;
}>(); }>();
</script> </script>

View file

@ -1,6 +1,6 @@
<template> <template>
<Collapsible as-child> <Collapsible as-child>
<Base :pref="pref"> <Base :pref="pref" :name="name">
<template #default> <template #default>
<CollapsibleTrigger as-child> <CollapsibleTrigger as-child>
<Button variant="outline"> <Button variant="outline">
@ -25,10 +25,12 @@ import {
CollapsibleTrigger, CollapsibleTrigger,
} from "~/components/ui/collapsible"; } from "~/components/ui/collapsible";
import { Input, UrlInput } from "~/components/ui/input"; import { Input, UrlInput } from "~/components/ui/input";
import type { preferences as prefs } from "../preferences";
import type { TextPreference } from "../types"; import type { TextPreference } from "../types";
import Base from "./base.vue"; import Base from "./base.vue";
const { pref } = defineProps<{ const { pref, name } = defineProps<{
pref: TextPreference; pref: TextPreference;
name: keyof typeof prefs;
}>(); }>();
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<Avatar :class="[shape.value === 'square' && 'rounded-md', 'bg-secondary']"> <Avatar :class="['rounded-md bg-secondary']">
<AvatarFallback v-if="name"> <AvatarFallback v-if="name">
{{ getInitials(name) }} {{ getInitials(name) }}
</AvatarFallback> </AvatarFallback>
@ -8,7 +8,6 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { SettingIds } from "~/settings";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
const { name } = defineProps<{ const { name } = defineProps<{
@ -29,6 +28,4 @@ const getInitials = (name: string): string => {
return `${firstLetter}${secondLetter}`.toUpperCase(); return `${firstLetter}${secondLetter}`.toUpperCase();
}; };
const shape = useSetting(SettingIds.AvatarShape);
</script> </script>

View file

@ -74,7 +74,6 @@ import { Button } from "~/components/ui/button";
import { Card, CardContent, CardFooter, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardFooter, CardTitle } from "~/components/ui/card";
import { Separator } from "~/components/ui/separator"; import { Separator } from "~/components/ui/separator";
import * as m from "~/paraglide/messages.js"; import * as m from "~/paraglide/messages.js";
import { SettingIds } from "~/settings";
import { confirmModalService } from "../modals/composable"; import { confirmModalService } from "../modals/composable";
import ProfileActions from "./profile-actions.vue"; import ProfileActions from "./profile-actions.vue";
import ProfileBadges from "./profile-badges.vue"; import ProfileBadges from "./profile-badges.vue";
@ -91,10 +90,8 @@ const { relationship, isLoading } = useRelationship(client, account.id);
const isMe = identity.value?.account.id === account.id; const isMe = identity.value?.account.id === account.id;
const [username, instance] = account.acct.split("@"); const [username, instance] = account.acct.split("@");
const confirmFollows = useSetting(SettingIds.ConfirmFollow);
const follow = async () => { const follow = async () => {
if (confirmFollows.value.value) { if (preferences.confirm_actions.value.includes("follow")) {
const confirmation = await confirmModalService.confirm({ const confirmation = await confirmModalService.confirm({
title: m.many_fair_capybara_imagine(), title: m.many_fair_capybara_imagine(),
message: m.mellow_yummy_jannes_cuddle({ message: m.mellow_yummy_jannes_cuddle({
@ -118,7 +115,7 @@ const follow = async () => {
}; };
const unfollow = async () => { const unfollow = async () => {
if (confirmFollows.value.value) { if (preferences.confirm_actions.value.includes("follow")) {
const confirmation = await confirmModalService.confirm({ const confirmation = await confirmModalService.confirm({
title: m.funny_aloof_swan_loop(), title: m.funny_aloof_swan_loop(),
message: m.white_best_dolphin_catch({ message: m.white_best_dolphin_catch({

View file

@ -1,13 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from "@/lib/utils";
import Timelines from "~/components/navigation/timelines.vue"; import Timelines from "~/components/navigation/timelines.vue";
import { SidebarInset } from "~/components/ui/sidebar";
import { SettingIds } from "~/settings";
import LeftSidebar from "./left-sidebar.vue"; import LeftSidebar from "./left-sidebar.vue";
import RightSidebar from "./right-sidebar.vue"; import RightSidebar from "./right-sidebar.vue";
const showRightSidebar = useSetting(SettingIds.NotificationsSidebar);
const route = useRoute(); const route = useRoute();
const isMd = useMediaQuery("(max-width: 768px)"); const isMd = useMediaQuery("(max-width: 768px)");
const showTimelines = computed( const showTimelines = computed(
@ -28,5 +23,5 @@ const showTimelines = computed(
</header> </header>
<slot /> <slot />
</main> </main>
<RightSidebar v-if="identity" v-show="showRightSidebar.value" /> <RightSidebar v-if="identity" v-show="preferences.display_notifications_sidebar" />
</template> </template>

View file

@ -24,7 +24,7 @@
<!-- If there are no posts at all --> <!-- If there are no posts at all -->
<NoPosts v-else-if="hasReachedEnd && items.length === 0" /> <NoPosts v-else-if="hasReachedEnd && items.length === 0" />
<div v-else-if="!infiniteScroll.value" class="py-10 px-4"> <div v-else-if="!preferences.infinite_scroll" class="py-10 px-4">
<Button <Button
variant="secondary" variant="secondary"
@click="loadNext" @click="loadNext"
@ -43,7 +43,6 @@
import type { Notification, Status } from "@versia/client/types"; import type { Notification, Status } from "@versia/client/types";
import { useIntersectionObserver } from "@vueuse/core"; import { useIntersectionObserver } from "@vueuse/core";
import * as m from "~/paraglide/messages.js"; import * as m from "~/paraglide/messages.js";
import { SettingIds } from "~/settings";
import NoPosts from "../errors/NoPosts.vue"; import NoPosts from "../errors/NoPosts.vue";
import ReachedEnd from "../errors/ReachedEnd.vue"; import ReachedEnd from "../errors/ReachedEnd.vue";
import Spinner from "../graphics/spinner.vue"; import Spinner from "../graphics/spinner.vue";
@ -66,14 +65,13 @@ const emit = defineEmits<(e: "update") => void>();
const loadMoreTrigger = ref<HTMLElement | null>(null); const loadMoreTrigger = ref<HTMLElement | null>(null);
// @ts-expect-error Too complex?
useIntersectionObserver(loadMoreTrigger, ([observer]) => { useIntersectionObserver(loadMoreTrigger, ([observer]) => {
if (observer?.isIntersecting && !props.isLoading && !props.hasReachedEnd) { if (observer?.isIntersecting && !props.isLoading && !props.hasReachedEnd) {
props.loadNext(); props.loadNext();
} }
}); });
const infiniteScroll = useSetting(SettingIds.InfiniteScroll);
watch( watch(
() => props.items, () => props.items,
() => { () => {

View file

@ -1,7 +1,3 @@
import { SettingIds } from "~/settings";
export const useLanguage = () => { export const useLanguage = () => {
const lang = useSetting(SettingIds.Language); return computed(() => preferences.language.value);
return computed(() => lang.value.value as "en" | "fr" | "en-pt");
}; };

56
composables/Preference.ts Normal file
View file

@ -0,0 +1,56 @@
import { StorageSerializers } from "@vueuse/core";
import { preferences as prefs } from "~/components/preferences2/preferences.ts";
type SerializedPreferences = {
[K in keyof typeof prefs]: (typeof prefs)[K]["options"]["defaultValue"];
};
const usePreferences = (): {
[K in keyof typeof prefs]: WritableComputedRef<
(typeof prefs)[K]["options"]["defaultValue"]
>;
} => {
const localStorage = useLocalStorage<SerializedPreferences>(
"versia:preferences",
Object.fromEntries(
Object.entries(prefs).map(([key, value]) => [
key,
value.options.defaultValue,
]),
) as SerializedPreferences,
{
serializer: {
read(raw) {
return StorageSerializers.object.read(raw);
},
write(value) {
return StorageSerializers.object.write(value);
},
},
},
);
return Object.fromEntries(
Object.entries(prefs).map(([key, value]) => [
key,
computed({
get() {
return (
localStorage.value[key as keyof typeof prefs] ??
value.options.defaultValue
);
},
set(newValue) {
// @ts-expect-error Key is marked as readonly in the type
localStorage.value[key] = newValue;
},
}),
]),
) as {
[K in keyof typeof prefs]: WritableComputedRef<
(typeof prefs)[K]["options"]["defaultValue"]
>;
};
};
export const preferences = usePreferences();

View file

@ -1,51 +0,0 @@
import { StorageSerializers } from "@vueuse/core";
import {
type SettingIds,
type Settings,
mergeSettings,
settings as settingsJson,
} from "~/settings";
const useSettings = () => {
return useLocalStorage<Settings>("versia:settings", settingsJson(), {
serializer: {
read(raw) {
const json = StorageSerializers.object.read(raw);
return mergeSettings(json);
},
write(value) {
const json = Object.fromEntries(
Object.entries(value).map(([key, value]) => [
key,
value.value,
]),
);
// flatMap object values to .value
return StorageSerializers.object.write(json);
},
},
});
};
export const settings = useSettings();
export const useSetting = <Id extends SettingIds>(
id: Id,
): Ref<Settings[Id]> => {
const setting = ref(settings.value[id]) as Ref<Settings[Id]>;
watch(settings, (newSettings) => {
setting.value = newSettings[id];
});
watch(setting, (newSetting) => {
settings.value = {
...settings.value,
[id]: newSetting,
};
});
return setting;
};

View file

@ -19,10 +19,8 @@ import MobileNavbar from "~/components/navigation/mobile-navbar.vue";
import Preferences from "~/components/preferences2/index.vue"; import Preferences from "~/components/preferences2/index.vue";
import AppSidebar from "~/components/sidebars/sidebar.vue"; import AppSidebar from "~/components/sidebars/sidebar.vue";
import { SidebarProvider } from "~/components/ui/sidebar"; import { SidebarProvider } from "~/components/ui/sidebar";
import { SettingIds } from "~/settings";
const colorMode = useColorMode(); const colorMode = useColorMode();
const themeSetting = useSetting(SettingIds.Theme);
const { n, d } = useMagicKeys(); const { n, d } = useMagicKeys();
const activeElement = useActiveElement(); const activeElement = useActiveElement();
const notUsingInput = computed( const notUsingInput = computed(
@ -44,10 +42,10 @@ watch([n, notUsingInput, d], async () => {
// Swap theme from dark to light or vice versa // Swap theme from dark to light or vice versa
if (colorMode.value === "dark") { if (colorMode.value === "dark") {
colorMode.preference = "light"; colorMode.preference = "light";
themeSetting.value.value = "light"; preferences.color_theme.value = "light";
} else { } else {
colorMode.preference = "dark"; colorMode.preference = "dark";
themeSetting.value.value = "dark"; preferences.color_theme.value = "dark";
} }
} }
}); });

View file

@ -1,64 +0,0 @@
<template>
<!-- Add padding at bottom to prevent hiding some content by the bottom navbar -->
<div class="md:px-8 px-4 py-4 pb-20 max-w-7xl mx-auto w-full">
<h1 class="scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl capitalize">
{{ page }}
</h1>
<div class="grid grid-cols-1 gap-2 mt-6">
<template v-for="[id, setting] of settingEntries">
<SwitchPreference v-if="setting.type === SettingType.Boolean" :setting="(setting as BooleanSetting)" @update:setting="updateSetting(id, setting)" />
<SelectPreference v-else-if="setting.type === SettingType.Enum" :setting="(setting as EnumSetting)" @update:setting="updateSetting(id, setting)" />
<CodePreference v-else-if="setting.type === SettingType.Code" :setting="(setting as CodeSetting)" @update:setting="updateSetting(id, setting)" />
<StringPreference v-else-if="setting.type === SettingType.String" :setting="(setting as StringSetting)" @update:setting="updateSetting(id, setting)" />
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import CodePreference from "~/components/preferences/code.vue";
import SelectPreference from "~/components/preferences/select.vue";
import StringPreference from "~/components/preferences/string.vue";
import SwitchPreference from "~/components/preferences/switch.vue";
import * as m from "~/paraglide/messages.js";
import {
type BooleanSetting,
type CodeSetting,
type EnumSetting,
type Setting,
type SettingIds,
type SettingPages,
SettingType,
type StringSetting,
} from "~/settings.ts";
useHead({
title: m.broad_whole_herring_reside(),
});
definePageMeta({
layout: "app",
breadcrumbs: () => [
{
text: m.broad_whole_herring_reside(),
},
],
requiresAuth: true,
});
const route = useRoute();
const page = route.params.page as SettingPages;
const settingEntries = computed(() =>
(Object.entries(settings.value) as [SettingIds, Setting][]).filter(
([, s]) => s.page === page && !s.notImplemented,
),
);
const updateSetting = (id: SettingIds, setting: Setting) => {
settings.value = {
...settings.value,
[id]: setting,
};
};
</script>

View file

@ -1,106 +0,0 @@
<template>
<div class="md:px-8 px-4 py-2 max-w-7xl mx-auto w-full space-y-6">
<div :class="cn('grid gap-2', canUpload && 'grid-cols-[1fr_auto]')">
<h1
class="scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl capitalize"
>
{{ m.suave_smart_mantis_climb() }}
</h1>
<Uploader v-if="canUpload">
<Button variant="default"> <Upload /> Upload </Button>
</Uploader>
</div>
<div v-if="emojis.length > 0" class="max-w-sm w-full relative">
<Input v-model="search" placeholder="Search" class="pl-8" />
<Search
class="absolute size-4 top-1/2 left-2.5 transform -translate-y-1/2"
/>
</div>
<Category
v-if="emojis.length > 0"
v-for="[name, emojis] in categories"
:key="name"
:emojis="emojis"
:name="name"
/>
<Card v-else class="shadow-none bg-transparent border-none p-4">
<CardHeader class="text-center gap-y-4">
<CardTitle>{{ m.actual_steep_llama_rest() }}</CardTitle>
<CardDescription>
{{ m.lucky_suave_myna_adore() }}
</CardDescription>
</CardHeader>
</Card>
</div>
</template>
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import { type Emoji, RolePermission } from "@versia/client/types";
import { Search, Upload } from "lucide-vue-next";
import Category from "~/components/preferences/emojis/category.vue";
import Uploader from "~/components/preferences/emojis/uploader.vue";
import { Button } from "~/components/ui/button";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import * as m from "~/paraglide/messages.js";
useHead({
title: m.mild_many_dolphin_mend(),
});
definePageMeta({
layout: "app",
breadcrumbs: () => [
{
text: m.broad_whole_herring_reside(),
},
],
requiresAuth: true,
});
const permissions = usePermissions();
const canUpload = computed(
() =>
permissions.value.includes(RolePermission.ManageOwnEmojis) ||
permissions.value.includes(RolePermission.ManageEmojis),
);
const emojis = computed(
() =>
identity.value?.emojis?.filter((emoji) =>
emoji.shortcode.toLowerCase().includes(search.value.toLowerCase()),
) ?? [],
);
const search = ref("");
/**
* Sort emojis by category
*/
const categories = computed(() => {
const categories = new Map<string, Emoji[]>();
for (const emoji of emojis.value) {
if (!emoji.category) {
if (!categories.has(m.lucky_ago_rat_pinch())) {
categories.set(m.lucky_ago_rat_pinch(), []);
}
categories.get(m.lucky_ago_rat_pinch())?.push(emoji);
continue;
}
if (!categories.has(emoji.category)) {
categories.set(emoji.category, []);
}
categories.get(emoji.category)?.push(emoji);
}
return categories;
});
</script>

View file

@ -1,19 +0,0 @@
<template>
<div>
</div>
</template>
<script lang="ts" setup>
import * as m from "~/paraglide/messages.js";
definePageMeta({
layout: "app",
breadcrumbs: () => [
{
text: m.broad_whole_herring_reside(),
},
],
requiresAuth: true,
});
</script>

View file

@ -1,5 +1,4 @@
import type { Emoji } from "@versia/client/types"; import type { Emoji } from "@versia/client/types";
import { SettingIds } from "~/settings";
const emojisRegex = const emojisRegex =
/\p{RI}\p{RI}|\p{Emoji}(\p{EMod}|\uFE0F\u20E3?|[\u{E0020}-\u{E007E}]+\u{E007F})?(\u200D(\p{RI}\p{RI}|\p{Emoji}(\p{EMod}|\uFE0F\u20E3?|[\u{E0020}-\u{E007E}]+\u{E007F})?))*/gu; /\p{RI}\p{RI}|\p{Emoji}(\p{EMod}|\uFE0F\u20E3?|[\u{E0020}-\u{E007E}]+\u{E007F})?(\u200D(\p{RI}\p{RI}|\p{Emoji}(\p{EMod}|\uFE0F\u20E3?|[\u{E0020}-\u{E007E}]+\u{E007F})?))*/gu;
@ -8,11 +7,8 @@ const incorrectEmojisRegex = /^[#*0-9©®]$/;
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.directive<HTMLElement, Emoji[]>("render-emojis", { nuxtApp.vueApp.directive<HTMLElement, Emoji[]>("render-emojis", {
beforeMount(el, binding) { beforeMount(el, binding) {
const shouldRenderEmoji = useSetting(SettingIds.CustomEmojis);
const emojiFont = useSetting(SettingIds.EmojiTheme);
// Replace emoji shortcodes with images // Replace emoji shortcodes with images
if (shouldRenderEmoji.value.value) { if (preferences.custom_emojis.value) {
el.innerHTML = el.innerHTML.replace( el.innerHTML = el.innerHTML.replace(
/:([a-zA-Z0-9_-]+):/g, /:([a-zA-Z0-9_-]+):/g,
(match, emoji) => { (match, emoji) => {
@ -35,13 +31,13 @@ export default defineNuxtPlugin((nuxtApp) => {
); );
} }
if (emojiFont.value.value !== "native") { if (preferences.emoji_theme.value !== "native") {
el.innerHTML = el.innerHTML.replace(emojisRegex, (match) => { el.innerHTML = el.innerHTML.replace(emojisRegex, (match) => {
if (incorrectEmojisRegex.test(match)) { if (incorrectEmojisRegex.test(match)) {
return match; return match;
} }
return `<img src="/emojis/${emojiFont}/${match}.svg" alt="${match}" class="h-[1em] inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out">`; return `<img src="/emojis/${preferences.emoji_theme.value}/${match}.svg" alt="${match}" class="h-[1em] inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out">`;
}); });
} }