mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
feat: ✨ Wire up new preferences and remove old settings
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
This commit is contained in:
parent
412e49dfe2
commit
3ce71dd4df
20
app.vue
20
app.vue
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
60
components/preferences2/developer.vue
Normal file
60
components/preferences2/developer.vue
Normal 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>
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
() => {
|
() => {
|
||||||
|
|
|
||||||
|
|
@ -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
56
composables/Preference.ts
Normal 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();
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
|
|
@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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">`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue