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>
|
||||
<TooltipProvider>
|
||||
<Component is="style">
|
||||
{{ customCss.value }}
|
||||
{{ preferences.custom_css }}
|
||||
</Component>
|
||||
<NuxtPwaAssets />
|
||||
<NuxtLayout>
|
||||
|
|
@ -19,7 +19,6 @@ import ConfirmationModal from "./components/modals/confirm.vue";
|
|||
import { Toaster } from "./components/ui/sonner";
|
||||
import { TooltipProvider } from "./components/ui/tooltip";
|
||||
import { overwriteGetLocale } from "./paraglide/runtime";
|
||||
import { type EnumSetting, SettingIds } from "./settings";
|
||||
// Sin
|
||||
//import "~/styles/mcdonalds.css";
|
||||
|
||||
|
|
@ -31,17 +30,16 @@ const origin = useRequestURL().searchParams.get("origin");
|
|||
const appData = useAppData();
|
||||
const instance = useInstance();
|
||||
const description = useExtendedDescription(client);
|
||||
const customCss = useSetting(SettingIds.CustomCSS);
|
||||
const route = useRoute();
|
||||
|
||||
// Theme switcher
|
||||
const theme = useSetting(SettingIds.Theme) as Ref<EnumSetting>;
|
||||
const colorMode = useColorMode();
|
||||
const radius = useCssVar("--radius");
|
||||
|
||||
watch(theme.value, () => {
|
||||
watch(preferences.color_theme, (newVal) => {
|
||||
// Add theme-changing class to html to trigger transition
|
||||
document.documentElement.classList.add("theme-changing");
|
||||
colorMode.preference = theme.value.value;
|
||||
colorMode.preference = newVal;
|
||||
|
||||
setTimeout(() => {
|
||||
// Remove theme-changing class after transition
|
||||
|
|
@ -49,6 +47,16 @@ watch(theme.value, () => {
|
|||
}, 1000);
|
||||
});
|
||||
|
||||
watch(
|
||||
preferences.border_radius,
|
||||
(newVal) => {
|
||||
radius.value = `${newVal}rem`;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
useSeoMeta({
|
||||
titleTemplate: (titleChunk) => {
|
||||
return titleChunk ? `${titleChunk} · Versia` : "Versia";
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@
|
|||
"noUnusedVariables": "off",
|
||||
"noUnusedImports": "off",
|
||||
"noUndeclaredDependencies": "off",
|
||||
"useImportExtensions": "off"
|
||||
"useImportExtensions": "off",
|
||||
"useJsxKeyInIterable": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noExcessiveCognitiveComplexity": "off"
|
||||
|
|
|
|||
|
|
@ -159,7 +159,6 @@ import {
|
|||
SelectTrigger,
|
||||
} from "~/components/ui/select";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { SettingIds } from "~/settings";
|
||||
import EditorContent from "../editor/content.vue";
|
||||
import { Button } from "../ui/button";
|
||||
import { DialogFooter } from "../ui/dialog";
|
||||
|
|
@ -169,13 +168,11 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
|||
import Files from "./files.vue";
|
||||
|
||||
const { Control_Enter, Command_Enter } = useMagicKeys();
|
||||
const ctrlEnterSend = useSetting(SettingIds.CtrlEnterToSend);
|
||||
const defaultVisibility = useSetting(SettingIds.DefaultVisibility);
|
||||
const { play } = useAudio();
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
watch([Control_Enter, Command_Enter], () => {
|
||||
if (sending.value || !ctrlEnterSend.value.value) {
|
||||
if (sending.value || !preferences.ctrl_enter_send.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -220,9 +217,10 @@ const state = reactive({
|
|||
sensitive: relation?.type === "edit" ? relation.note.sensitive : false,
|
||||
contentWarning: relation?.type === "edit" ? relation.note.spoiler_text : "",
|
||||
contentType: "text/html" as "text/html" | "text/plain",
|
||||
visibility: (relation?.type === "edit"
|
||||
? relation.note.visibility
|
||||
: (defaultVisibility.value.value ?? "public")) as Status["visibility"],
|
||||
visibility:
|
||||
relation?.type === "edit"
|
||||
? relation.note.visibility
|
||||
: preferences.default_visibility.value,
|
||||
files: (relation?.type === "edit"
|
||||
? relation.note.media_attachments.map((a) => ({
|
||||
apiId: a.id,
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ defineProps<{
|
|||
modalOptions: ConfirmModalOptions;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
confirm: (result: ConfirmModalResult) => void;
|
||||
cancel: () => void;
|
||||
const emit = defineEmits<{
|
||||
confirm: [result: ConfirmModalResult];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
const inputValue = ref<string>("");
|
||||
|
|
@ -55,10 +55,10 @@ const inputValue = ref<string>("");
|
|||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="() => $emit('cancel')">
|
||||
<Button variant="outline" @click="() => emit('cancel')">
|
||||
{{ modalOptions.cancelText }}
|
||||
</Button>
|
||||
<Button @click="() => $emit('confirm', {
|
||||
<Button @click="() => emit('confirm', {
|
||||
confirmed: true,
|
||||
value: inputValue,
|
||||
})">
|
||||
|
|
@ -67,4 +67,4 @@ const inputValue = ref<string>("");
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import { Ellipsis, Heart, Quote, Repeat, Reply } from "lucide-vue-next";
|
|||
import { toast } from "vue-sonner";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { getLocale } from "~/paraglide/runtime";
|
||||
import { SettingIds } from "~/settings";
|
||||
import { confirmModalService } from "../modals/composable";
|
||||
import ActionButton from "./action-button.vue";
|
||||
import Menu from "./menu.vue";
|
||||
|
|
@ -48,11 +47,8 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
const { play } = useAudio();
|
||||
|
||||
const confirmLikes = useSetting(SettingIds.ConfirmLike);
|
||||
const confirmReblogs = useSetting(SettingIds.ConfirmReblog);
|
||||
|
||||
const like = async () => {
|
||||
if (confirmLikes.value.value) {
|
||||
if (preferences.confirm_actions.value.includes("like")) {
|
||||
const confirmation = await confirmModalService.confirm({
|
||||
title: m.slimy_least_ray_aid(),
|
||||
message: m.stale_new_ray_jolt(),
|
||||
|
|
@ -74,7 +70,7 @@ const like = async () => {
|
|||
};
|
||||
|
||||
const unlike = async () => {
|
||||
if (confirmLikes.value.value) {
|
||||
if (preferences.confirm_actions.value.includes("like")) {
|
||||
const confirmation = await confirmModalService.confirm({
|
||||
title: m.odd_strong_halibut_prosper(),
|
||||
message: m.slow_blue_parrot_savor(),
|
||||
|
|
@ -95,7 +91,7 @@ const unlike = async () => {
|
|||
};
|
||||
|
||||
const reblog = async () => {
|
||||
if (confirmReblogs.value.value) {
|
||||
if (preferences.confirm_actions.value.includes("reblog")) {
|
||||
const confirmation = await confirmModalService.confirm({
|
||||
title: m.best_mellow_llama_surge(),
|
||||
message: m.salty_plain_mallard_gaze(),
|
||||
|
|
@ -116,7 +112,7 @@ const reblog = async () => {
|
|||
};
|
||||
|
||||
const unreblog = async () => {
|
||||
if (confirmReblogs.value.value) {
|
||||
if (preferences.confirm_actions.value.includes("reblog")) {
|
||||
const confirmation = await confirmModalService.confirm({
|
||||
title: m.main_fancy_octopus_loop(),
|
||||
message: m.odd_alive_swan_express(),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<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>
|
||||
</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">
|
||||
<Note :note="quote" :hide-actions="true" :small-layout="true" />
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import type { Attachment, Emoji, Status } from "@versia/client/types";
|
||||
import { type BooleanSetting, SettingIds } from "~/settings";
|
||||
import Attachments from "./attachments.vue";
|
||||
import ContentWarning from "./content-warning.vue";
|
||||
import Note from "./note.vue";
|
||||
|
|
@ -32,7 +31,6 @@ const { content, plainContent, sensitive, contentWarning } = defineProps<{
|
|||
}>();
|
||||
|
||||
const blurred = ref(sensitive || !!contentWarning);
|
||||
const showCw = useSetting(SettingIds.ShowContentWarning) as Ref<BooleanSetting>;
|
||||
|
||||
const characterCount = plainContent?.length;
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="rounded flex flex-row items-center gap-3">
|
||||
<HoverCard v-model:open="popupOpen" @update:open="() => {
|
||||
if (!enableHoverCard.value) {
|
||||
if (!preferences.popup_avatar_hover) {
|
||||
popupOpen = false;
|
||||
}
|
||||
}" :open-delay="2000">
|
||||
|
|
@ -51,7 +51,6 @@ import type {
|
|||
} from "@vueuse/core";
|
||||
import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next";
|
||||
import { getLocale } from "~/paraglide/runtime";
|
||||
import { SettingIds } from "~/settings";
|
||||
import Avatar from "../profiles/avatar.vue";
|
||||
import SmallCard from "../profiles/small-card.vue";
|
||||
import {
|
||||
|
|
@ -59,7 +58,6 @@ import {
|
|||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "../ui/hover-card";
|
||||
import CopyableText from "./copyable-text.vue";
|
||||
|
||||
const { createdAt, noteUrl, author, authorUrl } = defineProps<{
|
||||
cornerAvatar?: string;
|
||||
|
|
@ -94,7 +92,6 @@ const fullTime = new Intl.DateTimeFormat(getLocale(), {
|
|||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
}).format(createdAt);
|
||||
const enableHoverCard = useSetting(SettingIds.PopupAvatarHover);
|
||||
const popupOpen = ref(false);
|
||||
|
||||
const visibilities = {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import {
|
|||
import { toast } from "vue-sonner";
|
||||
import { confirmModalService } from "~/components/modals/composable.ts";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { SettingIds } from "~/settings";
|
||||
|
||||
const { authorId, noteId } = defineProps<{
|
||||
apiNoteString: string;
|
||||
|
|
@ -41,8 +40,6 @@ const { copy } = useClipboard();
|
|||
const loggedIn = !!identity.value;
|
||||
const authorIsMe = loggedIn && authorId === identity.value?.account.id;
|
||||
|
||||
const confirmDeletes = useSetting(SettingIds.ConfirmDelete);
|
||||
|
||||
const copyText = (text: string) => {
|
||||
copy(text);
|
||||
toast.success(m.flat_nice_worm_dream());
|
||||
|
|
@ -57,7 +54,7 @@ const blockUser = async (userId: string) => {
|
|||
};
|
||||
|
||||
const _delete = async () => {
|
||||
if (confirmDeletes.value.value) {
|
||||
if (preferences.confirm_actions.value.includes("delete")) {
|
||||
const confirmation = await confirmModalService.confirm({
|
||||
title: m.calm_icy_weasel_twirl(),
|
||||
message: m.gray_fun_toucan_slide(),
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@
|
|||
</CardTitle>
|
||||
<Card class="p-0 gap-0">
|
||||
<div v-for="preference of preferences" :key="preference">
|
||||
<TextPreferenceVue v-if="(prefs[preference] instanceof TextPreference)" :pref="(prefs[preference] as TextPreference)" />
|
||||
<BooleanPreferenceVue v-else-if="(prefs[preference] instanceof BooleanPreference)" :pref="(prefs[preference] as BooleanPreference)" />
|
||||
<SelectPreferenceVue v-else-if="(prefs[preference] instanceof SelectPreference)" :pref="(prefs[preference] as SelectPreference<string>)" />
|
||||
<NumberPreferenceVue v-else-if="(prefs[preference] instanceof NumberPreference)" :pref="(prefs[preference] as NumberPreference)" />
|
||||
<MultiSelectPreferenceVue v-else-if="(prefs[preference] instanceof MultiSelectPreference)" :pref="(prefs[preference] as MultiSelectPreference<string>)" />
|
||||
<CodePreferenceVue v-else-if="(prefs[preference] instanceof CodePreference)" :pref="(prefs[preference] as CodePreference)" />
|
||||
<UrlPreferenceVue v-else-if="(prefs[preference] instanceof UrlPreference)" :pref="(prefs[preference] as UrlPreference)" />
|
||||
<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)" :name="preference" />
|
||||
<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)" :name="preference" />
|
||||
<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)" :name="preference" />
|
||||
<UrlPreferenceVue v-else-if="(prefs[preference] instanceof UrlPreference)" :pref="(prefs[preference] as UrlPreference)" :name="preference" />
|
||||
</div>
|
||||
</Card>
|
||||
</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 { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
import Category from "./category.vue";
|
||||
import Developer from "./developer.vue";
|
||||
import Emojis from "./emojis/index.vue";
|
||||
import Page from "./page.vue";
|
||||
import { preferences } from "./preferences";
|
||||
|
|
@ -110,6 +111,11 @@ const { account: author3 } = useAccountFromAcct(
|
|||
<Emojis />
|
||||
</Page>
|
||||
</TabsContent>
|
||||
<TabsContent value="Developer" as-child>
|
||||
<Page title="Developer">
|
||||
<Developer />
|
||||
</Page>
|
||||
</TabsContent>
|
||||
<TabsContent value="About" as-child>
|
||||
<Page title="About">
|
||||
<section class="space-y-4">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ const data: [string, string | VNode][] = [
|
|||
["Author", pkg.author.name],
|
||||
[
|
||||
"Repository",
|
||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
||||
<a
|
||||
href={pkg.repository.url.replace("git+", "")}
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -13,17 +13,23 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { preferences as prefs } from "../preferences";
|
||||
import type { Preference } from "../types";
|
||||
|
||||
const { pref } = defineProps<{
|
||||
const { pref, name } = defineProps<{
|
||||
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>) => {
|
||||
value.value = toValue(newValue);
|
||||
};
|
||||
|
||||
watch(value, (newVal) => {
|
||||
preferences[name].value = newVal;
|
||||
});
|
||||
|
||||
defineSlots<{
|
||||
default(props: {
|
||||
value: any;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
<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" />
|
||||
</Base>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import type { preferences as prefs } from "../preferences";
|
||||
import type { BooleanPreference } from "../types";
|
||||
import Base from "./base.vue";
|
||||
|
||||
const { pref } = defineProps<{
|
||||
const { pref, name } = defineProps<{
|
||||
pref: BooleanPreference;
|
||||
name: keyof typeof prefs;
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<Collapsible as-child>
|
||||
<Base :pref="pref">
|
||||
<Base :name="name" :pref="pref">
|
||||
<template #default>
|
||||
<CollapsibleTrigger as-child>
|
||||
<Button variant="outline">
|
||||
|
|
@ -25,10 +25,12 @@ import {
|
|||
CollapsibleTrigger,
|
||||
} from "~/components/ui/collapsible";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import type { preferences as prefs } from "../preferences";
|
||||
import type { CodePreference } from "../types";
|
||||
import Base from "./base.vue";
|
||||
|
||||
const { pref } = defineProps<{
|
||||
const { pref, name } = defineProps<{
|
||||
pref: CodePreference;
|
||||
name: keyof typeof prefs;
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<Base :pref="pref" v-slot="{ setValue, value }">
|
||||
<Base :pref="pref" :name="name" v-slot="{ setValue, value }">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="outline">
|
||||
|
|
@ -30,10 +30,12 @@ import {
|
|||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
import type { preferences as prefs } from "../preferences";
|
||||
import type { MultiSelectPreference } from "../types";
|
||||
import Base from "./base.vue";
|
||||
|
||||
const { pref } = defineProps<{
|
||||
const { pref, name } = defineProps<{
|
||||
pref: MultiSelectPreference<string>;
|
||||
name: keyof typeof prefs;
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<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">
|
||||
<NumberFieldContent>
|
||||
<NumberFieldDecrement />
|
||||
|
|
@ -18,10 +18,12 @@ import {
|
|||
NumberFieldIncrement,
|
||||
NumberFieldInput,
|
||||
} from "~/components/ui/number-field";
|
||||
import type { preferences as prefs } from "../preferences";
|
||||
import type { NumberPreference } from "../types";
|
||||
import Base from "./base.vue";
|
||||
|
||||
const { pref } = defineProps<{
|
||||
const { pref, name } = defineProps<{
|
||||
pref: NumberPreference;
|
||||
name: keyof typeof prefs;
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<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">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
|
|
@ -24,10 +24,12 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/ui/select";
|
||||
import type { preferences as prefs } from "../preferences";
|
||||
import type { SelectPreference } from "../types";
|
||||
import Base from "./base.vue";
|
||||
|
||||
const { pref } = defineProps<{
|
||||
const { pref, name } = defineProps<{
|
||||
pref: SelectPreference<string>;
|
||||
name: keyof typeof prefs;
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
<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" />
|
||||
</Base>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Input } from "~/components/ui/input";
|
||||
import type { preferences as prefs } from "../preferences";
|
||||
import type { TextPreference } from "../types";
|
||||
import Base from "./base.vue";
|
||||
|
||||
const { pref } = defineProps<{
|
||||
const { pref, name } = defineProps<{
|
||||
pref: TextPreference;
|
||||
name: keyof typeof prefs;
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<Collapsible as-child>
|
||||
<Base :pref="pref">
|
||||
<Base :pref="pref" :name="name">
|
||||
<template #default>
|
||||
<CollapsibleTrigger as-child>
|
||||
<Button variant="outline">
|
||||
|
|
@ -25,10 +25,12 @@ import {
|
|||
CollapsibleTrigger,
|
||||
} from "~/components/ui/collapsible";
|
||||
import { Input, UrlInput } from "~/components/ui/input";
|
||||
import type { preferences as prefs } from "../preferences";
|
||||
import type { TextPreference } from "../types";
|
||||
import Base from "./base.vue";
|
||||
|
||||
const { pref } = defineProps<{
|
||||
const { pref, name } = defineProps<{
|
||||
pref: TextPreference;
|
||||
name: keyof typeof prefs;
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<Avatar :class="[shape.value === 'square' && 'rounded-md', 'bg-secondary']">
|
||||
<Avatar :class="['rounded-md bg-secondary']">
|
||||
<AvatarFallback v-if="name">
|
||||
{{ getInitials(name) }}
|
||||
</AvatarFallback>
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { SettingIds } from "~/settings";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||
|
||||
const { name } = defineProps<{
|
||||
|
|
@ -29,6 +28,4 @@ const getInitials = (name: string): string => {
|
|||
|
||||
return `${firstLetter}${secondLetter}`.toUpperCase();
|
||||
};
|
||||
|
||||
const shape = useSetting(SettingIds.AvatarShape);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ import { Button } from "~/components/ui/button";
|
|||
import { Card, CardContent, CardFooter, CardTitle } from "~/components/ui/card";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { SettingIds } from "~/settings";
|
||||
import { confirmModalService } from "../modals/composable";
|
||||
import ProfileActions from "./profile-actions.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 [username, instance] = account.acct.split("@");
|
||||
|
||||
const confirmFollows = useSetting(SettingIds.ConfirmFollow);
|
||||
|
||||
const follow = async () => {
|
||||
if (confirmFollows.value.value) {
|
||||
if (preferences.confirm_actions.value.includes("follow")) {
|
||||
const confirmation = await confirmModalService.confirm({
|
||||
title: m.many_fair_capybara_imagine(),
|
||||
message: m.mellow_yummy_jannes_cuddle({
|
||||
|
|
@ -118,7 +115,7 @@ const follow = async () => {
|
|||
};
|
||||
|
||||
const unfollow = async () => {
|
||||
if (confirmFollows.value.value) {
|
||||
if (preferences.confirm_actions.value.includes("follow")) {
|
||||
const confirmation = await confirmModalService.confirm({
|
||||
title: m.funny_aloof_swan_loop(),
|
||||
message: m.white_best_dolphin_catch({
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import Timelines from "~/components/navigation/timelines.vue";
|
||||
import { SidebarInset } from "~/components/ui/sidebar";
|
||||
import { SettingIds } from "~/settings";
|
||||
import LeftSidebar from "./left-sidebar.vue";
|
||||
import RightSidebar from "./right-sidebar.vue";
|
||||
|
||||
const showRightSidebar = useSetting(SettingIds.NotificationsSidebar);
|
||||
|
||||
const route = useRoute();
|
||||
const isMd = useMediaQuery("(max-width: 768px)");
|
||||
const showTimelines = computed(
|
||||
|
|
@ -28,5 +23,5 @@ const showTimelines = computed(
|
|||
</header>
|
||||
<slot />
|
||||
</main>
|
||||
<RightSidebar v-if="identity" v-show="showRightSidebar.value" />
|
||||
<RightSidebar v-if="identity" v-show="preferences.display_notifications_sidebar" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
<!-- If there are no posts at all -->
|
||||
<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
|
||||
variant="secondary"
|
||||
@click="loadNext"
|
||||
|
|
@ -43,7 +43,6 @@
|
|||
import type { Notification, Status } from "@versia/client/types";
|
||||
import { useIntersectionObserver } from "@vueuse/core";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { SettingIds } from "~/settings";
|
||||
import NoPosts from "../errors/NoPosts.vue";
|
||||
import ReachedEnd from "../errors/ReachedEnd.vue";
|
||||
import Spinner from "../graphics/spinner.vue";
|
||||
|
|
@ -66,14 +65,13 @@ const emit = defineEmits<(e: "update") => void>();
|
|||
|
||||
const loadMoreTrigger = ref<HTMLElement | null>(null);
|
||||
|
||||
// @ts-expect-error Too complex?
|
||||
useIntersectionObserver(loadMoreTrigger, ([observer]) => {
|
||||
if (observer?.isIntersecting && !props.isLoading && !props.hasReachedEnd) {
|
||||
props.loadNext();
|
||||
}
|
||||
});
|
||||
|
||||
const infiniteScroll = useSetting(SettingIds.InfiniteScroll);
|
||||
|
||||
watch(
|
||||
() => props.items,
|
||||
() => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
import { SettingIds } from "~/settings";
|
||||
|
||||
export const useLanguage = () => {
|
||||
const lang = useSetting(SettingIds.Language);
|
||||
|
||||
return computed(() => lang.value.value as "en" | "fr" | "en-pt");
|
||||
return computed(() => preferences.language.value);
|
||||
};
|
||||
|
|
|
|||
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 AppSidebar from "~/components/sidebars/sidebar.vue";
|
||||
import { SidebarProvider } from "~/components/ui/sidebar";
|
||||
import { SettingIds } from "~/settings";
|
||||
|
||||
const colorMode = useColorMode();
|
||||
const themeSetting = useSetting(SettingIds.Theme);
|
||||
const { n, d } = useMagicKeys();
|
||||
const activeElement = useActiveElement();
|
||||
const notUsingInput = computed(
|
||||
|
|
@ -44,10 +42,10 @@ watch([n, notUsingInput, d], async () => {
|
|||
// Swap theme from dark to light or vice versa
|
||||
if (colorMode.value === "dark") {
|
||||
colorMode.preference = "light";
|
||||
themeSetting.value.value = "light";
|
||||
preferences.color_theme.value = "light";
|
||||
} else {
|
||||
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 { SettingIds } from "~/settings";
|
||||
|
||||
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;
|
||||
|
|
@ -8,11 +7,8 @@ const incorrectEmojisRegex = /^[#*0-9©®]$/;
|
|||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.directive<HTMLElement, Emoji[]>("render-emojis", {
|
||||
beforeMount(el, binding) {
|
||||
const shouldRenderEmoji = useSetting(SettingIds.CustomEmojis);
|
||||
const emojiFont = useSetting(SettingIds.EmojiTheme);
|
||||
|
||||
// Replace emoji shortcodes with images
|
||||
if (shouldRenderEmoji.value.value) {
|
||||
if (preferences.custom_emojis.value) {
|
||||
el.innerHTML = el.innerHTML.replace(
|
||||
/:([a-zA-Z0-9_-]+):/g,
|
||||
(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) => {
|
||||
if (incorrectEmojisRegex.test(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