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,36 +1,42 @@
<!-- 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>
.ico {
cursor: pointer;
border-radius: 0.375rem;
height: 3.5rem;
width: 3.5rem;
min-height: 3.5rem;
margin-top: 0.5rem;
--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);
-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;
background-color: rgba(107, 114, 128, var(--tw-bg-opacity)); display: flex;
justify-content: center;
align-items: center;
transition-property: background-color, border-color, color, fill, stroke;
transition-duration: 150ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.ico:hover { <style>
--tw-bg-opacity: 1; .ico {
background-color: rgba(16, 185, 129, var(--tw-bg-opacity)); cursor: pointer;
} border-radius: 0.375rem;
</style> height: 3.5rem;
width: 3.5rem;
min-height: 3.5rem;
--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);
-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;
background-color: rgba(107, 114, 128, var(--tw-bg-opacity));
display: flex;
justify-content: center;
align-items: center;
transition-property: background-color, border-color, color, fill, stroke;
transition-duration: 150ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.ico:hover {
--tw-bg-opacity: 1;
background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
}
</style>

View File

@@ -1,189 +1,288 @@
<script lang="ts"> <script lang="ts">
import { CharEmotion, SizeStore, selectedCharID, settingsOpen, sideBarStore } from "../../ts/stores"; import {
import { DataBase } from "../../ts/database"; CharEmotion,
import BarIcon from "./BarIcon.svelte"; SizeStore,
import { Plus, User, X, Settings, Users, Edit3Icon, ArrowUp, ArrowDown, ListIcon, LayoutGridIcon, PlusIcon} from 'lucide-svelte' selectedCharID,
import { characterFormatUpdate, createNewCharacter, createNewGroup, getCharImage } from "../../ts/characters"; settingsOpen,
import {importCharacter} from 'src/ts/characterCards' sideBarStore,
import SettingsDom from './Settings.svelte' } from "../../ts/stores";
import CharConfig from "./CharConfig.svelte"; import { DataBase } from "../../ts/database";
import { language } from "../../lang"; import BarIcon from "./BarIcon.svelte";
import Botpreset from "../Others/botpreset.svelte"; import SidebarIndicator from "./SidebarIndicator.svelte";
import { onDestroy } from "svelte"; import {
import {isEqual} from 'lodash' Plus,
let openPresetList =false User,
let sideBarMode = 0 X,
let editMode = false Settings,
let menuMode = 0 Users,
export let openGrid = () => {} 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 { language } from "../../lang";
import Botpreset from "../Others/botpreset.svelte";
import { onDestroy } from "svelte";
import { isEqual } from "lodash";
import SidebarAvatar from "./SidebarAvatar.svelte";
import BaseRoundedButton from "../UI/BaseRoundedButton.svelte";
let openPresetList = false;
let sideBarMode = 0;
let editMode = false;
let menuMode = 0;
export let openGrid = () => {};
function createScratch() {
reseter();
const cid = createNewCharacter();
selectedCharID.set(-1);
}
function createGroup() {
reseter();
const cid = createNewGroup();
selectedCharID.set(-1);
}
async function createImport() {
reseter();
const cid = await importCharacter();
selectedCharID.set(-1);
}
function createScratch(){ function changeChar(index: number) {
reseter(); reseter();
const cid = createNewCharacter() characterFormatUpdate(index);
selectedCharID.set(-1) selectedCharID.set(index);
}
function reseter() {
menuMode = 0;
sideBarMode = 0;
editMode = false;
settingsOpen.set(false);
CharEmotion.set({});
}
let charImages: string[] = [];
const unsub = DataBase.subscribe((db) => {
let newCharImages: string[] = [];
for (const cha of db.characters) {
newCharImages.push(cha.image ?? "");
} }
function createGroup(){ if (!isEqual(charImages, newCharImages)) {
reseter(); charImages = newCharImages;
const cid = createNewGroup()
selectedCharID.set(-1)
}
async function createImport(){
reseter();
const cid = await importCharacter()
selectedCharID.set(-1)
} }
});
function changeChar(index:number){ onDestroy(unsub);
reseter();
characterFormatUpdate(index)
selectedCharID.set(index)
}
function reseter(){
menuMode = 0;
sideBarMode = 0;
editMode = false
settingsOpen.set(false)
CharEmotion.set({})
}
let charImages:string[] = []
const unsub = DataBase.subscribe((db) => {
let newCharImages:string[] = []
for(const cha of db.characters){
newCharImages.push(cha.image ?? '')
}
if(!isEqual(charImages, newCharImages)){
charImages = newCharImages
}
})
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
{#if menuMode === 0} 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}
{#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> />
{:else} {#if charimg !== ""}
<BarIcon onClick={() => {changeChar(i)}} additionalStyle={i === $selectedCharID ? 'background:#44475a' : ''}> <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div
</BarIcon> on:click={() => {
{/if} changeChar(i);
{#if editMode} }}
<div class="flex flex-col mt-2"> on:keydown={(e) => {
<button on:click={() => { if (e.key === "Enter") {
let chars = $DataBase.characters changeChar(i);
if(chars[i-1]){ }
const currentchar = chars[i] }}
chars[i] = chars[i-1] tabindex="0"
chars[i-1] = currentchar >
$DataBase.characters = chars {#await getCharImage($DataBase.characters[i].image, "plain") then img}
} <SidebarAvatar src={img} size="56" />
}}> {:catch}
<ArrowUp size={20}/> <SidebarAvatar size="56" src="https://via.placeholder.com/150" />
</button> {/await}
<button on:click={() => { </div>
let chars = $DataBase.characters
if(chars[i+1]){
const currentchar = chars[i]
chars[i] = chars[i+1]
chars[i+1] = currentchar
$DataBase.characters = chars
}
}}>
<ArrowDown size={22}/>
</button>
</div>
{/if}
</div>
{/each}
<BarIcon onClick={() => {
if(sideBarMode === 1){
reseter();
sideBarMode = 0
}
else{
reseter();
sideBarMode = 1
}
}}><PlusIcon/></BarIcon>
{:else}
<BarIcon onClick={() => {
if($settingsOpen){
reseter();
settingsOpen.set(false)
}
else{
reseter();
settingsOpen.set(true)
}
}}><Settings/></BarIcon>
<BarIcon onClick={() => {
reseter();
openGrid()
}}><LayoutGridIcon/></BarIcon>
{/if}
</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)}>
<button class="flex w-full justify-end text-gray-200" on:click={() => {sideBarStore.set(false)}}>
<button class="p-0 bg-transparent border-none text-gray-200"><X/></button>
</button>
{#if sideBarMode === 0}
{#if $selectedCharID < 0 || $settingsOpen}
<SettingsDom bind:openPresetList/>
{:else} {:else}
<CharConfig /> <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div
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}
{:else if sideBarMode === 1} {#if editMode}
<h2 class="title font-bold text-xl mt-2">Create</h2> <div class="mt-2 flex flex-col">
<button <button
on:click={createScratch} on:click={() => {
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"> let chars = $DataBase.characters;
{language.createfromScratch} if (chars[i - 1]) {
</button> const currentchar = chars[i];
<button chars[i] = chars[i - 1];
on:click={createImport} chars[i - 1] = currentchar;
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"> $DataBase.characters = chars;
{language.importCharacter} }
</button> }}
<button >
on:click={createGroup} <ArrowUp size={20} />
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"> </button>
{language.createGroup} <button
</button> on:click={() => {
<h2 class="title font-bold text-xl mt-4">Edit</h2> let chars = $DataBase.characters;
<button if (chars[i + 1]) {
on:click={() => {editMode = !editMode;$selectedCharID = -1}} const currentchar = chars[i];
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"> chars[i] = chars[i + 1];
{language.editOrder} chars[i + 1] = currentchar;
</button> $DataBase.characters = chars;
{/if} }
}}
>
<ArrowDown size={22} />
</button>
</div>
{/if}
</div>
{/each}
<div class="flex flex-col items-center space-y-2 px-2">
<BaseRoundedButton
onClick={() => {
if (sideBarMode === 1) {
reseter();
sideBarMode = 0;
} else {
reseter();
sideBarMode = 1;
}
}}
><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}
<BarIcon
onClick={() => {
if ($settingsOpen) {
reseter();
settingsOpen.set(false);
} else {
reseter();
settingsOpen.set(true);
}
}}><Settings /></BarIcon
>
<BarIcon
onClick={() => {
reseter();
openGrid();
}}><LayoutGridIcon /></BarIcon
>
{/if}
</div>
<div
class="setting-area flex w-96 flex-col overflow-y-auto overflow-x-hidden bg-darkbg p-6 text-gray-200"
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>
{#if sideBarMode === 0}
{#if $selectedCharID < 0 || $settingsOpen}
<SettingsDom bind:openPresetList />
{:else}
<CharConfig />
{/if}
{:else if sideBarMode === 1}
<h2 class="title mt-2 text-xl font-bold">Create</h2>
<button
on:click={createScratch}
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}
</button>
<button
on:click={createImport}
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}
</button>
<button
on:click={createGroup}
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}
</button>
<h2 class="title mt-4 text-xl font-bold">Edit</h2>
<button
on:click={() => {
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}
</button>
{/if}
</div> </div>
<style>
.minw96 {
min-width: 24rem; /* 384px */
}
.title{
margin-bottom: 0.5rem;
}
.editMode{
min-width: 6rem;
}
</style>
{#if openPresetList} {#if openPresetList}
<Botpreset close={() => {openPresetList = false}}/> <Botpreset
{/if} close={() => {
openPresetList = false;
}}
/>
{/if}
<style>
.minw96 {
min-width: 24rem; /* 384px */
}
.title {
margin-bottom: 0.5rem;
}
.editMode {
min-width: 6rem;
}
</style>

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>