feat(frontend): allow editing + using custom preferences
This commit is contained in:
parent
8bda5f9860
commit
9a80bb2e9b
11 changed files with 229 additions and 177 deletions
|
@ -8,6 +8,8 @@
|
|||
type FieldEntry,
|
||||
type MeUser,
|
||||
type Pronoun,
|
||||
PreferenceSize,
|
||||
type CustomPreferences,
|
||||
} from "$lib/api/entities";
|
||||
import FallbackImage from "$lib/components/FallbackImage.svelte";
|
||||
import { userStore } from "$lib/store";
|
||||
|
@ -37,6 +39,7 @@
|
|||
import { charCount, renderMarkdown } from "$lib/utils";
|
||||
import MarkdownHelp from "../MarkdownHelp.svelte";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import CustomPreference from "./CustomPreference.svelte";
|
||||
|
||||
const MAX_AVATAR_BYTES = 1_000_000;
|
||||
|
||||
|
@ -52,6 +55,7 @@
|
|||
let pronouns: Pronoun[] = window.structuredClone(data.user.pronouns);
|
||||
let fields: Field[] = window.structuredClone(data.user.fields);
|
||||
let list_private = data.user.list_private;
|
||||
let custom_preferences = window.structuredClone(data.user.custom_preferences);
|
||||
|
||||
let avatar: string | null;
|
||||
let avatar_files: FileList | null;
|
||||
|
@ -60,6 +64,9 @@
|
|||
let newPronouns = "";
|
||||
let newLink = "";
|
||||
|
||||
let preferenceIds: string[];
|
||||
$: preferenceIds = Object.keys(custom_preferences);
|
||||
|
||||
let modified = false;
|
||||
|
||||
$: modified = isModified(
|
||||
|
@ -73,6 +80,7 @@
|
|||
avatar,
|
||||
member_title,
|
||||
list_private,
|
||||
custom_preferences,
|
||||
);
|
||||
$: getAvatar(avatar_files).then((b64) => (avatar = b64));
|
||||
|
||||
|
@ -87,6 +95,7 @@
|
|||
avatar: string | null,
|
||||
member_title: string,
|
||||
list_private: boolean,
|
||||
custom_preferences: CustomPreferences,
|
||||
) => {
|
||||
if (bio !== (user.bio || "")) return true;
|
||||
if (display_name !== (user.display_name || "")) return true;
|
||||
|
@ -95,6 +104,7 @@
|
|||
if (!fieldsEqual(fields, user.fields)) return true;
|
||||
if (!namesEqual(names, user.names)) return true;
|
||||
if (!pronounsEqual(pronouns, user.pronouns)) return true;
|
||||
if (!customPreferencesEqual(custom_preferences, user.custom_preferences)) return true;
|
||||
if (avatar !== null) return true;
|
||||
if (list_private !== user.list_private) return true;
|
||||
|
||||
|
@ -136,6 +146,21 @@
|
|||
return arr1.every((_, i) => arr1[i] === arr2[i]);
|
||||
};
|
||||
|
||||
const customPreferencesEqual = (obj1: CustomPreferences, obj2: CustomPreferences) => {
|
||||
return Object.keys(obj1)
|
||||
.map((key) => {
|
||||
if (!(key in obj2)) return false;
|
||||
return (
|
||||
obj1[key].icon === obj2[key].icon &&
|
||||
obj1[key].tooltip === obj2[key].tooltip &&
|
||||
obj1[key].favourite === obj2[key].favourite &&
|
||||
obj1[key].muted === obj2[key].muted &&
|
||||
obj1[key].size === obj2[key].size
|
||||
);
|
||||
})
|
||||
.every((entry) => entry);
|
||||
};
|
||||
|
||||
const getAvatar = async (list: FileList | null) => {
|
||||
if (!list || list.length === 0) return null;
|
||||
if (list[0].size > MAX_AVATAR_BYTES) {
|
||||
|
@ -226,6 +251,19 @@
|
|||
newLink = "";
|
||||
};
|
||||
|
||||
const addPreference = () => {
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
custom_preferences[id] = {
|
||||
icon: "question",
|
||||
tooltip: "New preference",
|
||||
size: PreferenceSize.Normal,
|
||||
muted: false,
|
||||
favourite: false,
|
||||
};
|
||||
custom_preferences = custom_preferences;
|
||||
};
|
||||
|
||||
const removeName = (index: number) => {
|
||||
names.splice(index, 1);
|
||||
names = [...names];
|
||||
|
@ -246,6 +284,11 @@
|
|||
fields = [...fields];
|
||||
};
|
||||
|
||||
const removePreference = (id: string) => {
|
||||
delete custom_preferences[id];
|
||||
custom_preferences = custom_preferences;
|
||||
};
|
||||
|
||||
const updateUser = async () => {
|
||||
const toastId = addToast({
|
||||
header: "Saving changes",
|
||||
|
@ -264,6 +307,7 @@
|
|||
fields,
|
||||
member_title,
|
||||
list_private,
|
||||
custom_preferences,
|
||||
});
|
||||
|
||||
data.user = resp;
|
||||
|
@ -367,6 +411,7 @@
|
|||
<EditableName
|
||||
bind:value={names[index].value}
|
||||
bind:status={names[index].status}
|
||||
preferences={data.user.custom_preferences}
|
||||
moveUp={() => moveName(index, true)}
|
||||
moveDown={() => moveName(index, false)}
|
||||
remove={() => removeName(index)}
|
||||
|
@ -447,6 +492,7 @@
|
|||
{#each fields as _, index}
|
||||
<EditableField
|
||||
bind:field={fields[index]}
|
||||
preferences={data.user.custom_preferences}
|
||||
deleteField={() => removeField(index)}
|
||||
moveField={(up) => moveField(index, up)}
|
||||
/>
|
||||
|
@ -478,7 +524,7 @@
|
|||
</form>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tabId="other" tab="Other">
|
||||
<TabPane tabId="other" tab="Preferences & other">
|
||||
<div class="row mt-3">
|
||||
<div class="col-md">
|
||||
<FormGroup floating label={'"Members" header text'}>
|
||||
|
@ -510,5 +556,18 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>
|
||||
Preferences <Button on:click={addPreference} color="success"
|
||||
><Icon name="plus" aria-hidden /> Add new</Button
|
||||
>
|
||||
</h3>
|
||||
{#each preferenceIds as id}
|
||||
<CustomPreference
|
||||
bind:preference={custom_preferences[id]}
|
||||
remove={() => removePreference(id)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
|
|
86
frontend/src/routes/edit/profile/CustomPreference.svelte
Normal file
86
frontend/src/routes/edit/profile/CustomPreference.svelte
Normal file
|
@ -0,0 +1,86 @@
|
|||
<script lang="ts">
|
||||
import { PreferenceSize, type CustomPreference } from "$lib/api/entities";
|
||||
import IconButton from "$lib/components/IconButton.svelte";
|
||||
import {
|
||||
ButtonDropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
Icon,
|
||||
Input,
|
||||
InputGroup,
|
||||
Tooltip,
|
||||
} from "sveltestrap";
|
||||
import icons from "../../../icons";
|
||||
|
||||
export let preference: CustomPreference;
|
||||
export let remove: () => void;
|
||||
|
||||
let iconButton: HTMLElement;
|
||||
let sizeButton: HTMLElement;
|
||||
|
||||
const toggleMuted = () => (preference.muted = !preference.muted);
|
||||
const toggleFavourite = () => (preference.favourite = !preference.favourite);
|
||||
|
||||
let searchBox = "";
|
||||
let filteredIcons: string[] = [];
|
||||
$: filteredIcons = searchBox ? icons.filter((icon) => icon.includes(searchBox)).slice(0, 15) : [];
|
||||
</script>
|
||||
|
||||
<InputGroup class="m-1">
|
||||
<ButtonDropdown>
|
||||
<Tooltip target={iconButton} placement="top">Change icon</Tooltip>
|
||||
<DropdownToggle color="secondary" caret bind:inner={iconButton}>
|
||||
<Icon name={preference.icon} alt="Current icon" />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
<p class="px-2">
|
||||
<Input type="text" placeholder="Search for icons" bind:value={searchBox} />
|
||||
</p>
|
||||
<DropdownItem divider />
|
||||
{#each filteredIcons as icon}
|
||||
<DropdownItem active={preference.icon === icon} on:click={() => (preference.icon = icon)}
|
||||
><Icon name={icon} alt="Icon: {icon}" /> {icon}</DropdownItem
|
||||
>
|
||||
{:else}
|
||||
<p class="px-2">Start typing to filter</p>
|
||||
{/each}
|
||||
</DropdownMenu>
|
||||
</ButtonDropdown>
|
||||
<input type="text" class="form-control" bind:value={preference.tooltip} />
|
||||
<Tooltip target={sizeButton} placement="top">Change text size</Tooltip>
|
||||
<ButtonDropdown>
|
||||
<DropdownToggle color="secondary" caret bind:inner={sizeButton}>
|
||||
<Icon name="type" alt="Text size" />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
<DropdownItem
|
||||
active={preference.size === PreferenceSize.Large}
|
||||
on:click={() => (preference.size = PreferenceSize.Large)}>Large</DropdownItem
|
||||
>
|
||||
<DropdownItem
|
||||
active={preference.size === PreferenceSize.Normal}
|
||||
on:click={() => (preference.size = PreferenceSize.Normal)}>Medium</DropdownItem
|
||||
>
|
||||
<DropdownItem
|
||||
active={preference.size === PreferenceSize.Small}
|
||||
on:click={() => (preference.size = PreferenceSize.Small)}>Small</DropdownItem
|
||||
>
|
||||
</DropdownMenu>
|
||||
</ButtonDropdown>
|
||||
<IconButton
|
||||
color="secondary"
|
||||
icon={preference.favourite ? "star-fill" : "star"}
|
||||
click={toggleFavourite}
|
||||
active={preference.favourite}
|
||||
tooltip="Treat like favourite"
|
||||
/>
|
||||
<IconButton
|
||||
color="secondary"
|
||||
icon="fonts"
|
||||
click={toggleMuted}
|
||||
active={preference.muted}
|
||||
tooltip="Show as muted text"
|
||||
/>
|
||||
<IconButton color="danger" icon="trash3" tooltip="Remove preference" click={remove} />
|
||||
</InputGroup>
|
Loading…
Add table
Add a link
Reference in a new issue