forked from mirrors/pronouns.cc
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
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
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…
Reference in a new issue