feat(sidebar-avatar): Creat SidebarIndicator & Refactor Sidebar UI (#44)

This PR introduces several modifications to enhance the UI and code
structure. Here are the details:

1. Added SidebarIndicator to improve the sidebar navigation experience.

2. Removed the BarIcon components from the Character at Bar. They have
been replaced with the SidebarAvatar component. This change aims to
refactor the BarIcon into UI components (TODO: Implement the UI
component refactor and sidebar state flow)

3. Update shape and color of NewCharButton 

4. ~~Refactored the getCharImage function by removing unused code,
improving code cleanliness and maintainability.~~

* Reverted. Because, it's in use in so many places that it's probably
too big a scope to cover in this PR.

These changes aim to improve the UI and code structure. The Avatar UI
has been updated to a circular shape, but it can be easily reverted to a
rectangular shape based on your preference. Your feedback on this aspect
would be greatly appreciated.

Thank you for your attention to this pull request. Please let me know if
you have any questions, suggestions, or concerns.

Thank you!

## New Sidebar (Indicator, NewCharButton) Demo


https://github.com/kwaroran/RisuAI/assets/34825352/6f709aed-3330-4c68-b2e6-7024607faaf8
This commit is contained in:
kwaroran
2023-05-13 03:02:38 +09:00
committed by GitHub
5 changed files with 370 additions and 208 deletions

View File

@@ -1,13 +1,16 @@
<!-- TODO: REMOVE AND REFACTOR TO BASE BUTTON UI COMPONENT -->
<script lang="ts">
export let onClick = () => {};
export let additionalStyle: string | Promise<string> = "";
</script>
{#await additionalStyle} {#await additionalStyle}
<button on:click={onClick} class="ico"><slot /></button> <button on:click={onClick} class="ico"><slot /></button>
{:then as} {:then as}
<button on:click={onClick} class="ico" style={as}><slot /></button> <button on:click={onClick} class="ico" style={as}><slot /></button>
{/await} {/await}
<script lang="ts">
export let onClick = () => {}
export let additionalStyle:string|Promise<string> = ''
</script>
<style> <style>
.ico { .ico {
cursor: pointer; cursor: pointer;
@@ -15,13 +18,16 @@
height: 3.5rem; height: 3.5rem;
width: 3.5rem; width: 3.5rem;
min-height: 3.5rem; min-height: 3.5rem;
margin-top: 0.5rem;
--tw-shadow-color: 0, 0, 0; --tw-shadow-color: 0, 0, 0;
--tw-shadow: 0 10px 15px -3px rgba(var(--tw-shadow-color), 0.1), 0 4px 6px -2px rgba(var(--tw-shadow-color), 0.05); --tw-shadow: 0 10px 15px -3px rgba(var(--tw-shadow-color), 0.1),
-webkit-box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 0 4px 6px -2px rgba(var(--tw-shadow-color), 0.05);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -webkit-box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgba(107, 114, 128, var(--tw-bg-opacity)); display: flex; background-color: rgba(107, 114, 128, var(--tw-bg-opacity));
display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transition-property: background-color, border-color, color, fill, stroke; transition-property: background-color, border-color, color, fill, stroke;

View File

@@ -1,144 +1,232 @@
<script lang="ts"> <script lang="ts">
import { CharEmotion, SizeStore, selectedCharID, settingsOpen, sideBarStore } from "../../ts/stores"; import {
CharEmotion,
SizeStore,
selectedCharID,
settingsOpen,
sideBarStore,
} from "../../ts/stores";
import { DataBase } from "../../ts/database"; import { DataBase } from "../../ts/database";
import BarIcon from "./BarIcon.svelte"; import BarIcon from "./BarIcon.svelte";
import { Plus, User, X, Settings, Users, Edit3Icon, ArrowUp, ArrowDown, ListIcon, LayoutGridIcon, PlusIcon} from 'lucide-svelte' import SidebarIndicator from "./SidebarIndicator.svelte";
import { characterFormatUpdate, createNewCharacter, createNewGroup, getCharImage } from "../../ts/characters"; import {
import {importCharacter} from 'src/ts/characterCards' Plus,
import SettingsDom from './Settings.svelte' User,
X,
Settings,
Users,
Edit3Icon,
ArrowUp,
ArrowDown,
ListIcon,
LayoutGridIcon,
PlusIcon,
} from "lucide-svelte";
import {
characterFormatUpdate,
createNewCharacter,
createNewGroup,
getCharImage,
} from "../../ts/characters";
import { importCharacter } from "src/ts/characterCards";
import SettingsDom from "./Settings.svelte";
import CharConfig from "./CharConfig.svelte"; import CharConfig from "./CharConfig.svelte";
import { language } from "../../lang"; import { language } from "../../lang";
import Botpreset from "../Others/botpreset.svelte"; import Botpreset from "../Others/botpreset.svelte";
import { onDestroy } from "svelte"; import { onDestroy } from "svelte";
import {isEqual} from 'lodash' import { isEqual } from "lodash";
let openPresetList =false import SidebarAvatar from "./SidebarAvatar.svelte";
let sideBarMode = 0 import BaseRoundedButton from "../UI/BaseRoundedButton.svelte";
let editMode = false let openPresetList = false;
let menuMode = 0 let sideBarMode = 0;
export let openGrid = () => {} let editMode = false;
let menuMode = 0;
export let openGrid = () => {};
function createScratch() { function createScratch() {
reseter(); reseter();
const cid = createNewCharacter() const cid = createNewCharacter();
selectedCharID.set(-1) selectedCharID.set(-1);
} }
function createGroup() { function createGroup() {
reseter(); reseter();
const cid = createNewGroup() const cid = createNewGroup();
selectedCharID.set(-1) selectedCharID.set(-1);
} }
async function createImport() { async function createImport() {
reseter(); reseter();
const cid = await importCharacter() const cid = await importCharacter();
selectedCharID.set(-1) selectedCharID.set(-1);
} }
function changeChar(index: number) { function changeChar(index: number) {
reseter(); reseter();
characterFormatUpdate(index) characterFormatUpdate(index);
selectedCharID.set(index) selectedCharID.set(index);
} }
function reseter() { function reseter() {
menuMode = 0; menuMode = 0;
sideBarMode = 0; sideBarMode = 0;
editMode = false editMode = false;
settingsOpen.set(false) settingsOpen.set(false);
CharEmotion.set({}) CharEmotion.set({});
} }
let charImages:string[] = [] let charImages: string[] = [];
const unsub = DataBase.subscribe((db) => { const unsub = DataBase.subscribe((db) => {
let newCharImages:string[] = [] let newCharImages: string[] = [];
for (const cha of db.characters) { for (const cha of db.characters) {
newCharImages.push(cha.image ?? '') newCharImages.push(cha.image ?? "");
} }
if (!isEqual(charImages, newCharImages)) { if (!isEqual(charImages, newCharImages)) {
charImages = newCharImages charImages = newCharImages;
} }
}) });
onDestroy(unsub)
onDestroy(unsub);
</script> </script>
<div class="w-20 flex flex-col bg-bgcolor text-white items-center overflow-y-scroll h-full shadow-lg min-w-20 overflow-x-hidden"
class:editMode={editMode}> <div
<button class="bg-gray-500 w-14 min-w-14 flex justify-center h-8 items-center rounded-b-md cursor-pointer hover:bg-green-500 transition-colors absolute top-0" on:click={() => { class="flex h-full w-20 min-w-20 flex-col items-center overflow-x-hidden overflow-y-scroll bg-bgcolor text-white shadow-lg gap-2"
menuMode = 1 - menuMode class:editMode
}}><ListIcon/></button> >
<div class="w-14 min-w-14 h-8 min-h-8 bg-transparent"></div> <button
class="absolute top-0 flex h-8 w-14 min-w-14 cursor-pointer items-center justify-center rounded-b-md bg-gray-500 transition-colors hover:bg-green-500"
on:click={() => {
menuMode = 1 - menuMode;
}}><ListIcon /></button
>
<div class="h-8 min-h-8 w-14 min-w-14 bg-transparent" />
{#if menuMode === 0} {#if menuMode === 0}
{#each charImages as charimg, i} {#each charImages as charimg, i}
<div class="flex items-center"> <div class="group relative flex items-center px-2">
{#if charimg !== ''} <SidebarIndicator
<BarIcon onClick={() => {changeChar(i)}} additionalStyle={getCharImage($DataBase.characters[i].image, 'css')}> isActive={$selectedCharID === i && sideBarMode !== 1}
</BarIcon> />
{#if charimg !== ""}
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div
on:click={() => {
changeChar(i);
}}
on:keydown={(e) => {
if (e.key === "Enter") {
changeChar(i);
}
}}
tabindex="0"
>
{#await getCharImage($DataBase.characters[i].image, "plain") then img}
<SidebarAvatar src={img} size="56" />
{:catch}
<SidebarAvatar size="56" src="https://via.placeholder.com/150" />
{/await}
</div>
{:else} {:else}
<BarIcon onClick={() => {changeChar(i)}} additionalStyle={i === $selectedCharID ? 'background:#44475a' : ''}> <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div
</BarIcon> on:click={() => {
changeChar(i);
}}
on:keydown={(e) => {
if (e.key === "Enter") {
changeChar(i);
}
}}
tabindex="0"
>
<SidebarAvatar size="56" src="https://via.placeholder.com/150" />
</div>
{/if} {/if}
{#if editMode} {#if editMode}
<div class="flex flex-col mt-2"> <div class="mt-2 flex flex-col">
<button on:click={() => { <button
let chars = $DataBase.characters on:click={() => {
let chars = $DataBase.characters;
if (chars[i - 1]) { if (chars[i - 1]) {
const currentchar = chars[i] const currentchar = chars[i];
chars[i] = chars[i-1] chars[i] = chars[i - 1];
chars[i-1] = currentchar chars[i - 1] = currentchar;
$DataBase.characters = chars $DataBase.characters = chars;
} }
}}> }}
>
<ArrowUp size={20} /> <ArrowUp size={20} />
</button> </button>
<button on:click={() => { <button
let chars = $DataBase.characters on:click={() => {
let chars = $DataBase.characters;
if (chars[i + 1]) { if (chars[i + 1]) {
const currentchar = chars[i] const currentchar = chars[i];
chars[i] = chars[i+1] chars[i] = chars[i + 1];
chars[i+1] = currentchar chars[i + 1] = currentchar;
$DataBase.characters = chars $DataBase.characters = chars;
} }
}}> }}
>
<ArrowDown size={22} /> <ArrowDown size={22} />
</button> </button>
</div> </div>
{/if} {/if}
</div> </div>
{/each} {/each}
<BarIcon onClick={() => { <div class="flex flex-col items-center space-y-2 px-2">
<BaseRoundedButton
onClick={() => {
if (sideBarMode === 1) { if (sideBarMode === 1) {
reseter(); reseter();
sideBarMode = 0 sideBarMode = 0;
} } else {
else{
reseter(); reseter();
sideBarMode = 1 sideBarMode = 1;
} }
}}><PlusIcon/></BarIcon> }}
><svg viewBox="0 0 24 24" width="1.2em" height="1.2em"
><path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
></BaseRoundedButton
>
</div>
{:else} {:else}
<BarIcon onClick={() => { <BarIcon
onClick={() => {
if ($settingsOpen) { if ($settingsOpen) {
reseter(); reseter();
settingsOpen.set(false) settingsOpen.set(false);
} } else {
else{
reseter(); reseter();
settingsOpen.set(true) settingsOpen.set(true);
} }
}}><Settings/></BarIcon> }}><Settings /></BarIcon
<BarIcon onClick={() => { >
<BarIcon
onClick={() => {
reseter(); reseter();
openGrid() openGrid();
}}><LayoutGridIcon/></BarIcon> }}><LayoutGridIcon /></BarIcon
>
{/if} {/if}
</div> </div>
<div class="w-96 p-6 flex flex-col bg-darkbg text-gray-200 overflow-y-auto overflow-x-hidden setting-area" class:flex-grow={($SizeStore.w <= 1000)} class:minw96={($SizeStore.w > 1000)}> <div
<button class="flex w-full justify-end text-gray-200" on:click={() => {sideBarStore.set(false)}}> class="setting-area flex w-96 flex-col overflow-y-auto overflow-x-hidden bg-darkbg p-6 text-gray-200"
<button class="p-0 bg-transparent border-none text-gray-200"><X/></button> class:flex-grow={$SizeStore.w <= 1000}
class:minw96={$SizeStore.w > 1000}
>
<button
class="flex w-full justify-end text-gray-200"
on:click={() => {
sideBarStore.set(false);
}}
>
<button class="border-none bg-transparent p-0 text-gray-200"><X /></button>
</button> </button>
{#if sideBarMode === 0} {#if sideBarMode === 0}
{#if $selectedCharID < 0 || $settingsOpen} {#if $selectedCharID < 0 || $settingsOpen}
@@ -147,31 +235,46 @@
<CharConfig /> <CharConfig />
{/if} {/if}
{:else if sideBarMode === 1} {:else if sideBarMode === 1}
<h2 class="title font-bold text-xl mt-2">Create</h2> <h2 class="title mt-2 text-xl font-bold">Create</h2>
<button <button
on:click={createScratch} on:click={createScratch}
class="drop-shadow-lg p-5 border-borderc border-solid mt-2 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-lg"> class="ml-2 mr-2 mt-2 flex items-center justify-center border-1 border-solid border-borderc p-5 text-lg drop-shadow-lg hover:bg-selected"
>
{language.createfromScratch} {language.createfromScratch}
</button> </button>
<button <button
on:click={createImport} on:click={createImport}
class="drop-shadow-lg p-5 border-borderc border-solid mt-2 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-lg"> class="ml-2 mr-2 mt-2 flex items-center justify-center border-1 border-solid border-borderc p-5 text-lg drop-shadow-lg hover:bg-selected"
>
{language.importCharacter} {language.importCharacter}
</button> </button>
<button <button
on:click={createGroup} on:click={createGroup}
class="drop-shadow-lg p-3 border-borderc border-solid mt-2 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected"> class="ml-2 mr-2 mt-2 flex items-center justify-center border-1 border-solid border-borderc p-3 drop-shadow-lg hover:bg-selected"
>
{language.createGroup} {language.createGroup}
</button> </button>
<h2 class="title font-bold text-xl mt-4">Edit</h2> <h2 class="title mt-4 text-xl font-bold">Edit</h2>
<button <button
on:click={() => {editMode = !editMode;$selectedCharID = -1}} on:click={() => {
class="drop-shadow-lg p-3 border-borderc border-solid mt-2 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected"> editMode = !editMode;
$selectedCharID = -1;
}}
class="ml-2 mr-2 mt-2 flex items-center justify-center border-1 border-solid border-borderc p-3 drop-shadow-lg hover:bg-selected"
>
{language.editOrder} {language.editOrder}
</button> </button>
{/if} {/if}
</div> </div>
{#if openPresetList}
<Botpreset
close={() => {
openPresetList = false;
}}
/>
{/if}
<style> <style>
.minw96 { .minw96 {
min-width: 24rem; /* 384px */ min-width: 24rem; /* 384px */
@@ -183,7 +286,3 @@
min-width: 6rem; min-width: 6rem;
} }
</style> </style>
{#if openPresetList}
<Botpreset close={() => {openPresetList = false}}/>
{/if}

View File

@@ -0,0 +1,24 @@
<script>
export let src;
export let size = "22";
</script>
<span class="flex shrink-0 items-center justify-center">
{#if src}
<img
{src}
class="bg-skin-border sidebar-avatar rounded-full object-cover"
style:width={size + "px"}
style:height={size + "px"}
style:minWidth={size + "px"}
alt="avatar"
/>
{:else}
<div
class="bg-skin-border sidebar-avatar rounded-full"
style:width={size + "px"}
style:height={size + "px"}
style:minWidth={size + "px"}
/>
{/if}
</span>

View File

@@ -0,0 +1,17 @@
<script lang="ts">
export let isActive: boolean;
</script>
<div
class="
group-hover:bg-white
absolute
left-[-4px]
h-[8px]
w-[8px]
rounded-full
transition-all
duration-300
{isActive ? 'bg-white !h-[20px]' : 'group-hover:h-[10px]'}
"
/>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
export let isDisabled: boolean = false;
export let onClick: () => void;
</script>
<button
disabled={isDisabled}
on:click={onClick}
class="flex h-[56px] w-[56px] cursor-pointer select-none items-center justify-center
transition-colors rounded-full
border border-gray-500 text-gray-300
hover:border-gray-300
{isDisabled ? '!cursor-not-allowed' : ''}"
>
<slot />
</button>