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}
<button on:click={onClick} class="ico"><slot/></button>
{:then as}
<button on:click={onClick} class="ico" style={as}><slot/></button>
<button on:click={onClick} class="ico"><slot /></button>
{:then as}
<button on:click={onClick} class="ico" style={as}><slot /></button>
{/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 {
--tw-bg-opacity: 1;
background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
}
</style>
<style>
.ico {
cursor: pointer;
border-radius: 0.375rem;
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">
import { CharEmotion, SizeStore, selectedCharID, settingsOpen, sideBarStore } from "../../ts/stores";
import { DataBase } from "../../ts/database";
import BarIcon from "./BarIcon.svelte";
import { Plus, 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 { language } from "../../lang";
import Botpreset from "../Others/botpreset.svelte";
import { onDestroy } from "svelte";
import {isEqual} from 'lodash'
let openPresetList =false
let sideBarMode = 0
let editMode = false
let menuMode = 0
export let openGrid = () => {}
import {
CharEmotion,
SizeStore,
selectedCharID,
settingsOpen,
sideBarStore,
} from "../../ts/stores";
import { DataBase } from "../../ts/database";
import BarIcon from "./BarIcon.svelte";
import SidebarIndicator from "./SidebarIndicator.svelte";
import {
Plus,
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 { 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(){
reseter();
const cid = createNewCharacter()
selectedCharID.set(-1)
function changeChar(index: number) {
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 ?? "");
}
function createGroup(){
reseter();
const cid = createNewGroup()
selectedCharID.set(-1)
}
async function createImport(){
reseter();
const cid = await importCharacter()
selectedCharID.set(-1)
if (!isEqual(charImages, newCharImages)) {
charImages = newCharImages;
}
});
function changeChar(index:number){
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)
onDestroy(unsub);
</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}>
<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={() => {
menuMode = 1 - menuMode
}}><ListIcon/></button>
<div class="w-14 min-w-14 h-8 min-h-8 bg-transparent"></div>
{#if menuMode === 0}
<div
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"
class:editMode
>
<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}
{#each charImages as charimg, i}
<div class="flex items-center">
{#if charimg !== ''}
<BarIcon onClick={() => {changeChar(i)}} additionalStyle={getCharImage($DataBase.characters[i].image, 'css')}>
</BarIcon>
{:else}
<BarIcon onClick={() => {changeChar(i)}} additionalStyle={i === $selectedCharID ? 'background:#44475a' : ''}>
</BarIcon>
{/if}
{#if editMode}
<div class="flex flex-col mt-2">
<button on:click={() => {
let chars = $DataBase.characters
if(chars[i-1]){
const currentchar = chars[i]
chars[i] = chars[i-1]
chars[i-1] = currentchar
$DataBase.characters = chars
}
}}>
<ArrowUp size={20}/>
</button>
<button on:click={() => {
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/>
<div class="group relative flex items-center px-2">
<SidebarIndicator
isActive={$selectedCharID === i && sideBarMode !== 1}
/>
{#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}
<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}
{:else if sideBarMode === 1}
<h2 class="title font-bold text-xl mt-2">Create</h2>
<button
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">
{language.createfromScratch}
</button>
<button
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">
{language.importCharacter}
</button>
<button
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">
{language.createGroup}
</button>
<h2 class="title font-bold text-xl mt-4">Edit</h2>
<button
on:click={() => {editMode = !editMode;$selectedCharID = -1}}
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">
{language.editOrder}
</button>
{/if}
{#if editMode}
<div class="mt-2 flex flex-col">
<button
on:click={() => {
let chars = $DataBase.characters;
if (chars[i - 1]) {
const currentchar = chars[i];
chars[i] = chars[i - 1];
chars[i - 1] = currentchar;
$DataBase.characters = chars;
}
}}
>
<ArrowUp size={20} />
</button>
<button
on:click={() => {
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}
<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>
<style>
.minw96 {
min-width: 24rem; /* 384px */
}
.title{
margin-bottom: 0.5rem;
}
.editMode{
min-width: 6rem;
}
</style>
{#if openPresetList}
<Botpreset close={() => {openPresetList = false}}/>
{/if}
<Botpreset
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>