feat(frontend): allow editing + using custom preferences

This commit is contained in:
Sam 2023-04-19 17:17:44 +02:00 committed by Gitea
parent 8bda5f9860
commit 9a80bb2e9b
11 changed files with 229 additions and 177 deletions

View File

@ -30,12 +30,19 @@ const defaultPreferences: CustomPreferences = {
favourite: false, favourite: false,
}, },
avoid: { avoid: {
icon: "people", icon: "hand-thumbs-down",
tooltip: "Avoid", tooltip: "Avoid",
size: PreferenceSize.Small, size: PreferenceSize.Small,
muted: true, muted: true,
favourite: false, favourite: false,
}, },
missing: {
icon: "question-lg",
tooltip: "Unknown (missing)",
size: PreferenceSize.Normal,
muted: false,
favourite: false,
},
}; };
export default defaultPreferences; export default defaultPreferences;

View File

@ -58,13 +58,13 @@ export interface Field {
export interface FieldEntry { export interface FieldEntry {
value: string; value: string;
status: WordStatus; status: string;
} }
export interface Pronoun { export interface Pronoun {
pronouns: string; pronouns: string;
display_text: string | null; display_text: string | null;
status: WordStatus; status: string;
} }
export enum WordStatus { export enum WordStatus {

View File

@ -13,7 +13,7 @@
let currentPreference: CustomPreference; let currentPreference: CustomPreference;
$: currentPreference = $: currentPreference =
status in mergedPreferences ? mergedPreferences[status] : defaultPreferences.okay; status in mergedPreferences ? mergedPreferences[status] : defaultPreferences.missing;
let iconElement: HTMLElement; let iconElement: HTMLElement;
</script> </script>

View File

@ -13,7 +13,7 @@
let currentPreference: CustomPreference; let currentPreference: CustomPreference;
$: currentPreference = $: currentPreference =
status in mergedPreferences ? mergedPreferences[status] : defaultPreferences.okay; status in mergedPreferences ? mergedPreferences[status] : defaultPreferences.missing;
let classes: string; let classes: string;
$: classes = setClasses(currentPreference); $: classes = setClasses(currentPreference);

View File

@ -1,10 +1,11 @@
<script lang="ts"> <script lang="ts">
import { WordStatus, type Field } from "$lib/api/entities"; import type { Field, CustomPreferences } from "$lib/api/entities";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";
import { Button, Input, InputGroup } from "sveltestrap"; import { Button, Input, InputGroup } from "sveltestrap";
import FieldEntry from "./FieldEntry.svelte"; import FieldEntry from "./FieldEntry.svelte";
export let field: Field; export let field: Field;
export let preferences: CustomPreferences;
export let deleteField: () => void; export let deleteField: () => void;
export let moveField: (up: boolean) => void; export let moveField: (up: boolean) => void;
@ -13,7 +14,7 @@
const addEntry = (event: Event) => { const addEntry = (event: Event) => {
event.preventDefault(); event.preventDefault();
field.entries = [...field.entries, { value: newEntry, status: WordStatus.Okay }]; field.entries = [...field.entries, { value: newEntry, status: "missing" }];
newEntry = ""; newEntry = "";
}; };
@ -57,6 +58,7 @@
<FieldEntry <FieldEntry
bind:value={field.entries[index].value} bind:value={field.entries[index].value}
bind:status={field.entries[index].status} bind:status={field.entries[index].status}
{preferences}
moveUp={() => moveEntry(index, true)} moveUp={() => moveEntry(index, true)}
moveDown={() => moveEntry(index, false)} moveDown={() => moveEntry(index, false)}
remove={() => removeEntry(index)} remove={() => removeEntry(index)}

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { WordStatus } from "$lib/api/entities"; import defaultPreferences from "$lib/api/default_preferences";
import type { CustomPreference, CustomPreferences } from "$lib/api/entities";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";
import { import {
ButtonDropdown, ButtonDropdown,
@ -11,46 +12,23 @@
} from "sveltestrap"; } from "sveltestrap";
export let value: string; export let value: string;
export let status: WordStatus; export let status: string;
export let preferences: CustomPreferences;
export let moveUp: () => void; export let moveUp: () => void;
export let moveDown: () => void; export let moveDown: () => void;
export let remove: () => void; export let remove: () => void;
let buttonElement: HTMLElement; let buttonElement: HTMLElement;
const iconFor = (wordStatus: WordStatus) => { let mergedPreferences: CustomPreferences;
switch (wordStatus) { $: mergedPreferences = Object.assign(defaultPreferences, preferences);
case WordStatus.Favourite:
return "heart-fill";
case WordStatus.Okay:
return "hand-thumbs-up";
case WordStatus.Jokingly:
return "emoji-laughing";
case WordStatus.FriendsOnly:
return "people";
case WordStatus.Avoid:
return "hand-thumbs-down";
default:
return "hand-thumbs-up";
}
};
const textFor = (wordStatus: WordStatus) => { let currentPreference: CustomPreference;
switch (wordStatus) { $: currentPreference =
case WordStatus.Favourite: status in mergedPreferences ? mergedPreferences[status] : defaultPreferences.missing;
return "Favourite";
case WordStatus.Okay: let preferenceIds: string[];
return "Okay"; $: preferenceIds = Object.keys(mergedPreferences).filter((s) => s !== "missing");
case WordStatus.Jokingly:
return "Jokingly";
case WordStatus.FriendsOnly:
return "Friends only";
case WordStatus.Avoid:
return "Avoid";
default:
return "Okay";
}
};
</script> </script>
<div class="input-group m-1"> <div class="input-group m-1">
@ -58,30 +36,17 @@
<IconButton icon="chevron-down" color="secondary" tooltip="Move name down" click={moveDown} /> <IconButton icon="chevron-down" color="secondary" tooltip="Move name down" click={moveDown} />
<input type="text" class="form-control" bind:value /> <input type="text" class="form-control" bind:value />
<ButtonDropdown> <ButtonDropdown>
<Tooltip target={buttonElement} placement="top">{textFor(status)}</Tooltip> <Tooltip target={buttonElement} placement="top">{currentPreference.tooltip}</Tooltip>
<DropdownToggle color="secondary" caret bind:inner={buttonElement}> <DropdownToggle color="secondary" caret bind:inner={buttonElement}>
<Icon name={iconFor(status)} /> <Icon name={currentPreference.icon} />
</DropdownToggle> </DropdownToggle>
<DropdownMenu> <DropdownMenu>
<DropdownItem {#each preferenceIds as id}
on:click={() => (status = WordStatus.Favourite)} <DropdownItem on:click={() => (status = id)} active={status === id}>
active={status === WordStatus.Favourite}>Favourite</DropdownItem <Icon name={mergedPreferences[id].icon} aria-hidden />
> {mergedPreferences[id].tooltip}
<DropdownItem on:click={() => (status = WordStatus.Okay)} active={status === WordStatus.Okay} </DropdownItem>
>Okay</DropdownItem {/each}
>
<DropdownItem
on:click={() => (status = WordStatus.Jokingly)}
active={status === WordStatus.Jokingly}>Jokingly</DropdownItem
>
<DropdownItem
on:click={() => (status = WordStatus.FriendsOnly)}
active={status === WordStatus.FriendsOnly}>Friends only</DropdownItem
>
<DropdownItem
on:click={() => (status = WordStatus.Avoid)}
active={status === WordStatus.Avoid}>Avoid</DropdownItem
>
</DropdownMenu> </DropdownMenu>
</ButtonDropdown> </ButtonDropdown>
<IconButton color="danger" icon="trash3" tooltip="Remove name" click={remove} /> <IconButton color="danger" icon="trash3" tooltip="Remove name" click={remove} />

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { WordStatus, type Pronoun, pronounDisplay } from "$lib/api/entities"; import defaultPreferences from "$lib/api/default_preferences";
import type { Pronoun, CustomPreference, CustomPreferences } from "$lib/api/entities";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";
import { import {
Button, Button,
@ -15,6 +16,7 @@
} from "sveltestrap"; } from "sveltestrap";
export let pronoun: Pronoun; export let pronoun: Pronoun;
export let preferences: CustomPreferences;
export let moveUp: () => void; export let moveUp: () => void;
export let moveDown: () => void; export let moveDown: () => void;
export let remove: () => void; export let remove: () => void;
@ -23,39 +25,17 @@
let displayOpen = false; let displayOpen = false;
const toggleDisplay = () => (displayOpen = !displayOpen); const toggleDisplay = () => (displayOpen = !displayOpen);
const iconFor = (wordStatus: WordStatus) => { let mergedPreferences: CustomPreferences;
switch (wordStatus) { $: mergedPreferences = Object.assign(defaultPreferences, preferences);
case WordStatus.Favourite:
return "heart-fill";
case WordStatus.Okay:
return "hand-thumbs-up";
case WordStatus.Jokingly:
return "emoji-laughing";
case WordStatus.FriendsOnly:
return "people";
case WordStatus.Avoid:
return "hand-thumbs-down";
default:
return "hand-thumbs-up";
}
};
const textFor = (wordStatus: WordStatus) => { let currentPreference: CustomPreference;
switch (wordStatus) { $: currentPreference =
case WordStatus.Favourite: pronoun.status in mergedPreferences
return "Favourite"; ? mergedPreferences[pronoun.status]
case WordStatus.Okay: : defaultPreferences.missing;
return "Okay";
case WordStatus.Jokingly: let preferenceIds: string[];
return "Jokingly"; $: preferenceIds = Object.keys(mergedPreferences).filter((s) => s !== "missing");
case WordStatus.FriendsOnly:
return "Friends only";
case WordStatus.Avoid:
return "Avoid";
default:
return "Okay";
}
};
</script> </script>
<div class="input-group m-1"> <div class="input-group m-1">
@ -75,31 +55,17 @@
click={toggleDisplay} click={toggleDisplay}
/> />
<ButtonDropdown> <ButtonDropdown>
<Tooltip target={buttonElement} placement="top">{textFor(pronoun.status)}</Tooltip> <Tooltip target={buttonElement} placement="top">{currentPreference.tooltip}</Tooltip>
<DropdownToggle color="secondary" caret bind:inner={buttonElement}> <DropdownToggle color="secondary" caret bind:inner={buttonElement}>
<Icon name={iconFor(pronoun.status)} /> <Icon name={currentPreference.icon} />
</DropdownToggle> </DropdownToggle>
<DropdownMenu> <DropdownMenu>
<DropdownItem {#each preferenceIds as id}
on:click={() => (pronoun.status = WordStatus.Favourite)} <DropdownItem on:click={() => (pronoun.status = id)} active={pronoun.status === id}>
active={pronoun.status === WordStatus.Favourite}>Favourite</DropdownItem <Icon name={mergedPreferences[id].icon} aria-hidden />
> {mergedPreferences[id].tooltip}
<DropdownItem </DropdownItem>
on:click={() => (pronoun.status = WordStatus.Okay)} {/each}
active={pronoun.status === WordStatus.Okay}>Okay</DropdownItem
>
<DropdownItem
on:click={() => (pronoun.status = WordStatus.Jokingly)}
active={pronoun.status === WordStatus.Jokingly}>Jokingly</DropdownItem
>
<DropdownItem
on:click={() => (pronoun.status = WordStatus.FriendsOnly)}
active={pronoun.status === WordStatus.FriendsOnly}>Friends only</DropdownItem
>
<DropdownItem
on:click={() => (pronoun.status = WordStatus.Avoid)}
active={pronoun.status === WordStatus.Avoid}>Avoid</DropdownItem
>
</DropdownMenu> </DropdownMenu>
</ButtonDropdown> </ButtonDropdown>
<IconButton color="danger" icon="trash3" tooltip="Remove pronouns" click={remove} /> <IconButton color="danger" icon="trash3" tooltip="Remove pronouns" click={remove} />

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { WordStatus } from "$lib/api/entities"; import defaultPreferences from "$lib/api/default_preferences";
import type { CustomPreference, CustomPreferences } from "$lib/api/entities";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";
import { import {
ButtonDropdown, ButtonDropdown,
@ -11,46 +12,23 @@
} from "sveltestrap"; } from "sveltestrap";
export let value: string; export let value: string;
export let status: WordStatus; export let status: string;
export let preferences: CustomPreferences;
export let moveUp: () => void; export let moveUp: () => void;
export let moveDown: () => void; export let moveDown: () => void;
export let remove: () => void; export let remove: () => void;
let buttonElement: HTMLElement; let buttonElement: HTMLElement;
const iconFor = (wordStatus: WordStatus) => { let mergedPreferences: CustomPreferences;
switch (wordStatus) { $: mergedPreferences = Object.assign(defaultPreferences, preferences);
case WordStatus.Favourite:
return "heart-fill";
case WordStatus.Okay:
return "hand-thumbs-up";
case WordStatus.Jokingly:
return "emoji-laughing";
case WordStatus.FriendsOnly:
return "people";
case WordStatus.Avoid:
return "hand-thumbs-down";
default:
return "hand-thumbs-up";
}
};
const textFor = (wordStatus: WordStatus) => { let currentPreference: CustomPreference;
switch (wordStatus) { $: currentPreference =
case WordStatus.Favourite: status in mergedPreferences ? mergedPreferences[status] : defaultPreferences.missing;
return "Favourite";
case WordStatus.Okay: let preferenceIds: string[];
return "Okay"; $: preferenceIds = Object.keys(mergedPreferences).filter((s) => s !== "missing");
case WordStatus.Jokingly:
return "Jokingly";
case WordStatus.FriendsOnly:
return "Friends only";
case WordStatus.Avoid:
return "Avoid";
default:
return "Okay";
}
};
</script> </script>
<div class="input-group m-1"> <div class="input-group m-1">
@ -58,30 +36,17 @@
<IconButton icon="chevron-down" color="secondary" tooltip="Move entry down" click={moveDown} /> <IconButton icon="chevron-down" color="secondary" tooltip="Move entry down" click={moveDown} />
<input type="text" class="form-control" bind:value /> <input type="text" class="form-control" bind:value />
<ButtonDropdown> <ButtonDropdown>
<Tooltip target={buttonElement} placement="top">{textFor(status)}</Tooltip> <Tooltip target={buttonElement} placement="top">{currentPreference.tooltip}</Tooltip>
<DropdownToggle color="secondary" caret bind:inner={buttonElement}> <DropdownToggle color="secondary" caret bind:inner={buttonElement}>
<Icon name={iconFor(status)} /> <Icon name={currentPreference.icon} />
</DropdownToggle> </DropdownToggle>
<DropdownMenu> <DropdownMenu>
<DropdownItem {#each preferenceIds as id}
on:click={() => (status = WordStatus.Favourite)} <DropdownItem on:click={() => (status = id)} active={status === id}>
active={status === WordStatus.Favourite}>Favourite</DropdownItem <Icon name={mergedPreferences[id].icon} aria-hidden />
> {mergedPreferences[id].tooltip}
<DropdownItem on:click={() => (status = WordStatus.Okay)} active={status === WordStatus.Okay} </DropdownItem>
>Okay</DropdownItem {/each}
>
<DropdownItem
on:click={() => (status = WordStatus.Jokingly)}
active={status === WordStatus.Jokingly}>Jokingly</DropdownItem
>
<DropdownItem
on:click={() => (status = WordStatus.FriendsOnly)}
active={status === WordStatus.FriendsOnly}>Friends only</DropdownItem
>
<DropdownItem
on:click={() => (status = WordStatus.Avoid)}
active={status === WordStatus.Avoid}>Avoid</DropdownItem
>
</DropdownMenu> </DropdownMenu>
</ButtonDropdown> </ButtonDropdown>
<IconButton color="danger" icon="trash3" tooltip="Remove entry" click={remove} /> <IconButton color="danger" icon="trash3" tooltip="Remove entry" click={remove} />

View File

@ -153,10 +153,9 @@
if (list[0].size > MAX_AVATAR_BYTES) { if (list[0].size > MAX_AVATAR_BYTES) {
addToast({ addToast({
header: "Avatar too large", header: "Avatar too large",
body: body: `This avatar is too large, please resize it (maximum is ${prettyBytes(
`This avatar is too large, please resize it (maximum is ${prettyBytes( MAX_AVATAR_BYTES,
MAX_AVATAR_BYTES, )}, the file you tried to upload is ${prettyBytes(list[0].size)})`,
)}, the file you tried to upload is ${prettyBytes(list[0].size)})`,
}); });
return null; return null;
} }
@ -440,6 +439,7 @@
<EditableName <EditableName
bind:value={names[index].value} bind:value={names[index].value}
bind:status={names[index].status} bind:status={names[index].status}
preferences={data.user.custom_preferences}
moveUp={() => moveName(index, true)} moveUp={() => moveName(index, true)}
moveDown={() => moveName(index, false)} moveDown={() => moveName(index, false)}
remove={() => removeName(index)} remove={() => removeName(index)}
@ -479,6 +479,7 @@
{#each pronouns as _, index} {#each pronouns as _, index}
<EditablePronouns <EditablePronouns
bind:pronoun={pronouns[index]} bind:pronoun={pronouns[index]}
preferences={data.user.custom_preferences}
moveUp={() => movePronoun(index, true)} moveUp={() => movePronoun(index, true)}
moveDown={() => movePronoun(index, false)} moveDown={() => movePronoun(index, false)}
remove={() => removePronoun(index)} remove={() => removePronoun(index)}
@ -520,6 +521,7 @@
{#each fields as _, index} {#each fields as _, index}
<EditableField <EditableField
bind:field={fields[index]} bind:field={fields[index]}
preferences={data.user.custom_preferences}
deleteField={() => removeField(index)} deleteField={() => removeField(index)}
moveField={(up) => moveField(index, up)} moveField={(up) => moveField(index, up)}
/> />

View File

@ -8,6 +8,8 @@
type FieldEntry, type FieldEntry,
type MeUser, type MeUser,
type Pronoun, type Pronoun,
PreferenceSize,
type CustomPreferences,
} from "$lib/api/entities"; } from "$lib/api/entities";
import FallbackImage from "$lib/components/FallbackImage.svelte"; import FallbackImage from "$lib/components/FallbackImage.svelte";
import { userStore } from "$lib/store"; import { userStore } from "$lib/store";
@ -37,6 +39,7 @@
import { charCount, renderMarkdown } from "$lib/utils"; import { charCount, renderMarkdown } from "$lib/utils";
import MarkdownHelp from "../MarkdownHelp.svelte"; import MarkdownHelp from "../MarkdownHelp.svelte";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import CustomPreference from "./CustomPreference.svelte";
const MAX_AVATAR_BYTES = 1_000_000; const MAX_AVATAR_BYTES = 1_000_000;
@ -52,6 +55,7 @@
let pronouns: Pronoun[] = window.structuredClone(data.user.pronouns); let pronouns: Pronoun[] = window.structuredClone(data.user.pronouns);
let fields: Field[] = window.structuredClone(data.user.fields); let fields: Field[] = window.structuredClone(data.user.fields);
let list_private = data.user.list_private; let list_private = data.user.list_private;
let custom_preferences = window.structuredClone(data.user.custom_preferences);
let avatar: string | null; let avatar: string | null;
let avatar_files: FileList | null; let avatar_files: FileList | null;
@ -60,6 +64,9 @@
let newPronouns = ""; let newPronouns = "";
let newLink = ""; let newLink = "";
let preferenceIds: string[];
$: preferenceIds = Object.keys(custom_preferences);
let modified = false; let modified = false;
$: modified = isModified( $: modified = isModified(
@ -73,6 +80,7 @@
avatar, avatar,
member_title, member_title,
list_private, list_private,
custom_preferences,
); );
$: getAvatar(avatar_files).then((b64) => (avatar = b64)); $: getAvatar(avatar_files).then((b64) => (avatar = b64));
@ -87,6 +95,7 @@
avatar: string | null, avatar: string | null,
member_title: string, member_title: string,
list_private: boolean, list_private: boolean,
custom_preferences: CustomPreferences,
) => { ) => {
if (bio !== (user.bio || "")) return true; if (bio !== (user.bio || "")) return true;
if (display_name !== (user.display_name || "")) return true; if (display_name !== (user.display_name || "")) return true;
@ -95,6 +104,7 @@
if (!fieldsEqual(fields, user.fields)) return true; if (!fieldsEqual(fields, user.fields)) return true;
if (!namesEqual(names, user.names)) return true; if (!namesEqual(names, user.names)) return true;
if (!pronounsEqual(pronouns, user.pronouns)) return true; if (!pronounsEqual(pronouns, user.pronouns)) return true;
if (!customPreferencesEqual(custom_preferences, user.custom_preferences)) return true;
if (avatar !== null) return true; if (avatar !== null) return true;
if (list_private !== user.list_private) return true; if (list_private !== user.list_private) return true;
@ -136,6 +146,21 @@
return arr1.every((_, i) => arr1[i] === arr2[i]); 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) => { const getAvatar = async (list: FileList | null) => {
if (!list || list.length === 0) return null; if (!list || list.length === 0) return null;
if (list[0].size > MAX_AVATAR_BYTES) { if (list[0].size > MAX_AVATAR_BYTES) {
@ -226,6 +251,19 @@
newLink = ""; 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) => { const removeName = (index: number) => {
names.splice(index, 1); names.splice(index, 1);
names = [...names]; names = [...names];
@ -246,6 +284,11 @@
fields = [...fields]; fields = [...fields];
}; };
const removePreference = (id: string) => {
delete custom_preferences[id];
custom_preferences = custom_preferences;
};
const updateUser = async () => { const updateUser = async () => {
const toastId = addToast({ const toastId = addToast({
header: "Saving changes", header: "Saving changes",
@ -264,6 +307,7 @@
fields, fields,
member_title, member_title,
list_private, list_private,
custom_preferences,
}); });
data.user = resp; data.user = resp;
@ -367,6 +411,7 @@
<EditableName <EditableName
bind:value={names[index].value} bind:value={names[index].value}
bind:status={names[index].status} bind:status={names[index].status}
preferences={data.user.custom_preferences}
moveUp={() => moveName(index, true)} moveUp={() => moveName(index, true)}
moveDown={() => moveName(index, false)} moveDown={() => moveName(index, false)}
remove={() => removeName(index)} remove={() => removeName(index)}
@ -447,6 +492,7 @@
{#each fields as _, index} {#each fields as _, index}
<EditableField <EditableField
bind:field={fields[index]} bind:field={fields[index]}
preferences={data.user.custom_preferences}
deleteField={() => removeField(index)} deleteField={() => removeField(index)}
moveField={(up) => moveField(index, up)} moveField={(up) => moveField(index, up)}
/> />
@ -478,7 +524,7 @@
</form> </form>
</div> </div>
</TabPane> </TabPane>
<TabPane tabId="other" tab="Other"> <TabPane tabId="other" tab="Preferences & other">
<div class="row mt-3"> <div class="row mt-3">
<div class="col-md"> <div class="col-md">
<FormGroup floating label={'"Members" header text'}> <FormGroup floating label={'"Members" header text'}>
@ -510,5 +556,18 @@
</p> </p>
</div> </div>
</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> </TabPane>
</TabContent> </TabContent>

View 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>