1
0
Fork 0

WIP design and UI features

This commit is contained in:
Leon Grünewald 2024-03-21 20:54:25 +01:00
parent b07210507e
commit 048c8f1380
13 changed files with 214 additions and 105 deletions

View file

@ -20,18 +20,18 @@
"devDependencies": { "devDependencies": {
"@skeletonlabs/skeleton": "^2.9.0", "@skeletonlabs/skeleton": "^2.9.0",
"@skeletonlabs/tw-plugin": "^0.3.1", "@skeletonlabs/tw-plugin": "^0.3.1",
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.5.4",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tauri-apps/cli": "^1.5.11", "@tauri-apps/cli": "^1.5.11",
"autoprefixer": "^10.4.18", "autoprefixer": "^10.4.18",
"postcss": "^8.4.35", "postcss": "^8.4.35",
"postcss-load-config": "^5.0.3", "postcss-load-config": "^5.0.3",
"svelte": "^4.2.12",
"svelte-check": "^3.6.7",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.4.2", "@sveltejs/adapter-auto": "^3.0.0",
"vite": "^5.1.6" "@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"typescript": "^5.0.0",
"vite": "^5.0.3"
} }
} }

View file

@ -32,16 +32,16 @@ devDependencies:
specifier: ^0.3.1 specifier: ^0.3.1
version: 0.3.1(tailwindcss@3.4.1) version: 0.3.1(tailwindcss@3.4.1)
'@sveltejs/adapter-auto': '@sveltejs/adapter-auto':
specifier: ^3.1.1 specifier: ^3.0.0
version: 3.1.1(@sveltejs/kit@2.5.4) version: 3.1.1(@sveltejs/kit@2.5.4)
'@sveltejs/adapter-static': '@sveltejs/adapter-static':
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.0.1(@sveltejs/kit@2.5.4) version: 3.0.1(@sveltejs/kit@2.5.4)
'@sveltejs/kit': '@sveltejs/kit':
specifier: ^2.5.4 specifier: ^2.0.0
version: 2.5.4(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.1.6) version: 2.5.4(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.1.6)
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: ^3.0.2 specifier: ^3.0.0
version: 3.0.2(svelte@4.2.12)(vite@5.1.6) version: 3.0.2(svelte@4.2.12)(vite@5.1.6)
'@tauri-apps/cli': '@tauri-apps/cli':
specifier: ^1.5.11 specifier: ^1.5.11
@ -56,19 +56,19 @@ devDependencies:
specifier: ^5.0.3 specifier: ^5.0.3
version: 5.0.3(postcss@8.4.35) version: 5.0.3(postcss@8.4.35)
svelte: svelte:
specifier: ^4.2.12 specifier: ^4.2.7
version: 4.2.12 version: 4.2.12
svelte-check: svelte-check:
specifier: ^3.6.7 specifier: ^3.6.0
version: 3.6.7(postcss-load-config@5.0.3)(postcss@8.4.35)(svelte@4.2.12) version: 3.6.7(postcss-load-config@5.0.3)(postcss@8.4.35)(svelte@4.2.12)
tailwindcss: tailwindcss:
specifier: ^3.4.1 specifier: ^3.4.1
version: 3.4.1 version: 3.4.1
typescript: typescript:
specifier: ^5.4.2 specifier: ^5.0.0
version: 5.4.2 version: 5.4.2
vite: vite:
specifier: ^5.1.6 specifier: ^5.0.3
version: 5.1.6 version: 5.1.6
packages: packages:

31
src/lib/PostList.js Normal file
View file

@ -0,0 +1,31 @@
import { TAG_TYPES } from '$lib/Tags';
export default class PostList {
posts = [];
constructor(posts) {
this.posts = posts;
}
filterPosts(tags = []) {
const filteredPosts = [];
postLoop: for (const post of this.posts) {
for (const tagType of Object.keys(TAG_TYPES)) {
if (post.tags[tagType].some((postTag) => tags.includes(postTag))) {
console.info('Skipping post', post);
continue postLoop;
}
}
filteredPosts.push(post);
}
this.posts = filteredPosts;
return this;
}
getPosts() {
return this.posts;
}
}

9
src/lib/Tags.js Normal file
View file

@ -0,0 +1,9 @@
export const TAG_TYPES = {
artist: 'Artist',
character: 'Character',
copyright: 'Copyright',
general: 'General',
lore: 'Lore',
meta: 'Meta',
invalid: 'Invalid'
};

View file

