improve settings page
This commit is contained in:
parent
232575974a
commit
59fde80edb
6 changed files with 466 additions and 299 deletions
59
frontend/src/components/ChipBar.svelte
Normal file
59
frontend/src/components/ChipBar.svelte
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<script>
|
||||||
|
export let options = [];
|
||||||
|
export let selectedOptionId = null;
|
||||||
|
export let onSelect = (_) => {};
|
||||||
|
|
||||||
|
if (selectedOptionId) {
|
||||||
|
onSelect(selectedOptionId);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--foreground-color-2);
|
||||||
|
background-color: var(--background-color-2);
|
||||||
|
padding: 0.35em;
|
||||||
|
margin-top: var(--space-xs);
|
||||||
|
margin-right: var(--space-sm);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--background-color-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.selected {
|
||||||
|
color: var(--background-color-2);
|
||||||
|
background-color: var(--foreground-color-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.selected .material-icons-outlined {
|
||||||
|
color: var(--background-color-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons-outlined {
|
||||||
|
margin-right: var(--space-xxs);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#each options as option (option.id)}
|
||||||
|
<button class="button" class:selected={ selectedOptionId === option.id } on:click={ () => { selectedOptionId = option.id; onSelect(selectedOptionId); } }>
|
||||||
|
{#if option.icon}
|
||||||
|
<span class="material-icons-outlined">{ option.icon }</span>
|
||||||
|
{/if}
|
||||||
|
{ option.text }
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
23
frontend/src/components/StoredSwitch.svelte
Normal file
23
frontend/src/components/StoredSwitch.svelte
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<script>
|
||||||
|
import Switch from "./Switch.svelte";
|
||||||
|
export let store = null;
|
||||||
|
export let inverted = false;
|
||||||
|
|
||||||
|
const getValue = () => {
|
||||||
|
if (!store) return false;
|
||||||
|
|
||||||
|
if (inverted) {
|
||||||
|
return !store.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!store.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setValue = (value) => {
|
||||||
|
if (!store) return;
|
||||||
|
|
||||||
|
store.set(inverted ? !value : !!value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Switch checked={ getValue() } onUpdated={setValue}></Switch>
|
46
frontend/src/components/Switch.svelte
Normal file
46
frontend/src/components/Switch.svelte
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<script>
|
||||||
|
export let onUpdated = (_) => {};
|
||||||
|
export let checked = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.switch {
|
||||||
|
width: 52px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 999px;
|
||||||
|
margin: 4px;
|
||||||
|
background-color: var(--background-color-3);
|
||||||
|
transition: 0.15s;
|
||||||
|
contain: strict;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-slider {
|
||||||
|
position: absolute;
|
||||||
|
margin: 3px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: var(--foreground-color-3);
|
||||||
|
transition: 0.15s;
|
||||||
|
contain: strict;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-checkbox {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.switch.on {
|
||||||
|
background-color: var(--green-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch.on .switch-slider {
|
||||||
|
background-color: var(--foreground-color-2);
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="switch" class:on={ checked } on:click={() => {checked = !checked; onUpdated(checked); }}>
|
||||||
|
<div class="switch-slider"></div>
|
||||||
|
<input class="switch-checkbox" type="checkbox" tabIndex="0" />
|
||||||
|
</div>
|
|
@ -1,13 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import { overlayStore, userInfoStore, smallViewport, theme, doAnimations, OverlayType } from "../../stores";
|
import { overlayStore, userInfoStore, smallViewport, theme, doAnimations, OverlayType, sendTypingUpdatesItemStore } from "../../stores";
|
||||||
import { logOut } from "../../auth";
|
import { logOut } from "../../auth";
|
||||||
import { maybeModalFade, maybeModalScale } from "../../animations";
|
import { maybeModalFade, maybeModalScale } from "../../animations";
|
||||||
import request, { methods, remoteBlobUpload, remoteCall } from "../../request";
|
import request, { methods, remoteBlobUpload, remoteCall } from "../../request";
|
||||||
import { apiRoute, getItem } from "../../storage";
|
import { apiRoute, getItem } from "../../storage";
|
||||||
import UserView from "../UserView.svelte";
|
import UserView from "../UserView.svelte";
|
||||||
|
import ChipBar from "../ChipBar.svelte";
|
||||||
|
import Switch from "../Switch.svelte";
|
||||||
|
import StoredSwitch from "../StoredSwitch.svelte";
|
||||||
|
|
||||||
export let close = () => {};
|
export let close = () => {};
|
||||||
let avatarFileInput;
|
let avatarFileInput;
|
||||||
|
let selectedTab;
|
||||||
|
|
||||||
const doSuperuserPrompt = async () => {
|
const doSuperuserPrompt = async () => {
|
||||||
const { ok } = await request("POST", apiRoute("users/self/promote"), true);
|
const { ok } = await request("POST", apiRoute("users/self/promote"), true);
|
||||||
|
@ -72,6 +76,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<style>
|
<style>
|
||||||
.full-width {
|
.full-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -90,6 +95,22 @@
|
||||||
border: 1px solid var(--background-color-2);
|
border: 1px solid var(--background-color-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.switch-option-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: left;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: var(--space-md);
|
||||||
|
}
|
||||||
|
.switch-option-card .info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.switch-option-card .option-switch {
|
||||||
|
padding-left: var(--space-xs);
|
||||||
|
margin-left: auto;
|
||||||
|
justify-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.selection-option {
|
.selection-option {
|
||||||
border: 1px solid var(--background-color-2);
|
border: 1px solid var(--background-color-2);
|
||||||
}
|
}
|
||||||
|
@ -105,19 +126,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.large-settings {
|
.large-settings {
|
||||||
min-width: 540px;
|
width: 600px;
|
||||||
|
min-height: 425px;
|
||||||
padding-bottom: var(--space-xs);
|
padding-bottom: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-buttons {
|
.left-auto {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-modal {
|
.settings-modal {
|
||||||
background-color: var(--background-color-1);
|
background-color: var(--background-color-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-modal .modal-header {
|
||||||
|
padding-bottom: var(--space-xxs);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="modal-backdrop" transition:maybeModalFade on:click="{ close }">
|
<div class="modal-backdrop" transition:maybeModalFade on:click="{ close }">
|
||||||
<div class="modal settings-modal" class:large-settings="{ !$smallViewport }" transition:maybeModalScale on:click|stopPropagation>
|
<div class="modal settings-modal" class:large-settings="{ !$smallViewport }" transition:maybeModalScale on:click|stopPropagation>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
@ -125,31 +152,58 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<span class="input-label" on:click={ doDeveloper }>Account</span>
|
<ChipBar selectedOptionId="ACCOUNT" onSelect={ (tab) => selectedTab = tab } options={[
|
||||||
|
{ id: "ACCOUNT", text: "Account", icon: "person" },
|
||||||
|
{ id: "PRIVACY", text: "Privacy", icon: "lock" },
|
||||||
|
{ id: "APPEARANCE", text: "Appearance", icon: "palette" },
|
||||||
|
]}></ChipBar>
|
||||||
|
|
||||||
|
<div class="separator" />
|
||||||
|
|
||||||
|
{#if selectedTab === "ACCOUNT"}
|
||||||
<div class="settings-card full-width">
|
<div class="settings-card full-width">
|
||||||
<UserView user={$userInfoStore}></UserView>
|
<UserView user={$userInfoStore}></UserView>
|
||||||
<input type="file" style="display: none;" accept="image/png, image/jpeg, image/webp" name="avatar-upload" multiple={false} bind:this={avatarFileInput} on:change={onAvatarFileChange}>
|
<input type="file" style="display: none;" accept="image/png, image/jpeg, image/webp" name="avatar-upload" multiple={false} bind:this={avatarFileInput} on:change={onAvatarFileChange}>
|
||||||
<div class="account-buttons">
|
<div class="left-auto">
|
||||||
<button class="button" on:click="{ openAvatarInput }">Update Avatar</button>
|
<button class="button" on:click="{ openAvatarInput }">Update Avatar</button>
|
||||||
<button class="button button-danger" on:click="{ doLogout }">Log Out</button>
|
<button class="button button-danger" on:click="{ doLogout }">Log Out</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if selectedTab == "PRIVACY"}
|
||||||
<div class="separator" />
|
<div class="switch-option-card full-width">
|
||||||
|
<div class="info">
|
||||||
|
<span>Let others know when I'm typing</span>
|
||||||
|
<span class="text-fg-3 text-small">If this is enabled, other users will see an indicator while you're typing a message.</span>
|
||||||
|
</div>
|
||||||
|
<div class="option-switch">
|
||||||
|
<StoredSwitch store={ sendTypingUpdatesItemStore } />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="switch-option-card full-width">
|
||||||
|
<div class="info">
|
||||||
|
<span>Make Waffle work</span>
|
||||||
|
<span class="text-fg-3 text-small">Waffle needs to store data such as your messages, created channels, your username, your profile picture and more in order to work. If you'd like to stop this, you can delete your account.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if selectedTab === "APPEARANCE"}
|
||||||
<span class="input-label">Theme</span>
|
<span class="input-label">Theme</span>
|
||||||
<div class="horizontal-selections">
|
<div class="horizontal-selections">
|
||||||
<button class="button selection-option full-width selected" class:selected="{ $theme === "dark" }" on:click="{ () => theme.set('dark') }">Dark</button>
|
<button class="button selection-option full-width selected" class:selected="{ $theme === "dark" }" on:click="{ () => theme.set('dark') }">Dark</button>
|
||||||
<button class="button selection-option full-width" class:selected="{ $theme === "light" }" on:click="{ () => theme.set('light') }">Light</button>
|
<button class="button selection-option full-width" class:selected="{ $theme === "light" }" on:click="{ () => theme.set('light') }">Light</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="separator" />
|
<div class="switch-option-card full-width">
|
||||||
|
<div class="info">
|
||||||
<span class="input-label">Animations</span>
|
<span>Reduce animations</span>
|
||||||
<div class="horizontal-selections">
|
<span class="text-fg-3 text-small">Reduce the amount of animations and visual effects.</span>
|
||||||
<button class="button selection-option full-width" class:selected="{ $doAnimations === true }" on:click="{ () => doAnimations.set(true) }">Enabled</button>
|
|
||||||
<button class="button selection-option full-width selected" class:selected="{ $doAnimations === false }" on:click="{ () => doAnimations.set(false) }">Disabled</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="option-switch">
|
||||||
|
<StoredSwitch store={ doAnimations } inverted={ true } />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<span>Page not found: { selectedTab }</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -526,7 +526,7 @@ class TypingStore extends Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
async didInputKey() {
|
async didInputKey() {
|
||||||
if (!userInfoStore.value || !getItem("ui:online:sendTypingUpdates"))
|
if (!userInfoStore.value || !sendTypingUpdatesItemStore.value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.startedTyping(userInfoStore.value, selectedChannel.value.id, 6500);
|
this.startedTyping(userInfoStore.value, selectedChannel.value.id, 6500);
|
||||||
|
@ -839,6 +839,7 @@ export const showPresenceSidebar = new Store(false, "showPresenceSidebar");
|
||||||
export const smallViewport = new Store(false, "smallViewport");
|
export const smallViewport = new Store(false, "smallViewport");
|
||||||
export const showChannelView = new Store(true, "showChannelView");
|
export const showChannelView = new Store(true, "showChannelView");
|
||||||
export const theme = new StorageItemStore("ui:theme");
|
export const theme = new StorageItemStore("ui:theme");
|
||||||
|
export const sendTypingUpdatesItemStore = new StorageItemStore("ui:online:sendTypingUpdates");
|
||||||
export const doAnimations = new StorageItemStore("ui:doAnimations");
|
export const doAnimations = new StorageItemStore("ui:doAnimations");
|
||||||
export const communities = new CommunitiesStore();
|
export const communities = new CommunitiesStore();
|
||||||
export const gatewayStatus = new GatewayStatusStore();
|
export const gatewayStatus = new GatewayStatusStore();
|
||||||
|
|
Loading…
Reference in a new issue