Migrate away from tailwind and move back to stores
This commit is contained in:
parent
35a9b95c8e
commit
507ea37f74
20 changed files with 568 additions and 299 deletions
18
src/app.pcss
18
src/app.pcss
|
@ -1,18 +0,0 @@
|
||||||
/* Write your global styles here, in PostCSS syntax */
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
<script>
|
|
||||||
import useSettings from '$lib/useSettings.svelte.js';
|
|
||||||
const settings = useSettings();
|
|
||||||
|
|
||||||
let tabPos = $state(0);
|
|
||||||
let showApikey = $state(false);
|
|
||||||
let apikey = $state('');
|
|
||||||
</script>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<h2 class="h2 mb-2 text-dark-token">Settings</h2>
|
|
||||||
<!--<TabGroup>
|
|
||||||
<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>
|
|
||||||
<Tab class="text-dark-token" bind:group={tabPos} name="tab4" value={3}>Chats</Tab>
|
|
||||||
|
|
||||||
{#snippet panel()}
|
|
||||||
{#if tabPos === 0}
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<h3 class="h3 text-dark-token font-bold">Image</h3>
|
|
||||||
<SlideToggle name="image-limit-height" bind:checked={settings.limitImageHeight}><span class="text-dark-token">Limit image height</span></SlideToggle>
|
|
||||||
|
|
||||||
<h3 class="h3 text-dark-token font-bold">Grid</h3>
|
|
||||||
<div class="basis-1/2">
|
|
||||||
<RangeSlider accent="text-dark-token" name="grid-columns" min={1} max={12} bind:value={settings.imageGridColumns} ticked={true}>
|
|
||||||
<div class="flex flex-grow justify-between items-center dark">
|
|
||||||
<div class="text-dark-token font-bold mb-1">Grid Columns</div>
|
|
||||||
<div class="text-xs text-dark-token">{settings.imageGridColumns} / {12}</div>
|
|
||||||
</div>
|
|
||||||
</RangeSlider>
|
|
||||||
</div>
|
|
||||||
<div class="basis-1/2">
|
|
||||||
<label class="label">
|
|
||||||
<span class="text-dark-token font-bold">Preview type</span>
|
|
||||||
<select class="select" bind:value={settings.postGridPreviewQuality}>
|
|
||||||
<option value="preview">Preview</option>
|
|
||||||
<option value="sample">Sample</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/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={settings.blacklist} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if tabPos === 3}
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<div class="">
|
|
||||||
<p class="text-dark-token font-bold">Chats</p>
|
|
||||||
<InputChip name="blacklist" label="Blacklist" bind:value={settings.blacklist} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
</TabGroup>-->
|
|
||||||
</div>
|
|
|
@ -2,6 +2,7 @@
|
||||||
import {faArrowLeft} from '@fortawesome/free-solid-svg-icons';
|
import {faArrowLeft} from '@fortawesome/free-solid-svg-icons';
|
||||||
import {Fa} from 'svelte-fa';
|
import {Fa} from 'svelte-fa';
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
|
import Button from "$lib/components/Button.svelte";
|
||||||
|
|
||||||
let { popCount = 1 } = $props();
|
let { popCount = 1 } = $props();
|
||||||
let href = $state();
|
let href = $state();
|
||||||
|
@ -28,6 +29,6 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a {href} class="flex flex-row aspect-square place-content-center justify-center items-center bg-gray-600 h-10">
|
<Button {href} >
|
||||||
<Fa color="white" size={'1.5x'} icon={faArrowLeft} />
|
<Fa color="white" size={'1.5x'} icon={faArrowLeft} />
|
||||||
</a>
|
</Button>
|
||||||
|
|
30
src/lib/components/Button.svelte
Normal file
30
src/lib/components/Button.svelte
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<script>
|
||||||
|
let { children, class: className, fill = false, type = 'button', onclick = null} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button class={className}
|
||||||
|
style:width={fill ? '100%' : 'auto'}
|
||||||
|
{type}
|
||||||
|
onclick={() => onclick?.()}>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
place-content: center;
|
||||||
|
height: 40px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
background-color: #4b5563;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #434b58;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
58
src/lib/components/Chips.svelte
Normal file
58
src/lib/components/Chips.svelte
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<script>
|
||||||
|
import { Button } from '$lib/components';
|
||||||
|
|
||||||
|
let { values = $bindable([]), onAdd = () => {} } = $props();
|
||||||
|
let newValue = $state('');
|
||||||
|
|
||||||
|
function onAddNewValue() {
|
||||||
|
onAdd?.(newValue);
|
||||||
|
newValue = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chips-container">
|
||||||
|
<form>
|
||||||
|
<input bind:value={newValue} type="text" />
|
||||||
|
<Button type="submit" onclick={onAddNewValue}>Add</Button>
|
||||||
|
</form>
|
||||||
|
<div class="chips">
|
||||||
|
{#if values}
|
||||||
|
{#each values as chip}
|
||||||
|
<div class="chip">
|
||||||
|
<Button onclick={() => {
|
||||||
|
values = values.filter(value => value !== chip);
|
||||||
|
}}>{chip}</Button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chips-container {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chips {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
background-color: #4b5563;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #434b58;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,22 +1,29 @@
|
||||||
<script>
|
<script>
|
||||||
import useSettings from '$lib/useSettings.svelte.js';
|
import {loading} from '$lib/stores.js';
|
||||||
import {loading} from "$lib/stores.js";
|
import {Button} from '$lib/components';
|
||||||
|
import {lastSearch} from '$lib/stores.js';
|
||||||
|
|
||||||
const settings = useSettings();
|
|
||||||
let searchString = $state('');
|
let searchString = $state('');
|
||||||
const {onsubmit = null} = $props();
|
const {onsubmit = null} = $props();
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
searchString = settings.lastSearch
|
searchString = $lastSearch
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="flex flex-row gap-2"
|
<form
|
||||||
onsubmit={(e) => {
|
onsubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
settings.lastSearch = searchString;
|
$lastSearch = searchString;
|
||||||
loading.set(true);
|
loading.set(true);
|
||||||
onsubmit?.(searchString, true);
|
onsubmit?.(searchString, true);
|
||||||
}}>
|
}}>
|
||||||
<input class="h-10 p-2" type="text" placeholder="tags" bind:value={searchString} />
|
<input type="text" placeholder="tags" bind:value={searchString} />
|
||||||
<button class="h-10 p-2 text-white bg-gray-600" type="submit">Search</button>
|
<Button type="submit">Search</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,11 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import {BackButton, GlobalSearch} from "$lib/components";
|
import {BackButton, GlobalSearch, LinkButton} from "$lib/components";
|
||||||
import {faArrowLeft, faCogs} from "@fortawesome/free-solid-svg-icons";
|
import {faCogs} from "@fortawesome/free-solid-svg-icons";
|
||||||
import {Fa} from "svelte-fa";
|
import {Fa} from "svelte-fa";
|
||||||
let {onsubmit = null, back = null, class: className, showSearch = true} = $props();
|
let {onsubmit = null, back = null, class: className, showSearch = true, gridArea = ""} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="absolute w-full top-0 flex bg-transparent py-2 px-4 gap-1 {className}">
|
<header class={className} style:grid-area={gridArea}>
|
||||||
|
<div class="left">
|
||||||
{#if back}
|
{#if back}
|
||||||
<BackButton popCount={back} />
|
<BackButton popCount={back} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -14,7 +15,25 @@
|
||||||
onsubmit?.(val, explicitRefresh);
|
onsubmit?.(val, explicitRefresh);
|
||||||
}} />
|
}} />
|
||||||
{/if}
|
{/if}
|
||||||
<a href="/settings" class="flex flex-row aspect-square place-content-center justify-center items-center bg-gray-600 h-10">
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<LinkButton href="/settings/main">
|
||||||
<Fa color="white" size={'1.5x'} icon={faCogs} />
|
<Fa color="white" size={'1.5x'} icon={faCogs} />
|
||||||
</a>
|
</LinkButton>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
header {
|
||||||
|
margin: 10px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
grid-area: header;
|
||||||
|
width: 100%;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,25 +1,36 @@
|
||||||
<script>
|
<script>
|
||||||
import useSettings from "$lib/useSettings.svelte.js";
|
import { loading, imageGridColumns, postGridPreviewQuality } from "$lib/stores.js";
|
||||||
import { loading } from "$lib/stores.js";
|
|
||||||
|
|
||||||
let { posts = [] } = $props();
|
let { posts = [] } = $props();
|
||||||
const settings = useSettings();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid gap-3" style={`grid-template-columns: repeat(${settings.imageGridColumns}, minmax(0, 1fr))`}>
|
<div class="post-grid" style={`grid-template-columns: repeat(${$imageGridColumns}, minmax(0, 1fr))`}>
|
||||||
{#each posts as post}
|
{#each posts as post}
|
||||||
<a href={`/posts/${post.id}`} class="card p-3 flex flex-col justify-end items-center" onclick={() => {
|
<a href={`/posts/${post.id}`} class="post-grid-item" onclick={() => {
|
||||||
loading.set(true);
|
loading.set(true);
|
||||||
}}>
|
}}>
|
||||||
<header class="card-header p-0"></header>
|
<img src={post[$postGridPreviewQuality].url}
|
||||||
<section class="flex flex-col">
|
|
||||||
<img src={post[settings.postGridPreviewQuality].url}
|
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
alt={`Post ${post.id}`} />
|
alt={`Post ${post.id}`} />
|
||||||
<div class="bg-black px-2 py-1">
|
<p class="post-info-bar">{post.id} {post.file.ext} {post.rating}</p>
|
||||||
<p class="text-center text-white">{post.id} {post.file.ext} {post.rating}</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.post-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-grid-item {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-info-bar {
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
</style>
|
27
src/lib/components/LinkButton.svelte
Normal file
27
src/lib/components/LinkButton.svelte
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<script>
|
||||||
|
let { children, class: className, href = $bindable(""), fill = false} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a class={className} style:width={fill ? "100%" : "auto"} {href}>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
place-content: center;
|
||||||
|
height: 40px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
background-color: #4b5563;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #434b58;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import useSettings from '$lib/useSettings.svelte.js';
|
import {limitImageHeight} from '$lib/stores.js';
|
||||||
const settings = useSettings();
|
import {faCaretDown, faCaretUp, faCopy, faHeart, faStar} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import {Fa} from "svelte-fa";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Props
|
* @typedef {Object} Props
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if post?.preview?.has}
|
{#if post?.preview?.has}
|
||||||
<img class:max-h-dvh={settings.limitImageHeight} class={className} src={post.preview.url} alt={`Post ${post.id}`} />
|
<img style:max-height={$limitImageHeight ? '100vh' : 'none'} class={className} 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={className} controls playsinline onclick={async e => {
|
<video class={className} controls playsinline onclick={async e => {
|
||||||
|
@ -29,3 +30,72 @@
|
||||||
}}/>
|
}}/>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
<section class="post-info-bar">
|
||||||
|
<div class="post-info-bar-left">
|
||||||
|
<button class="btn-favorited">
|
||||||
|
<span class:accent-yellow-50={!post.is_favorited} class:accent-yellow-500={post.is_favorited}>
|
||||||
|
<Fa size={'1.5x'} icon={faHeart} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{post.fav_count}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn-score">
|
||||||
|
<span class="text-secondary-500">
|
||||||
|
<Fa size={'1.5x'} icon={faStar} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{post.score.total}
|
||||||
|
</button>
|
||||||
|
<button class="btn-like">
|
||||||
|
<Fa size={'1.5x'} icon={faCaretUp} />
|
||||||
|
</button>
|
||||||
|
<button class="btn-dislike">
|
||||||
|
<Fa size={'1.5x'} icon={faCaretDown} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="post-info-bar-right">
|
||||||
|
<button class="btn-url"
|
||||||
|
onclick={async () => {await navigator.clipboard.writeText(`https://e621.net/posts/${post.id}`)}}>
|
||||||
|
<Fa size={'1.5x'} icon={faCopy} />
|
||||||
|
</button>
|
||||||
|
<button class="btn-id">
|
||||||
|
ID: {post.id}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
video, img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-info-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-like, .btn-dislike, .btn-favorited, .btn-id, .btn-score, .btn-url {
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: black;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-info-bar-left, .post-info-bar-right {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,15 +1,19 @@
|
||||||
import AppSettings from './AppSettings.svelte';
|
|
||||||
import BackButton from './BackButton.svelte';
|
import BackButton from './BackButton.svelte';
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
import Chips from './Chips.svelte';
|
||||||
import GlobalSearch from './GlobalSearch.svelte';
|
import GlobalSearch from './GlobalSearch.svelte';
|
||||||
import ImageGrid from './ImageGrid.svelte';
|
|
||||||
import PostMedia from './PostMedia.svelte';
|
|
||||||
import Header from './Header.svelte';
|
import Header from './Header.svelte';
|
||||||
|
import ImageGrid from './ImageGrid.svelte';
|
||||||
|
import LinkButton from './LinkButton.svelte';
|
||||||
|
import PostMedia from './PostMedia.svelte';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AppSettings,
|
|
||||||
BackButton,
|
BackButton,
|
||||||
|
Button,
|
||||||
|
Chips,
|
||||||
GlobalSearch,
|
GlobalSearch,
|
||||||
ImageGrid,
|
|
||||||
PostMedia,
|
|
||||||
Header,
|
Header,
|
||||||
|
ImageGrid,
|
||||||
|
LinkButton,
|
||||||
|
PostMedia,
|
||||||
}
|
}
|
|
@ -1,4 +1,22 @@
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
function localStorageStore(storageKey, defaultValue = null) {
|
||||||
|
if (!browser) return defaultValue;
|
||||||
|
let value = window.localStorage.getItem(storageKey);
|
||||||
|
if (value === null) {
|
||||||
|
if (defaultValue !== null) window.localStorage.setItem(storageKey, defaultValue);
|
||||||
|
value = defaultValue;
|
||||||
|
}
|
||||||
|
return writable(value)
|
||||||
|
}
|
||||||
|
|
||||||
export const postSearchResults = writable(null);
|
export const postSearchResults = writable(null);
|
||||||
export const loading = writable(false);
|
export const loading = writable(false);
|
||||||
|
export const lastSearch = localStorageStore('lastSearch', '');
|
||||||
|
export const showSearch = writable(true);
|
||||||
|
export const imageGridColumns = localStorageStore('gridColumns', 6);
|
||||||
|
export const postGridPreviewQuality = localStorageStore('gridPreviewQuality', 'preview');
|
||||||
|
export const blacklist = localStorageStore('blacklist', ['gore', 'scat'])
|
||||||
|
|
||||||
|
export const limitImageHeight = localStorageStore('limitImageHeight', false)
|
|
@ -1,52 +0,0 @@
|
||||||
import {onMount} from "svelte";
|
|
||||||
|
|
||||||
export default function useSettings() {
|
|
||||||
let lastSearch = $state(null),
|
|
||||||
imageGridColumns = $state(6),
|
|
||||||
limitImageHeight = $state(true),
|
|
||||||
postGridPreviewQuality = $state('preview'),
|
|
||||||
blacklist = $state([
|
|
||||||
'gore',
|
|
||||||
'scat',
|
|
||||||
'watersports',
|
|
||||||
'young',
|
|
||||||
'loli',
|
|
||||||
'shota',
|
|
||||||
]);
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
lastSearch = localStorage.getItem('lastSearch') ?? lastSearch;
|
|
||||||
imageGridColumns = localStorage.getItem('gridColumns') ?? imageGridColumns;
|
|
||||||
limitImageHeight = localStorage.getItem('limitImageHeight') ?? limitImageHeight;
|
|
||||||
postGridPreviewQuality = localStorage.getItem('gridPreviewQuality') ?? postGridPreviewQuality;
|
|
||||||
blacklist = localStorage.getItem('blacklist') ?? blacklist;
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
get lastSearch() {return lastSearch},
|
|
||||||
get imageGridColumns() {return imageGridColumns},
|
|
||||||
get postGridPreviewQuality() {return postGridPreviewQuality},
|
|
||||||
get blacklist() {return blacklist},
|
|
||||||
get limitImageHeight() {return limitImageHeight},
|
|
||||||
set lastSearch(val) {
|
|
||||||
lastSearch = val;
|
|
||||||
localStorage.setItem('lastSearch', val);
|
|
||||||
},
|
|
||||||
set imageGridColumns(val) {
|
|
||||||
imageGridColumns = val;
|
|
||||||
localStorage.setItem('gridColumns', val);
|
|
||||||
},
|
|
||||||
set postGridPreviewQuality(val) {
|
|
||||||
postGridPreviewQuality = val
|
|
||||||
localStorage.setItem('gridPreviewQuality', val);
|
|
||||||
},
|
|
||||||
set blacklist(val) {
|
|
||||||
blacklist = val
|
|
||||||
localStorage.setItem('blacklist', val);
|
|
||||||
},
|
|
||||||
set limitImageHeight(val) {
|
|
||||||
limitImageHeight = val
|
|
||||||
localStorage.setItem('limitImageHeight', val);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import '../app.pcss';
|
|
||||||
import { loading } from '$lib/stores.js';
|
import { loading } from '$lib/stores.js';
|
||||||
import { Fa } from 'svelte-fa';
|
import { Fa } from 'svelte-fa';
|
||||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
@ -7,16 +6,59 @@
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-gray-200">
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if $loading}
|
{#if $loading}
|
||||||
<div class="absolute bottom-4 right-4 spin">
|
<div class="loading spin">
|
||||||
<Fa size={'2x'} icon={faSpinner}></Fa>
|
<Fa size={'2x'} icon={faSpinner}></Fa>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
:global(*) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body) {
|
||||||
|
color: #fff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(h1, h2, h3, h4, h5, h6) {
|
||||||
|
font: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(p) {
|
||||||
|
font: inherit;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(h1) {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(input) {
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
min-height: 40px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(button) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.spin {
|
.spin {
|
||||||
animation: spin 2s infinite;
|
animation: spin 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,14 @@
|
||||||
import { Header, ImageGrid } from '$lib/components';
|
import { Header, ImageGrid } from '$lib/components';
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import PostList from '$lib/PostList';
|
import PostList from '$lib/PostList';
|
||||||
import useSettings from "$lib/useSettings.svelte.js";
|
import {onMount} from 'svelte';
|
||||||
import {onMount} from "svelte";
|
import {loading, postSearchResults, blacklist, lastSearch} from '$lib/stores.js';
|
||||||
import {loading, postSearchResults} from "$lib/stores.js";
|
|
||||||
|
|
||||||
const settings = useSettings();
|
|
||||||
|
|
||||||
function onSearch(val = '', explicitRefresh = false) {
|
function onSearch(val = '', explicitRefresh = false) {
|
||||||
loading.set(true);
|
loading.set(true);
|
||||||
invoke('get_posts', { query: val }).then((resPosts) => {
|
invoke('get_posts', { query: val }).then((resPosts) => {
|
||||||
postSearchResults.set(new PostList(resPosts).filterPosts(settings.blacklist).getPosts())
|
postSearchResults.set(new PostList(resPosts).filterPosts($blacklist).getPosts())
|
||||||
loading.set(false)
|
loading.set(false)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -24,18 +22,45 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($postSearchResults === null) {
|
if ($postSearchResults === null) {
|
||||||
onSearch(settings.lastSearch, true);
|
onSearch($lastSearch, true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
|
||||||
<Header onsubmit={(val = '', explicitRefresh = false) => {onSearch(val, explicitRefresh);}}></Header>
|
<div class="container">
|
||||||
|
<Header gridArea="header" onsubmit={(val = '', explicitRefresh = false) => {onSearch(val, explicitRefresh);}}></Header>
|
||||||
<main>
|
<main>
|
||||||
<div class="overflow-scroll p-2">
|
{#if !$loading}
|
||||||
{#if $postSearchResults}
|
{#if $postSearchResults && $postSearchResults.length > 0}
|
||||||
<ImageGrid posts={$postSearchResults} />
|
<ImageGrid posts={$postSearchResults} />
|
||||||
|
{:else}
|
||||||
|
<p>No search results found</p>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
padding: 0 10px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: "header header header" "main main main";
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
grid-area: aside;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
grid-area: main;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1210px) {
|
||||||
|
.container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { faStar, faCaretUp, faCaretDown, faHeart, faCopy } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { Fa } from 'svelte-fa';
|
|
||||||
import { PostMedia, Header } from '$lib/components';
|
import { PostMedia, Header } from '$lib/components';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { TAG_TYPES } from '$lib/Tags';
|
import { TAG_TYPES } from '$lib/Tags';
|
||||||
|
@ -19,77 +17,80 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid [grid-template-areas:'header_header_header''main_main_aside']">
|
<div class="container">
|
||||||
<Header class="[grid-area:header]" back={2} showSearch={false} onsubmit={(val = '') => {
|
<Header back={2} showSearch={false} onsubmit={(val = '') => {
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
queryParams.set('search', val);
|
queryParams.set('search', val);
|
||||||
goto("/?" + queryParams.toString())
|
goto("/?" + queryParams.toString())
|
||||||
}}></Header>
|
}}></Header>
|
||||||
|
|
||||||
<main class="[grid-area:main]">
|
<main id="container-main">
|
||||||
{#if post}
|
{#if post}
|
||||||
<div class="flex flex-col">
|
<div class="post-main">
|
||||||
<PostMedia class="max-h-[95vh] w-auto" {post} />
|
<PostMedia {post} />
|
||||||
<section class="flex flex-row justify-between bg-black">
|
<footer id="post-footer">
|
||||||
<div class="flex flex-row px-2 gap-2">
|
<h3 style:text-decoration={post.description === '' ? 'strike-trough' : 'none' } >Description</h3>
|
||||||
<button class="py-2 px-3 flex flex-row place-items-center gap-1 text-white hover:text-black hover:bg-white">
|
<p>{post.description}</p>
|
||||||
<span class:accent-yellow-50={!post.is_favorited} class:accent-yellow-500={post.is_favorited}>
|
|
||||||
<Fa size={'1.5x'} icon={faHeart} />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{post.fav_count}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button class="py-2 px-3 flex flex-row place-items-center text-white hover:text-black hover:bg-white">
|
|
||||||
<span class="text-secondary-500">
|
|
||||||
<Fa size={'1.5x'} icon={faStar} />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{post.score.total}
|
|
||||||
</button>
|
|
||||||
<button class="py-2 px-3 flex flex-row place-items-center text-white hover:text-black hover:bg-white">
|
|
||||||
<Fa size={'1.5x'} icon={faCaretUp} />
|
|
||||||
</button>
|
|
||||||
<button class="py-2 px-3 flex flex-row place-items-center text-white hover:text-black hover:bg-white">
|
|
||||||
<Fa size={'1.5x'} icon={faCaretDown} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row px-2 gap-2">
|
|
||||||
<button class="py-2 px-3 flex flex-row place-items-center text-white hover:text-black hover:bg-white"
|
|
||||||
onclick={async () => {await navigator.clipboard.writeText(`https://e621.net/posts/${post.id}`)}}>
|
|
||||||
<Fa size={'1.5x'} icon={faCopy} />
|
|
||||||
</button>
|
|
||||||
<button class="py-2 px-3 flex flex-row place-items-center text-white hover:text-black hover:bg-white">
|
|
||||||
ID: {post.id}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<footer class="card-footer py-4 px-2">
|
|
||||||
<h3 class="font-bold mb-2" class:line-through={post.description === ''} >Description</h3>
|
|
||||||
<p class="text-wrap">{post.description}</p>
|
|
||||||
<!--<pre>{JSON.stringify(post, null, 4)}</pre>-->
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<aside class="[grid-area:aside]">
|
<aside id="container-aside">
|
||||||
{#if post}
|
{#if post}
|
||||||
<div class="p-2">
|
|
||||||
{#each tagTypes as [name, label]}
|
{#each tagTypes as [name, label]}
|
||||||
{#if post.tags[name].length > 0}
|
{#if post.tags[name].length > 0}
|
||||||
<div class="mb-2">
|
<div class="tags-type-wrapper">
|
||||||
<p class="font-bold">{label}</p>
|
<p class="tag-type">{label}</p>
|
||||||
{#each post.tags[name] as tag}
|
{#each post.tags[name] as tag}
|
||||||
<p>{tag}</p>
|
<p class="tag">{tag}</p>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<footer id="container-footer">
|
||||||
|
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss"></style>
|
<style>
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: "header header header" "aside main main" "footer footer footer";
|
||||||
|
grid-gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-type {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-main {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container-main {
|
||||||
|
grid-area: main;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container-aside {
|
||||||
|
min-width: 300px;
|
||||||
|
grid-area: aside;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container-footer {
|
||||||
|
grid-area: footer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#post-footer {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
<script>
|
|
||||||
import {goto} from "$app/navigation";
|
|
||||||
import {Header} from "$lib/components/index.js";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="grid [grid-template-areas:'header_header_header''main_main_aside']">
|
|
||||||
<Header class="[grid-area:header]" back={2} showSearch={false} onsubmit={(val = '') => {
|
|
||||||
const queryParams = new URLSearchParams();
|
|
||||||
queryParams.set('search', val);
|
|
||||||
goto("/?" + queryParams.toString())
|
|
||||||
}}></Header>
|
|
||||||
</div>
|
|
35
src/routes/settings/SettingsNav.svelte
Normal file
35
src/routes/settings/SettingsNav.svelte
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<script>
|
||||||
|
import {LinkButton} from "$lib/components";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<LinkButton href="/settings/main" fill={true}>
|
||||||
|
Main
|
||||||
|
</LinkButton>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<LinkButton href="/settings/display" fill={true}>
|
||||||
|
Display
|
||||||
|
</LinkButton>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<LinkButton href="/settings/blacklist" fill={true}>
|
||||||
|
Blacklist
|
||||||
|
</LinkButton>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
48
src/routes/settings/blacklist/+page.svelte
Normal file
48
src/routes/settings/blacklist/+page.svelte
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<script>
|
||||||
|
import {goto} from '$app/navigation';
|
||||||
|
import {Chips, Header} from '$lib/components';
|
||||||
|
import SettingsNav from '../SettingsNav.svelte';
|
||||||
|
import {blacklist} from '$lib/stores.js';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<Header gridArea="header" back={2} showSearch={false} onsubmit={(val = '') => {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
queryParams.set('search', val);
|
||||||
|
goto("/?" + queryParams.toString());
|
||||||
|
}}></Header>
|
||||||
|
<aside>
|
||||||
|
<SettingsNav></SettingsNav>
|
||||||
|
</aside>
|
||||||
|
<main>
|
||||||
|
{JSON.stringify($blacklist)}
|
||||||
|
<!--<Chips bind:values={$blacklist} onAdd={val => {
|
||||||
|
$blacklist.push(val);
|
||||||
|
}}></Chips>-->
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
padding: 0 10px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: "header header header" "aside main main";
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
grid-area: aside;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
grid-area: main;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1210px) {
|
||||||
|
.container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
43
src/routes/settings/main/+page.svelte
Normal file
43
src/routes/settings/main/+page.svelte
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<script>
|
||||||
|
import {goto} from "$app/navigation";
|
||||||
|
import {Header, Chips} from "$lib/components";
|
||||||
|
import SettingsNav from "../SettingsNav.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<Header gridArea="header" back={2} showSearch={false} onsubmit={(val = '') => {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
queryParams.set('search', val);
|
||||||
|
goto("/?" + queryParams.toString());
|
||||||
|
}}></Header>
|
||||||
|
<aside>
|
||||||
|
<SettingsNav></SettingsNav>
|
||||||
|
</aside>
|
||||||
|
<main>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
padding: 0 10px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: "header header header" "aside main main";
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
grid-area: aside;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
grid-area: main;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1210px) {
|
||||||
|
.container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in a new issue