@ -1,21 +1,29 @@
<script> <script>
import {RangeSlider, Tab, TabGroup} from "@skeletonlabs/skeleton"; import { RangeSlider, Tab, TabGroup, InputChip, SlideToggle } from '@skeletonlabs/skeleton';
import {imageGridColumns, postGridPreviewQuality} from "$lib/stores.js"; import { imageGridColumns, postGridPreviewQuality, blacklist, limitImageHeight } from '$lib/settings';
let tabPos = 0; let tabPos = 0;
let showApikey = false;
let apikey = '';
</script> </script>
<div class="flex flex-col"> <div class="flex flex-col">
<h2 class="h2 mb-2 text-dark-token">Settings</h2> <h2 class="h2 mb-2 text-dark-token">Settings</h2>
<TabGroup> <TabGroup>
<Tab class="text-dark-token" bind:group={tabPos} name="tab1" value={0}>Search Grid</Tab> <Tab class="text-dark-token" bind:group={tabPos} name="tab1" value={0}>Display</Tab>
<Tab class="text-dark-token" bind:group={tabPos} name="tab2" value={1}>API</Tab>
<Tab class="text-dark-token" bind:group={tabPos} name="tab3" value={2}>User</Tab>
<svelte:fragment slot="panel"> <svelte:fragment slot="panel">
{#if tabPos === 0} {#if tabPos === 0}
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<h3 class="h3 text-dark-token font-bold">Image</h3>
<SlideToggle name="image-limit-height" checked={$limitImageHeight}>Limit image height</SlideToggle>
<h3 class="h3 text-dark-token font-bold">Grid</h3>
<div class="basis-1/2"> <div class="basis-1/2">
<RangeSlider name="grid-columns" min={1} max={12} bind:value={$imageGridColumns} ticked={true}> <RangeSlider accent="text-dark-token" name="grid-columns" min={1} max={12} bind:value={$imageGridColumns} ticked={true}>
<div class="flex flex-grow justify-between items-center"> <div class="flex flex-grow justify-between items-center dark">
<div class="text-dark-token font-bold">Grid Columns</div> <div class="text-dark-token font-bold mb-1">Grid Columns</div>
<div class="text-xs text-dark-token">{$imageGridColumns} / {12}</div> <div class="text-xs text-dark-token">{$imageGridColumns} / {12}</div>
</div> </div>
</RangeSlider> </RangeSlider>
@ -31,6 +39,40 @@
</div> </div>
</div> </div>
{/if} {/if}
{#if tabPos === 1}
<div class="flex flex-col gap-3">
<div class="">
<label class="label">
<span class="text-dark-token font-bold">e621 Username</span>
<input class="input" />
</label>
</div>
<div class="">
<label class="label">
<span class="text-dark-token font-bold">e621 Apikey</span>
{#if showApikey}
<input class="input" type="text" bind:value={apikey} />
{:else}
<input class="input" type="password" bind:value={apikey} />
{/if}
</label>
<label class="label">
<span class="text-dark-token font-bold">Show apikey</span>
<input class="checkbox" type="checkbox" bind:checked={showApikey} />
</label>
</div>
</div>
{/if}
{#if tabPos === 2}
<div class="flex flex-col gap-3">
<div class="">
<p class="text-dark-token font-bold">Blacklist</p>
<InputChip name="blacklist" label="Blacklist" bind:value={$blacklist} />
</div>
</div>
{/if}
</svelte:fragment> </svelte:fragment>
</TabGroup> </TabGroup>
</div> </div>

View file

@ -1,18 +1,19 @@
<script> <script>
import { showSearch, lastSearch, loading } from '$lib/stores.js'; import { showSearch, loading } from '$lib/stores';
import { lastSearch } from '$lib/settings';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
let searchValue = $lastSearch; let searchValue = $lastSearch;
</script> </script>
{#if $showSearch} {#if $showSearch}
<form class="w-80 pt-2 px-2 flex flex-row gap-2" <form class="flex flex-row gap-2"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
$loading = true; $loading = true;
goto('/') goto('/')
lastSearch.set(searchValue); lastSearch.set(searchValue);
}}> }}>
<input class="input py-1 px-2" type="text" placeholder="Search" bind:value={searchValue} /> <input class="input py-1 px-2 w-96" type="text" placeholder="Search" bind:value={searchValue} />
<button class="btn btn-md variant-filled" type="submit">Sumbit</button> <button class="btn btn-md variant-filled" type="submit">Submit</button>
</form> </form>
{/if} {/if}

View file

@ -1,5 +1,6 @@
<script> <script>
import {postGridPreviewQuality, imageGridColumns, showSearch, loading} from '$lib/stores'; import { showSearch, loading } from '$lib/stores';
import { postGridPreviewQuality, imageGridColumns } from '$lib/settings';
export let posts = []; export let posts = [];
</script> </script>

View file

@ -1,17 +1,19 @@
<script> <script>
import { limitImageHeight } from '$lib/settings.js';
export let post = null; export let post = null;
</script> </script>
<div> <div>
{#if post?.preview?.has} {#if post?.preview?.has}
<img class="mb-2" src={post.preview.url} alt={`Post ${post.id}`} /> <img class:h-dvh={$limitImageHeight} src={post.preview.url} alt={`Post ${post.id}`} />
{:else} {:else}
{#if ['webm', 'mp4'].includes(post.file.ext)} {#if ['webm', 'mp4'].includes(post.file.ext)}
<video class="mb-2" controls playsinline> <video class:h-dvh={$limitImageHeight} controls playsinline>
<source src={post.file.url} /> <source src={post.file.url} />
</video> </video>
{:else} {:else}
<img class="mb-2" src={post.file.url} alt={`Post ${post.id}}`} /> <img class:h-dvh={$limitImageHeight} src={post.file.url} alt={`Post ${post.id}}`} />
{/if} {/if}
{/if} {/if}
</div> </div>

14
src/lib/settings.js Normal file
View file

@ -0,0 +1,14 @@
import { localStorageStore } from '@skeletonlabs/skeleton';
export const lastSearch = localStorageStore('lastSearch', '');
export const imageGridColumns = localStorageStore('gridColumns', 6);
export const postGridPreviewQuality = localStorageStore('gridPreviewQuality', 'preview');
export const blacklist = localStorageStore('blacklist', [
'gore',
'scat',
'watersports',
'young',
'loli',
'shota',
]);
export const limitImageHeight = localStorageStore('limitImageHeight', false);

View file

@ -1,9 +1,5 @@
import { localStorageStore } from '@skeletonlabs/skeleton';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
export const lastSearch = localStorageStore('lastSearch', '');
export const imageGridColumns = localStorageStore('gridColumns', 6);
export const postSearchResults = writable(null); export const postSearchResults = writable(null);
export const postGridPreviewQuality = localStorageStore('gridPreviewQuality', 'preview');
export const loading = writable(false); export const loading = writable(false);
export const showSearch = writable(true); export const showSearch = writable(true);

View file

@ -1,30 +1,26 @@
<script> <script>
import { Fa } from 'svelte-fa'; import { Fa } from 'svelte-fa';
import {GlobalSearch, ImageGrid, SearchAppRailAnchor} from '$lib/components'; import { GlobalSearch, ImageGrid, SearchAppRailAnchor} from '$lib/components';
import { faGear } from '@fortawesome/free-solid-svg-icons'; import { faGear } from '@fortawesome/free-solid-svg-icons';
import { invoke } from '@tauri-apps/api/tauri' import { invoke } from '@tauri-apps/api/tauri'
import { lastSearch, postSearchResults, loading } from '$lib/stores'; import { lastSearch, blacklist } from '$lib/settings';
import { postSearchResults, loading, showSearch} from '$lib/stores';
import { import {
AppBar,
AppRail, AppRail,
AppRailAnchor, AppRailAnchor,
AppShell, AppShell,
getDrawerStore getDrawerStore
} from '@skeletonlabs/skeleton'; } from '@skeletonlabs/skeleton';
import {onMount} from 'svelte'; import PostList from '$lib/PostList';
const drawerStore = getDrawerStore(); const drawerStore = getDrawerStore();
onMount(() => {
$loading = false;
});
function onSearch() { function onSearch() {
$loading = true; $loading = true;
invoke('get_posts', { query: $lastSearch }).then((resPosts) => { invoke('get_posts', { query: $lastSearch }).then((resPosts) => {
postSearchResults.set(resPosts); postSearchResults.set(new PostList(resPosts).filterPosts($blacklist).getPosts());
requestAnimationFrame(() => { requestAnimationFrame(() => $loading = false);
$loading = false;
});
}); });
} }
@ -39,7 +35,11 @@
<AppShell> <AppShell>
<svelte:fragment slot="pageHeader"> <svelte:fragment slot="pageHeader">
{#if $showSearch}
<AppBar>
<GlobalSearch /> <GlobalSearch />
</AppBar>
{/if}
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="sidebarLeft"> <svelte:fragment slot="sidebarLeft">

View file

@ -5,10 +5,8 @@ export const ssr = false;
/** @type {import('./$types').PageLoad} */ /** @type {import('./$types').PageLoad} */
export async function load({ params }) { export async function load({ params }) {
// const post = await invoke('get_post', {post_id: params.post_id}); // const post = await invoke('get_post', {post_id: params.post_id});
const post = await invoke('get_post', {postId: +params.post_id});
return { return {
postId: params.post_id, postId: params.post_id
post
}; };
} }

View file

@ -1,49 +1,60 @@
<script> <script>
import { import {
AppBar,
AppRail, AppRail,
AppRailAnchor, AppRailAnchor,
AppShell, AppShell,
getDrawerStore, getDrawerStore,
clipboard,
getToastStore
} from '@skeletonlabs/skeleton'; } from '@skeletonlabs/skeleton';
import { faGear, faCopy } from '@fortawesome/free-solid-svg-icons'; import { faGear, faCopy } from '@fortawesome/free-solid-svg-icons';
import { Fa } from 'svelte-fa'; import { Fa } from 'svelte-fa';
import { BackAppRailAnchor, GlobalSearch, PostMedia, SearchAppRailAnchor } from '$lib/components'; import { BackAppRailAnchor, GlobalSearch, PostMedia, SearchAppRailAnchor } from '$lib/components';
import {onMount} from 'svelte'; import { onMount } from 'svelte';
import {loading} from '$lib/stores.js'; import { loading, showSearch } from '$lib/stores';
import { limitImageHeight } from '$lib/settings.js';
import { TAG_TYPES } from '$lib/Tags';
import { invoke } from '@tauri-apps/api/tauri';
export let data; export let data;
let drawerStore = getDrawerStore(); const drawerStore = getDrawerStore();
const tagTypes = Object.entries({ const tagTypes = Object.entries(TAG_TYPES);
artist: 'Artist', const toastStore = getToastStore();
character: 'Character',
copyright: 'Copyright',
general: 'General',
lore: 'Lore',
meta: 'Meta',
invalid: 'Invalid'
});
onMount(() => { /** @type any|null */
let post = null;
onMount(async () => {
$loading = true;
const postRequest = await invoke('get_post', {postId: +data.postId});
$loading = false; $loading = false;
post = postRequest;
}); });
</script> </script>
<AppShell> <AppShell>
<svelte:fragment slot="pageHeader"> <svelte:fragment slot="pageHeader">
{#if $showSearch}
<AppBar>
<GlobalSearch /> <GlobalSearch />
</AppBar>
{/if}
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="sidebarRight"> <svelte:fragment slot="sidebarRight">
{#if post}
<div class="p-2 h-dvh"> <div class="p-2 h-dvh">
{#each tagTypes as [name, label]} {#each tagTypes as [name, label]}
<div class="mb-2"> <div class="mb-2">
<p class="font-bold">{label}</p> <p class="font-bold">{label}</p>
{#each data.post.tags[name] as tag} {#each post.tags[name] as tag}
<p>{tag}</p> <p>{tag}</p>
{/each} {/each}
</div> </div>
{/each} {/each}
</div> </div>
{/if}
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="sidebarLeft"> <svelte:fragment slot="sidebarLeft">
@ -54,17 +65,19 @@
<SearchAppRailAnchor /> <SearchAppRailAnchor />
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="trail"> <svelte:fragment slot="trail">
<AppRailAnchor on:click={() => {}}> {#if post}
<div class="flex flex-col gap-2 place-content-center"> <AppRailAnchor>
<button class="w-full flex flex-col gap-2 place-content-center place-items-center" use:clipboard={`https://e621.net/posts/${post.id}`} on:click={() => toastStore.trigger({message: 'Copied link'})}>
<Fa size={'2x'} icon={faCopy} /> <Fa size={'2x'} icon={faCopy} />
<p>Copy link</p> Copy link
</div> </button>
</AppRailAnchor> </AppRailAnchor>
{/if}
<AppRailAnchor on:click={() => drawerStore.open()}> <AppRailAnchor on:click={() => drawerStore.open()}>
<div class="flex flex-col gap-2 place-content-center"> <button class="w-full flex flex-col gap-2 place-content-center place-items-center">
<Fa size={'2x'} icon={faGear} /> <Fa size={'2x'} icon={faGear} />
<p>Settings</p> Settings
</div> </button>
</AppRailAnchor> </AppRailAnchor>
</svelte:fragment> </svelte:fragment>
</AppRail> </AppRail>
@ -72,19 +85,21 @@
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="default"> <svelte:fragment slot="default">
{#if post}
<div class="container mx-auto p-2"> <div class="container mx-auto p-2">
<div class="flex flex-row place-content-center"> <div class="flex flex-row place-content-center" >
<PostMedia post={data.post} /> <PostMedia post={post} />
</div> </div>
<div class="card"> <div class="card">
<header class="px-3 pt-2"> <header class="px-3 pt-2">
<p class="font-bold">Description</p> <p class="font-bold" class:font-red={post.description === ''}>Description</p>
</header> </header>
<section class="p-3"> <section class="p-3">
<p>{data.post.description}</p> <p>{post.description}</p>
</section> </section>
</div> </div>
</div> </div>
{/if}
</svelte:fragment> </svelte:fragment>
</AppShell> </AppShell>