Compare commits
No commits in common. "507ea37f74799784b82b3e9b517656d4ebb5e1c9" and "b55f6e25439e1dd7c7ee2906936d75d7c76183b3" have entirely different histories.
507ea37f74
...
b55f6e2543
21 changed files with 298 additions and 568 deletions
18
src/app.pcss
Normal file
18
src/app.pcss
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/* 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;
|
||||||
|
}
|
|
@ -5,6 +5,5 @@ export const TAG_TYPES = {
|
||||||
general: 'General',
|
general: 'General',
|
||||||
lore: 'Lore',
|
lore: 'Lore',
|
||||||
meta: 'Meta',
|
meta: 'Meta',
|
||||||
invalid: 'Invalid',
|
invalid: 'Invalid'
|
||||||
species: 'Species'
|
|
||||||
};
|
};
|
88
src/lib/components/AppSettings.svelte
Normal file
88
src/lib/components/AppSettings.svelte
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<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,7 +2,6 @@
|
||||||
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();
|
||||||
|
@ -29,6 +28,6 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button {href} >
|
<a {href} class="flex flex-row aspect-square place-content-center justify-center items-center bg-gray-600 h-10">
|
||||||
<Fa color="white" size={'1.5x'} icon={faArrowLeft} />
|
<Fa color="white" size={'1.5x'} icon={faArrowLeft} />
|
||||||
</Button>
|
</a>
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,58 +0,0 @@
|
||||||
<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,29 +1,22 @@
|
||||||
<script>
|
<script>
|
||||||
import {loading} from '$lib/stores.js';
|
import useSettings from '$lib/useSettings.svelte.js';
|
||||||
import {Button} from '$lib/components';
|
import {loading} from "$lib/stores.js";
|
||||||
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 = $lastSearch
|
searchString = settings.lastSearch
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form class="flex flex-row gap-2"
|
||||||
onsubmit={(e) => {
|
onsubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$lastSearch = searchString;
|
settings.lastSearch = searchString;
|
||||||
loading.set(true);
|
loading.set(true);
|
||||||
onsubmit?.(searchString, true);
|
onsubmit?.(searchString, true);
|
||||||
}}>
|
}}>
|
||||||
<input type="text" placeholder="tags" bind:value={searchString} />
|
<input class="h-10 p-2" type="text" placeholder="tags" bind:value={searchString} />
|
||||||
<Button type="submit">Search</Button>
|
<button class="h-10 p-2 text-white bg-gray-600" type="submit">Search</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<style>
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,12 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import {BackButton, GlobalSearch, LinkButton} from "$lib/components";
|
import {BackButton, GlobalSearch} from "$lib/components";
|
||||||
import {faCogs} from "@fortawesome/free-solid-svg-icons";
|
import {faArrowLeft, 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, gridArea = ""} = $props();
|
let {onsubmit = null, back = null, class: className, showSearch = true} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class={className} style:grid-area={gridArea}>
|
<header class="absolute w-full top-0 flex bg-transparent py-2 px-4 gap-1 {className}">
|
||||||
<div class="left">
|
|
||||||
{#if back}
|
{#if back}
|
||||||
<BackButton popCount={back} />
|
<BackButton popCount={back} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -15,25 +14,7 @@
|
||||||
onsubmit?.(val, explicitRefresh);
|
onsubmit?.(val, explicitRefresh);
|
||||||
}} />
|
}} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
<a href="/settings" class="flex flex-row aspect-square place-content-center justify-center items-center bg-gray-600 h-10">
|
||||||
<div class="right">
|
|
||||||
<LinkButton href="/settings/main">
|
|
||||||
<Fa color="white" size={'1.5x'} icon={faCogs} />
|
<Fa color="white" size={'1.5x'} icon={faCogs} />
|
||||||
</LinkButton>
|
</a>
|
||||||
</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,36 +1,25 @@
|
||||||
<script>
|
<script>
|
||||||
import { loading, imageGridColumns, postGridPreviewQuality } from "$lib/stores.js";
|
import useSettings from "$lib/useSettings.svelte.js";
|
||||||
|
import { loading } from "$lib/stores.js";
|
||||||
|
|
||||||
let { posts = [] } = $props();
|
let { posts = [] } = $props();
|
||||||
|
const settings = useSettings();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="post-grid" style={`grid-template-columns: repeat(${$imageGridColumns}, minmax(0, 1fr))`}>
|
<div class="grid gap-3" style={`grid-template-columns: repeat(${settings.imageGridColumns}, minmax(0, 1fr))`}>
|
||||||
{#each posts as post}
|
{#each posts as post}
|
||||||
<a href={`/posts/${post.id}`} class="post-grid-item" onclick={() => {
|
<a href={`/posts/${post.id}`} class="card p-3 flex flex-col justify-end items-center" onclick={() => {
|
||||||
loading.set(true);
|
loading.set(true);
|
||||||
}}>
|
}}>
|
||||||
<img src={post[$postGridPreviewQuality].url}
|
<header class="card-header p-0"></header>
|
||||||
|
<section class="flex flex-col">
|
||||||
|
<img src={post[settings.postGridPreviewQuality].url}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
alt={`Post ${post.id}`} />
|
alt={`Post ${post.id}`} />
|
||||||
<p class="post-info-bar">{post.id} {post.file.ext} {post.rating}</p>
|
<div class="bg-black px-2 py-1">
|
||||||
|
<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>
|
|
|
@ -1,27 +0,0 @@
|
||||||
<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,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import {limitImageHeight} from '$lib/stores.js';
|
import useSettings from '$lib/useSettings.svelte.js';
|
||||||
import {faCaretDown, faCaretUp, faCopy, faHeart, faStar} from "@fortawesome/free-solid-svg-icons";
|
const settings = useSettings();
|
||||||
import {Fa} from "svelte-fa";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Props
|
* @typedef {Object} Props
|
||||||
|
@ -14,7 +13,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if post?.preview?.has}
|
{#if post?.preview?.has}
|
||||||
<img style:max-height={$limitImageHeight ? '100vh' : 'none'} class={className} src={post.preview.url} alt={`Post ${post.id}`} />
|
<img class:max-h-dvh={settings.limitImageHeight} 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 => {
|
||||||
|
@ -30,72 +29,3 @@
|
||||||
}}/>
|
}}/>
|
||||||
{/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,19 +1,15 @@
|
||||||
|
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 Header from './Header.svelte';
|
|
||||||
import ImageGrid from './ImageGrid.svelte';
|
import ImageGrid from './ImageGrid.svelte';
|
||||||
import LinkButton from './LinkButton.svelte';
|
|
||||||
import PostMedia from './PostMedia.svelte';
|
import PostMedia from './PostMedia.svelte';
|
||||||
|
import Header from './Header.svelte';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
AppSettings,
|
||||||
BackButton,
|
BackButton,
|
||||||
Button,
|
|
||||||
Chips,
|
|
||||||
GlobalSearch,
|
GlobalSearch,
|
||||||
Header,
|
|
||||||
ImageGrid,
|
ImageGrid,
|
||||||
LinkButton,
|
|
||||||
PostMedia,
|
PostMedia,
|
||||||
|
Header,
|
||||||
}
|
}
|
|
@ -1,22 +1,4 @@
|
||||||
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)
|
|
52
src/lib/useSettings.svelte.js
Normal file
52
src/lib/useSettings.svelte.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
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,4 +1,5 @@
|
||||||
<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';
|
||||||
|
@ -6,59 +7,16 @@
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="bg-gray-200">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if $loading}
|
{#if $loading}
|
||||||
<div class="loading spin">
|
<div class="absolute bottom-4 right-4 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,14 +2,16 @@
|
||||||
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 {onMount} from 'svelte';
|
import useSettings from "$lib/useSettings.svelte.js";
|
||||||
import {loading, postSearchResults, blacklist, lastSearch} from '$lib/stores.js';
|
import {onMount} from "svelte";
|
||||||
|
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($blacklist).getPosts())
|
postSearchResults.set(new PostList(resPosts).filterPosts(settings.blacklist).getPosts())
|
||||||
loading.set(false)
|
loading.set(false)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -22,45 +24,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($postSearchResults === null) {
|
if ($postSearchResults === null) {
|
||||||
onSearch($lastSearch, true);
|
onSearch(settings.lastSearch, true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
<div class="container">
|
<Header onsubmit={(val = '', explicitRefresh = false) => {onSearch(val, explicitRefresh);}}></Header>
|
||||||
<Header gridArea="header" onsubmit={(val = '', explicitRefresh = false) => {onSearch(val, explicitRefresh);}}></Header>
|
|
||||||
<main>
|
<main>
|
||||||
{#if !$loading}
|
<div class="overflow-scroll p-2">
|
||||||
{#if $postSearchResults && $postSearchResults.length > 0}
|
{#if $postSearchResults}
|
||||||
<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,4 +1,6 @@
|
||||||
<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';
|
||||||
|
@ -17,80 +19,77 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="grid [grid-template-areas:'header_header_header''main_main_aside']">
|
||||||
<Header back={2} showSearch={false} onsubmit={(val = '') => {
|
<Header class="[grid-area: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 id="container-main">
|
<main class="[grid-area:main]">
|
||||||
{#if post}
|
{#if post}
|
||||||
<div class="post-main">
|
<div class="flex flex-col">
|
||||||
<PostMedia {post} />
|
<PostMedia class="max-h-[95vh] w-auto" {post} />
|
||||||
<footer id="post-footer">
|
<section class="flex flex-row justify-between bg-black">
|
||||||
<h3 style:text-decoration={post.description === '' ? 'strike-trough' : 'none' } >Description</h3>
|
<div class="flex flex-row px-2 gap-2">
|
||||||
<p>{post.description}</p>
|
<button class="py-2 px-3 flex flex-row place-items-center gap-1 text-white hover:text-black hover:bg-white">
|
||||||
|
<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 id="container-aside">
|
<aside class="[grid-area: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="tags-type-wrapper">
|
<div class="mb-2">
|
||||||
<p class="tag-type">{label}</p>
|
<p class="font-bold">{label}</p>
|
||||||
{#each post.tags[name] as tag}
|
{#each post.tags[name] as tag}
|
||||||
<p class="tag">{tag}</p>
|
<p>{tag}</p>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<footer id="container-footer">
|
|
||||||
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style lang="postcss"></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>
|
|
||||||
|
|
12
src/routes/settings/+page.svelte
Normal file
12
src/routes/settings/+page.svelte
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<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>
|
|
@ -1,35 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,48 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,43 +0,0 @@
|
||||||
<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