mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Fix: Local Source & QOL Animations
This commit is contained in:
@@ -1,3 +1,11 @@
|
|||||||
|
export const GET_LOCAL_MANGA = `
|
||||||
|
query GetLocalManga {
|
||||||
|
mangas(condition: { sourceId: "0" }) {
|
||||||
|
nodes { id title thumbnailUrl inLibrary }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const GET_EXTENSIONS = `
|
export const GET_EXTENSIONS = `
|
||||||
query GetExtensions {
|
query GetExtensions {
|
||||||
extensions {
|
extensions {
|
||||||
|
|||||||
@@ -19,6 +19,29 @@
|
|||||||
import TagTab from "./TagTab.svelte";
|
import TagTab from "./TagTab.svelte";
|
||||||
import SourceTab from "./SourceTab.svelte";
|
import SourceTab from "./SourceTab.svelte";
|
||||||
|
|
||||||
|
const anims = $derived(store.settings.qolAnimations ?? true);
|
||||||
|
|
||||||
|
const TABS = ["keyword", "tag", "source"] as const;
|
||||||
|
|
||||||
|
let tabsEl = $state<HTMLDivElement | undefined>(undefined);
|
||||||
|
let tabIndicator = $state({ left: 0, width: 0 });
|
||||||
|
|
||||||
|
function updateIndicator() {
|
||||||
|
if (!tabsEl) return;
|
||||||
|
const active = tabsEl.querySelector<HTMLElement>(".tab.tabActive");
|
||||||
|
if (!active) return;
|
||||||
|
const containerLeft = tabsEl.getBoundingClientRect().left;
|
||||||
|
tabIndicator = {
|
||||||
|
left: active.getBoundingClientRect().left - containerLeft,
|
||||||
|
width: active.offsetWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
tab; // reactive on tab change
|
||||||
|
if (anims) requestAnimationFrame(updateIndicator);
|
||||||
|
});
|
||||||
|
|
||||||
const SEARCH_PAGES = 3;
|
const SEARCH_PAGES = 3;
|
||||||
const SEARCH_LIMIT = 200;
|
const SEARCH_LIMIT = 200;
|
||||||
const SEARCH_BATCH = 20;
|
const SEARCH_BATCH = 20;
|
||||||
@@ -40,6 +63,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
let allSources: Source[] = $state([]);
|
let allSources: Source[] = $state([]);
|
||||||
|
let localSource: Source | null = $state(null);
|
||||||
let loadingSources = $state(false);
|
let loadingSources = $state(false);
|
||||||
|
|
||||||
const preferredLang = store.settings?.preferredExtensionLang ?? "en";
|
const preferredLang = store.settings?.preferredExtensionLang ?? "en";
|
||||||
@@ -49,7 +73,9 @@
|
|||||||
loadingSources = true;
|
loadingSources = true;
|
||||||
gql<{ sources: { nodes: Source[] } }>(GET_SOURCES)
|
gql<{ sources: { nodes: Source[] } }>(GET_SOURCES)
|
||||||
.then((d) => {
|
.then((d) => {
|
||||||
allSources = d.sources.nodes.filter((src: Source) => src.id !== "0");
|
const nodes = d.sources.nodes;
|
||||||
|
localSource = nodes.find((src: Source) => src.id === "0") ?? null;
|
||||||
|
allSources = nodes.filter((src: Source) => src.id !== "0");
|
||||||
startSourceCacheBuild();
|
startSourceCacheBuild();
|
||||||
popularStart(allSources);
|
popularStart(allSources);
|
||||||
})
|
})
|
||||||
@@ -230,10 +256,14 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root anim-fade-in">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1 class="heading">Search</h1>
|
<span class="heading">Search</span>
|
||||||
<div class="tabs">
|
|
||||||
|
<div class="tabs" class:tabs-anims={anims} bind:this={tabsEl}>
|
||||||
|
{#if anims && tabIndicator.width > 0}
|
||||||
|
<div class="tab-slide-indicator" style="left:{tabIndicator.left}px;width:{tabIndicator.width}px" aria-hidden="true"></div>
|
||||||
|
{/if}
|
||||||
<button class="tab" class:tabActive={tab === "keyword"} onclick={() => { deprioritizeQueue(); tab = "keyword"; }}>
|
<button class="tab" class:tabActive={tab === "keyword"} onclick={() => { deprioritizeQueue(); tab = "keyword"; }}>
|
||||||
<svg width="11" height="11" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
|
<svg width="11" height="11" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
|
||||||
<path d="M229.66,218.34l-50.07-50.07a88,88,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.31ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/>
|
<path d="M229.66,218.34l-50.07-50.07a88,88,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.31ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/>
|
||||||
@@ -281,6 +311,7 @@
|
|||||||
{allSources}
|
{allSources}
|
||||||
{availableLangs}
|
{availableLangs}
|
||||||
{loadingSources}
|
{loadingSources}
|
||||||
|
{localSource}
|
||||||
onPreview={setPreviewManga}
|
onPreview={setPreviewManga}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -288,11 +319,13 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
||||||
.header { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4) var(--sp-2); flex-shrink: 0; border-bottom: 1px solid var(--border-dim); gap: var(--sp-3); }
|
.header { position: relative; z-index: 100; display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-4) var(--sp-6); flex-shrink: 0; border-bottom: 1px solid var(--border-dim); }
|
||||||
.heading { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; flex-shrink: 0; }
|
.heading { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; flex-shrink: 0; }
|
||||||
.tabs { display: flex; gap: var(--sp-1); }
|
.tabs { display: flex; gap: 2px; background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 2px; position: relative; }
|
||||||
.tab { display: flex; align-items: center; gap: var(--sp-1); padding: 5px 10px; border-radius: var(--radius-md); border: 1px solid transparent; background: none; font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); color: var(--text-faint); cursor: pointer; transition: color var(--t-base), background var(--t-base), border-color var(--t-base); }
|
.tab-slide-indicator { position: absolute; top: 2px; bottom: 2px; border-radius: var(--radius-sm); background: var(--accent-muted); border: 1px solid var(--accent-dim); pointer-events: none; z-index: 0; transition: left 0.22s cubic-bezier(0.16,1,0.3,1), width 0.22s cubic-bezier(0.16,1,0.3,1); }
|
||||||
.tab:hover { color: var(--text-muted); background: var(--bg-raised); }
|
.tab { position: relative; z-index: 1; display: flex; align-items: center; gap: 5px; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 4px 10px; border-radius: var(--radius-sm); color: var(--text-faint); white-space: nowrap; transition: background var(--t-base), color var(--t-base); cursor: pointer; border: 1px solid transparent; }
|
||||||
|
.tab:hover { color: var(--text-muted); }
|
||||||
.tabActive { color: var(--accent-fg); background: var(--accent-muted); border-color: var(--accent-dim); }
|
.tabActive { color: var(--accent-fg); background: var(--accent-muted); border-color: var(--accent-dim); }
|
||||||
.tabActive:hover { color: var(--accent-fg); background: var(--accent-muted); }
|
.tabs-anims .tabActive { background: transparent; border-color: transparent; }
|
||||||
|
.tabActive:hover { color: var(--accent-fg); }
|
||||||
</style>
|
</style>
|
||||||
@@ -11,9 +11,10 @@
|
|||||||
allSources: Source[];
|
allSources: Source[];
|
||||||
availableLangs: string[];
|
availableLangs: string[];
|
||||||
loadingSources: boolean;
|
loadingSources: boolean;
|
||||||
|
localSource: Source | null;
|
||||||
onPreview: (m: Manga) => void;
|
onPreview: (m: Manga) => void;
|
||||||
}
|
}
|
||||||
let { allSources, availableLangs, loadingSources, onPreview }: Props = $props();
|
let { allSources, availableLangs, loadingSources, localSource, onPreview }: Props = $props();
|
||||||
|
|
||||||
const preferredLang = store.settings?.preferredExtensionLang ?? "en";
|
const preferredLang = store.settings?.preferredExtensionLang ?? "en";
|
||||||
|
|
||||||
@@ -116,6 +117,21 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="splitList">
|
<div class="splitList">
|
||||||
|
{#if localSource}
|
||||||
|
<button
|
||||||
|
class="splitItem splitItemSource"
|
||||||
|
class:splitItemActive={src_activeSource?.id === localSource.id}
|
||||||
|
onclick={() => srcSelectSource(localSource)}
|
||||||
|
>
|
||||||
|
<div class="localSourceIcon">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm44-84a44,44,0,1,1-44-44A44.05,44.05,0,0,1,172,128Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="splitItemLabel">Local Source</span>
|
||||||
|
</button>
|
||||||
|
<div class="localDivider"></div>
|
||||||
|
{/if}
|
||||||
{#each src_visibleSources as src (src.id)}
|
{#each src_visibleSources as src (src.id)}
|
||||||
<button
|
<button
|
||||||
class="splitItem splitItemSource"
|
class="splitItem splitItemSource"
|
||||||
@@ -230,6 +246,8 @@
|
|||||||
.langSelect option { background: var(--bg-surface); color: var(--text-secondary); }
|
.langSelect option { background: var(--bg-surface); color: var(--text-secondary); }
|
||||||
.splitLoading { flex: 1; display: flex; align-items: center; justify-content: center; padding: var(--sp-6); }
|
.splitLoading { flex: 1; display: flex; align-items: center; justify-content: center; padding: var(--sp-6); }
|
||||||
.splitList { flex: 1; overflow-y: auto; padding: var(--sp-1); scrollbar-width: thin; scrollbar-color: var(--border-dim) transparent; }
|
.splitList { flex: 1; overflow-y: auto; padding: var(--sp-1); scrollbar-width: thin; scrollbar-color: var(--border-dim) transparent; }
|
||||||
|
.localSourceIcon { width: 20px; height: 20px; border-radius: var(--radius-sm); background: var(--accent-muted); border: 1px solid var(--accent-dim); display: flex; align-items: center; justify-content: center; color: var(--accent-fg); flex-shrink: 0; }
|
||||||
|
.localDivider { height: 1px; background: var(--border-dim); margin: var(--sp-1) var(--sp-2); }
|
||||||
.splitItem { display: flex; align-items: center; gap: var(--sp-2); width: 100%; padding: 7px var(--sp-3); border-radius: var(--radius-md); border: 1px solid transparent; background: none; text-align: left; cursor: pointer; transition: background var(--t-fast), border-color var(--t-fast); }
|
.splitItem { display: flex; align-items: center; gap: var(--sp-2); width: 100%; padding: 7px var(--sp-3); border-radius: var(--radius-md); border: 1px solid transparent; background: none; text-align: left; cursor: pointer; transition: background var(--t-fast), border-color var(--t-fast); }
|
||||||
.splitItem:hover { background: var(--bg-raised); border-color: var(--border-dim); }
|
.splitItem:hover { background: var(--bg-raised); border-color: var(--border-dim); }
|
||||||
.splitItemActive { background: var(--accent-muted); border-color: var(--accent-dim); }
|
.splitItemActive { background: var(--accent-muted); border-color: var(--accent-dim); }
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
updateCount: number;
|
updateCount: number;
|
||||||
availableLangs: string[];
|
availableLangs: string[];
|
||||||
langFilter: string | null;
|
langFilter: string | null;
|
||||||
|
anims: boolean;
|
||||||
|
tabIndicator: { left: number; width: number };
|
||||||
|
tabsEl: HTMLDivElement | undefined;
|
||||||
onFilter: (f: Filter) => void;
|
onFilter: (f: Filter) => void;
|
||||||
onSearch: (q: string) => void;
|
onSearch: (q: string) => void;
|
||||||
onLang: (lang: string | null) => void;
|
onLang: (lang: string | null) => void;
|
||||||
@@ -20,6 +23,8 @@
|
|||||||
let {
|
let {
|
||||||
filter, search, panel, refreshing, updateCount,
|
filter, search, panel, refreshing, updateCount,
|
||||||
availableLangs, langFilter,
|
availableLangs, langFilter,
|
||||||
|
anims, tabIndicator,
|
||||||
|
tabsEl = $bindable(),
|
||||||
onFilter, onSearch, onLang, onPanel, onRefresh,
|
onFilter, onSearch, onLang, onPanel, onRefresh,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
@@ -27,7 +32,10 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<h1 class="heading">Extensions</h1>
|
<h1 class="heading">Extensions</h1>
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs" class:tabs-anims={anims} bind:this={tabsEl}>
|
||||||
|
{#if anims && tabIndicator.width > 0}
|
||||||
|
<div class="tab-slide-indicator" style="left:{tabIndicator.left}px;width:{tabIndicator.width}px" aria-hidden="true"></div>
|
||||||
|
{/if}
|
||||||
{#each FILTERS as f}
|
{#each FILTERS as f}
|
||||||
<button class="tab" class:active={filter === f.id} onclick={() => onFilter(f.id)}>
|
<button class="tab" class:active={filter === f.id} onclick={() => onFilter(f.id)}>
|
||||||
{f.id === "updates" && updateCount > 0 ? `Updates (${updateCount})` : f.label}
|
{f.id === "updates" && updateCount > 0 ? `Updates (${updateCount})` : f.label}
|
||||||
@@ -71,17 +79,19 @@
|
|||||||
.icon-btn:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
|
.icon-btn:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
|
||||||
.icon-btn:disabled { opacity: 0.4; }
|
.icon-btn:disabled { opacity: 0.4; }
|
||||||
.icon-btn.active { color: var(--accent-fg); background: var(--accent-muted); }
|
.icon-btn.active { color: var(--accent-fg); background: var(--accent-muted); }
|
||||||
.tabs { display: flex; gap: 2px; background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 2px; }
|
.tabs { display: flex; gap: 2px; background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 2px; position: relative; }
|
||||||
.tab { display: flex; align-items: center; gap: 5px; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 4px 10px; border-radius: var(--radius-sm); color: var(--text-faint); white-space: nowrap; transition: background var(--t-base), color var(--t-base); }
|
.tab-slide-indicator { position: absolute; top: 2px; bottom: 2px; border-radius: var(--radius-sm); background: var(--accent-muted); border: 1px solid var(--accent-dim); pointer-events: none; z-index: 0; transition: left 0.22s cubic-bezier(0.16,1,0.3,1), width 0.22s cubic-bezier(0.16,1,0.3,1); }
|
||||||
|
.tab { position: relative; z-index: 1; display: flex; align-items: center; gap: 5px; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 4px 10px; border-radius: var(--radius-sm); color: var(--text-faint); white-space: nowrap; transition: background var(--t-base), color var(--t-base); }
|
||||||
.tab:hover { color: var(--text-muted); }
|
.tab:hover { color: var(--text-muted); }
|
||||||
.tab.active { background: var(--accent-muted); color: var(--accent-fg); border: 1px solid var(--accent-dim); }
|
.tab.active { background: var(--accent-muted); color: var(--accent-fg); border: 1px solid var(--accent-dim); }
|
||||||
|
.tabs-anims .tab.active { background: transparent; border-color: transparent; }
|
||||||
.search-wrap { position: relative; display: flex; align-items: center; }
|
.search-wrap { position: relative; display: flex; align-items: center; }
|
||||||
.search-wrap :global(.search-icon) { position: absolute; left: 9px; color: var(--text-faint); pointer-events: none; }
|
.search-wrap :global(.search-icon) { position: absolute; left: 9px; color: var(--text-faint); pointer-events: none; }
|
||||||
.search { background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 5px 10px 5px 26px; color: var(--text-primary); font-size: var(--text-sm); width: 160px; outline: none; transition: border-color var(--t-base); }
|
.search { background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 5px 10px 5px 26px; color: var(--text-primary); font-size: var(--text-sm); width: 160px; outline: none; transition: border-color var(--t-base); }
|
||||||
.search::placeholder { color: var(--text-faint); }
|
.search::placeholder { color: var(--text-faint); }
|
||||||
.search:focus { border-color: var(--border-strong); }
|
.search:focus { border-color: var(--border-strong); }
|
||||||
.lang-bar { display: flex; align-items: center; gap: 4px; padding: var(--sp-2) var(--sp-6); flex-shrink: 0; flex-wrap: wrap; border-bottom: 1px solid var(--border-dim); }
|
.lang-bar { display: flex; align-items: center; gap: 4px; padding: var(--sp-2) var(--sp-6); flex-shrink: 0; flex-wrap: wrap; border-bottom: 1px solid var(--border-dim); }
|
||||||
.lang-pill { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wider); text-transform: uppercase; padding: 3px 9px; border-radius: var(--radius-full); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); cursor: pointer; transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast); }
|
.lang-pill { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wider); text-transform: uppercase; padding: 3px 9px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); cursor: pointer; transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast); }
|
||||||
.lang-pill:hover { color: var(--text-muted); border-color: var(--border-strong); background: var(--bg-raised); }
|
.lang-pill:hover { color: var(--text-muted); border-color: var(--border-strong); background: var(--bg-raised); }
|
||||||
.lang-pill.active { background: var(--accent-muted); border-color: var(--accent-dim); color: var(--accent-fg); }
|
.lang-pill.active { background: var(--accent-muted); border-color: var(--accent-dim); color: var(--accent-fg); }
|
||||||
</style>
|
</style>
|
||||||
@@ -1,16 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { untrack } from "svelte";
|
import { untrack } from "svelte";
|
||||||
import { CircleNotch, X, Check } from "phosphor-svelte";
|
import { CircleNotch, X, Check, HardDrives } from "phosphor-svelte";
|
||||||
import { gql } from "@api/client";
|
import { gql } from "@api/client";
|
||||||
import { store, addToast } from "@store/state.svelte";
|
import { store, addToast } from "@store/state.svelte";
|
||||||
import { GET_EXTENSIONS, GET_SETTINGS } from "@api/queries";
|
import { GET_EXTENSIONS, GET_SETTINGS, GET_LOCAL_MANGA } from "@api/queries";
|
||||||
import { SET_EXTENSION_REPOS, INSTALL_EXTERNAL_EXTENSION, FETCH_EXTENSIONS, UPDATE_EXTENSION } from "@api/mutations";
|
import { SET_EXTENSION_REPOS, INSTALL_EXTERNAL_EXTENSION, FETCH_EXTENSIONS, UPDATE_EXTENSION } from "@api/mutations";
|
||||||
import type { Extension } from "@types/index";
|
import type { Extension } from "@types/index";
|
||||||
import { matchesFilter, groupExtensions, validateUrl, type Filter, type Panel } from "../lib/extensionHelpers";
|
import { matchesFilter, groupExtensions, validateUrl, type Filter, type Panel } from "../lib/extensionHelpers";
|
||||||
import ExtensionFilters from "./ExtensionFilters.svelte";
|
import ExtensionFilters from "./ExtensionFilters.svelte";
|
||||||
import ExtensionCard from "./ExtensionCard.svelte";
|
import ExtensionCard from "./ExtensionCard.svelte";
|
||||||
|
|
||||||
|
const anims = $derived(store.settings.qolAnimations ?? true);
|
||||||
|
let tabsEl = $state<HTMLDivElement | undefined>(undefined);
|
||||||
|
let tabIndicator = $state({ left: 0, width: 0 });
|
||||||
|
|
||||||
|
function updateIndicator() {
|
||||||
|
if (!tabsEl) return;
|
||||||
|
const active = tabsEl.querySelector<HTMLElement>(".tab.active");
|
||||||
|
if (!active) return;
|
||||||
|
const containerLeft = tabsEl.getBoundingClientRect().left;
|
||||||
|
tabIndicator = { left: active.getBoundingClientRect().left - containerLeft, width: active.offsetWidth };
|
||||||
|
}
|
||||||
|
|
||||||
let extensions: Extension[] = $state([]);
|
let extensions: Extension[] = $state([]);
|
||||||
|
let localMangaCount = $state(0);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let refreshing = $state(false);
|
let refreshing = $state(false);
|
||||||
let filter = $state<Filter>("installed");
|
let filter = $state<Filter>("installed");
|
||||||
@@ -20,6 +33,8 @@
|
|||||||
let expanded = $state(new Set<string>());
|
let expanded = $state(new Set<string>());
|
||||||
let panel = $state<Panel>(null);
|
let panel = $state<Panel>(null);
|
||||||
|
|
||||||
|
$effect(() => { filter; if (anims) requestAnimationFrame(() => requestAnimationFrame(updateIndicator)); });
|
||||||
|
|
||||||
let externalUrl = $state("");
|
let externalUrl = $state("");
|
||||||
let installing = $state(false);
|
let installing = $state(false);
|
||||||
let installError = $state<string | null>(null);
|
let installError = $state<string | null>(null);
|
||||||
@@ -36,6 +51,11 @@
|
|||||||
if (d) extensions = d.extensions.nodes;
|
if (d) extensions = d.extensions.nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadLocalManga() {
|
||||||
|
const d = await gql<{ mangas: { nodes: { id: number }[] } }>(GET_LOCAL_MANGA).catch(console.error);
|
||||||
|
if (d) localMangaCount = d.mangas.nodes.length;
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchFromRepo() {
|
async function fetchFromRepo() {
|
||||||
refreshing = true;
|
refreshing = true;
|
||||||
const d = await gql<{ fetchExtensions: { extensions: Extension[] } }>(FETCH_EXTENSIONS)
|
const d = await gql<{ fetchExtensions: { extensions: Extension[] } }>(FETCH_EXTENSIONS)
|
||||||
@@ -128,30 +148,44 @@
|
|||||||
expanded = next;
|
expanded = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFilter(f: Filter) { filter = f; langFilter = null; }
|
function setFilter(f: Filter) {
|
||||||
|
if (f === filter) return;
|
||||||
|
filter = f;
|
||||||
|
langFilter = null;
|
||||||
|
}
|
||||||
|
|
||||||
const filtered = $derived(extensions.filter((e) => {
|
const showLocal = $derived(
|
||||||
|
(filter === "installed" || filter === "all") &&
|
||||||
|
(search === "" || "local source".includes(search.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
|
const allGroups = $derived(groupExtensions(extensions, store.settings.preferredExtensionLang));
|
||||||
|
|
||||||
|
const groups = $derived(allGroups.filter(({ primary, variants }) => {
|
||||||
|
const all = [primary, ...variants];
|
||||||
const q = search.toLowerCase();
|
const q = search.toLowerCase();
|
||||||
return (e.name.toLowerCase().includes(q) || e.lang.toLowerCase().includes(q))
|
const matchesSearch = all.some((e) => e.name.toLowerCase().includes(q) || e.lang.toLowerCase().includes(q));
|
||||||
&& matchesFilter(e, filter)
|
const matchesTab = all.some((e) => matchesFilter(e, filter));
|
||||||
&& (langFilter === null || e.lang === langFilter);
|
const matchesLang = langFilter === null || all.some((e) => e.lang === langFilter);
|
||||||
|
return matchesSearch && matchesTab && matchesLang;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const availableLangs = $derived(
|
const availableLangs = $derived(
|
||||||
[...new Set(extensions.filter((e) => matchesFilter(e, filter)).map((e) => e.lang))].sort()
|
[...new Set(extensions.filter((e) => matchesFilter(e, filter)).map((e) => e.lang))].sort()
|
||||||
);
|
);
|
||||||
|
|
||||||
const groups = $derived(groupExtensions(filtered, store.settings.preferredExtensionLang));
|
|
||||||
const updateCount = $derived(extensions.filter((e) => e.hasUpdate).length);
|
const updateCount = $derived(extensions.filter((e) => e.hasUpdate).length);
|
||||||
|
|
||||||
$effect(() => { untrack(() => fetchFromRepo().finally(() => { loading = false; })); });
|
$effect(() => { untrack(() => { loadLocalManga(); fetchFromRepo().finally(() => { loading = false; }); }); });
|
||||||
|
|
||||||
function focusOnMount(node: HTMLElement) { node.focus(); }
|
function focusOnMount(node: HTMLElement) { node.focus(); }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root anim-fade-in">
|
||||||
<ExtensionFilters
|
<ExtensionFilters
|
||||||
{filter} {search} {panel} {refreshing} {updateCount} {availableLangs} {langFilter}
|
{filter} {search} {panel} {refreshing} {updateCount} {availableLangs} {langFilter}
|
||||||
|
{anims} {tabIndicator}
|
||||||
|
bind:tabsEl
|
||||||
onFilter={setFilter}
|
onFilter={setFilter}
|
||||||
onSearch={(q) => search = q}
|
onSearch={(q) => search = q}
|
||||||
onLang={(l) => langFilter = l}
|
onLang={(l) => langFilter = l}
|
||||||
@@ -160,7 +194,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{#if panel === "apk"}
|
{#if panel === "apk"}
|
||||||
<div class="ext-panel">
|
<div class="ext-panel anim-fade-in">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-title">Install from APK URL</span>
|
<span class="panel-title">Install from APK URL</span>
|
||||||
<button class="icon-btn" onclick={() => panel = null}><X size={14} weight="light" /></button>
|
<button class="icon-btn" onclick={() => panel = null}><X size={14} weight="light" /></button>
|
||||||
@@ -186,7 +220,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if panel === "repos"}
|
{#if panel === "repos"}
|
||||||
<div class="ext-panel">
|
<div class="ext-panel anim-fade-in">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-title">Extension Repositories</span>
|
<span class="panel-title">Extension Repositories</span>
|
||||||
<button class="icon-btn" onclick={() => panel = null}><X size={14} weight="light" /></button>
|
<button class="icon-btn" onclick={() => panel = null}><X size={14} weight="light" /></button>
|
||||||
@@ -227,10 +261,18 @@
|
|||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="empty"><CircleNotch size={16} weight="light" class="anim-spin" style="color:var(--text-faint)" /></div>
|
<div class="empty"><CircleNotch size={16} weight="light" class="anim-spin" style="color:var(--text-faint)" /></div>
|
||||||
{:else if groups.length === 0}
|
|
||||||
<div class="empty">No extensions found.</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="list">
|
<div class="list">
|
||||||
|
{#if showLocal}
|
||||||
|
<div class="local-row">
|
||||||
|
<div class="local-icon"><HardDrives size={18} weight="bold" /></div>
|
||||||
|
<div class="info">
|
||||||
|
<span class="name">Local Source</span>
|
||||||
|
<span class="meta">Built-in · {localMangaCount} {localMangaCount === 1 ? "manga" : "manga"}</span>
|
||||||
|
</div>
|
||||||
|
<span class="local-badge">Built-in</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#each groups as { base, primary, variants }}
|
{#each groups as { base, primary, variants }}
|
||||||
<ExtensionCard
|
<ExtensionCard
|
||||||
{base} {primary} {variants} {working}
|
{base} {primary} {variants} {working}
|
||||||
@@ -239,17 +281,20 @@
|
|||||||
onMutate={mutate}
|
onMutate={mutate}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if !showLocal && groups.length === 0}
|
||||||
|
<div class="empty" style="flex:1">No extensions found.</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; animation: fadeIn 0.14s ease both; }
|
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
||||||
.list { flex: 1; overflow-y: auto; padding: 0 var(--sp-4) var(--sp-4); display: flex; flex-direction: column; gap: 1px; }
|
.list { flex: 1; overflow-y: auto; padding: 0 var(--sp-4) var(--sp-4); display: flex; flex-direction: column; gap: 1px; }
|
||||||
.empty { display: flex; align-items: center; justify-content: center; flex: 1; color: var(--text-faint); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); }
|
.empty { display: flex; align-items: center; justify-content: center; flex: 1; color: var(--text-faint); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); }
|
||||||
.icon-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); color: var(--text-muted); transition: color var(--t-base), background var(--t-base); }
|
.icon-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); color: var(--text-muted); transition: color var(--t-base), background var(--t-base); }
|
||||||
.icon-btn:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
|
.icon-btn:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
|
||||||
.ext-panel { display: flex; flex-direction: column; gap: var(--sp-2); padding: 0 var(--sp-6) var(--sp-3); flex-shrink: 0; animation: fadeIn 0.1s ease both; }
|
.ext-panel { display: flex; flex-direction: column; gap: var(--sp-2); padding: 0 var(--sp-6) var(--sp-3); flex-shrink: 0; }
|
||||||
.panel-header { display: flex; align-items: center; justify-content: space-between; }
|
.panel-header { display: flex; align-items: center; justify-content: space-between; }
|
||||||
.panel-title { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); }
|
.panel-title { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); }
|
||||||
.panel-error { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--color-error); letter-spacing: var(--tracking-wide); padding: 0 2px; }
|
.panel-error { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--color-error); letter-spacing: var(--tracking-wide); padding: 0 2px; }
|
||||||
@@ -269,4 +314,11 @@
|
|||||||
.repo-url { flex: 1; font-size: var(--text-2xs); color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.repo-url { flex: 1; font-size: var(--text-2xs); color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.repo-remove { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: var(--radius-sm); color: var(--text-faint); flex-shrink: 0; transition: color var(--t-base), background var(--t-base); }
|
.repo-remove { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: var(--radius-sm); color: var(--text-faint); flex-shrink: 0; transition: color var(--t-base), background var(--t-base); }
|
||||||
.repo-remove:hover:not(:disabled) { color: var(--color-error); background: var(--bg-overlay); }
|
.repo-remove:hover:not(:disabled) { color: var(--color-error); background: var(--bg-overlay); }
|
||||||
|
.local-row { display: flex; align-items: center; gap: var(--sp-3); padding: 8px var(--sp-3); border-radius: var(--radius-md); border: 1px solid transparent; transition: background var(--t-fast), border-color var(--t-fast); margin-bottom: 1px; }
|
||||||
|
.local-row:hover { background: var(--bg-raised); border-color: var(--border-dim); }
|
||||||
|
.local-icon { width: 32px; height: 32px; border-radius: var(--radius-md); background: var(--accent-muted); border: 1px solid var(--accent-dim); display: flex; align-items: center; justify-content: center; color: var(--accent-fg); flex-shrink: 0; }
|
||||||
|
.info { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow: hidden; }
|
||||||
|
.name { font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.meta { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
|
||||||
|
.local-badge { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 3px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); color: var(--text-faint); flex-shrink: 0; }
|
||||||
</style>
|
</style>
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
<SortAscending size={15} weight="bold" />
|
<SortAscending size={15} weight="bold" />
|
||||||
</button>
|
</button>
|
||||||
{#if sortPanelOpen}
|
{#if sortPanelOpen}
|
||||||
<div class="dropdown-panel sort-panel" role="menu">
|
<div class="dropdown-panel sort-panel anim-fade-in" role="menu">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-heading">Sort</span>
|
<span class="panel-heading">Sort</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
.refresh-progress { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--accent-fg); }
|
.refresh-progress { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--accent-fg); }
|
||||||
.refresh-btn-done { color: var(--color-success, #5cae6e) !important; border-color: color-mix(in srgb, var(--color-success, #5cae6e) 40%, transparent) !important; background: color-mix(in srgb, var(--color-success, #5cae6e) 10%, transparent) !important; }
|
.refresh-btn-done { color: var(--color-success, #5cae6e) !important; border-color: color-mix(in srgb, var(--color-success, #5cae6e) 40%, transparent) !important; background: color-mix(in srgb, var(--color-success, #5cae6e) 10%, transparent) !important; }
|
||||||
.sort-panel-wrap { position: relative; }
|
.sort-panel-wrap { position: relative; }
|
||||||
.dropdown-panel { position: absolute; top: calc(100% + 6px); right: 0; z-index: 9999; min-width: 220px; background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-lg); padding: var(--sp-1); box-shadow: 0 8px 32px rgba(0,0,0,0.5); animation: fadeIn 0.1s ease both; }
|
.dropdown-panel { position: absolute; top: calc(100% + 6px); right: 0; z-index: 9999; min-width: 220px; background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-lg); padding: var(--sp-1); box-shadow: 0 8px 32px rgba(0,0,0,0.5); }
|
||||||
.panel-label { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wider); text-transform: uppercase; color: var(--text-faint); padding: 4px 8px 8px; }
|
.panel-label { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wider); text-transform: uppercase; color: var(--text-faint); padding: 4px 8px 8px; }
|
||||||
.panel-item { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 7px 10px; border-radius: var(--radius-sm); border: none; background: transparent; color: var(--text-muted); font-family: var(--font-ui); font-size: var(--text-xs); cursor: pointer; text-align: left; transition: background var(--t-base), color var(--t-base); gap: var(--sp-2); }
|
.panel-item { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 7px 10px; border-radius: var(--radius-sm); border: none; background: transparent; color: var(--text-muted); font-family: var(--font-ui); font-size: var(--text-xs); cursor: pointer; text-align: left; transition: background var(--t-base), color var(--t-base); gap: var(--sp-2); }
|
||||||
.panel-item:hover { background: var(--bg-overlay); color: var(--text-primary); }
|
.panel-item:hover { background: var(--bg-overlay); color: var(--text-primary); }
|
||||||
@@ -237,5 +237,4 @@
|
|||||||
.panel-check-on { background: var(--accent); border-color: var(--accent); }
|
.panel-check-on { background: var(--accent); border-color: var(--accent); }
|
||||||
.dir-toggle { color: var(--text-secondary); justify-content: flex-start; gap: var(--sp-2); border-top: 1px solid var(--border-dim); border-radius: 0 0 var(--radius-sm) var(--radius-sm); margin-top: 2px; padding-top: 9px; }
|
.dir-toggle { color: var(--text-secondary); justify-content: flex-start; gap: var(--sp-2); border-top: 1px solid var(--border-dim); border-radius: 0 0 var(--radius-sm) var(--radius-sm); margin-top: 2px; padding-top: 9px; }
|
||||||
:global(.sort-caret) { flex-shrink: 0; }
|
:global(.sort-caret) { flex-shrink: 0; }
|
||||||
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
|
|
||||||
</style>
|
</style>
|
||||||
@@ -618,7 +618,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--sp-2);
|
gap: var(--sp-2);
|
||||||
|
border-bottom: 1px solid var(--border-dim);
|
||||||
}
|
}
|
||||||
|
.s-storage-wrap:last-child { border-bottom: none; }
|
||||||
|
|
||||||
.s-storage-header {
|
.s-storage-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -146,7 +146,17 @@
|
|||||||
|
|
||||||
async function browseDownloadsFolder() {
|
async function browseDownloadsFolder() {
|
||||||
const picked = await invoke<string | null>("pick_downloads_folder");
|
const picked = await invoke<string | null>("pick_downloads_folder");
|
||||||
if (picked) { downloadsPathInput = picked; pathsFieldError = { ...pathsFieldError, dl: undefined }; }
|
if (picked) { downloadsPathInput = picked; pathsFieldError = { ...pathsFieldError, dl: undefined }; await savePaths(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function browseLocalSourceFolder() {
|
||||||
|
const picked = await invoke<string | null>("pick_downloads_folder");
|
||||||
|
if (picked) { localSourcePathInput = picked; pathsFieldError = { ...pathsFieldError, loc: undefined }; await savePaths(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function browseExtraScanDir() {
|
||||||
|
const picked = await invoke<string | null>("pick_downloads_folder");
|
||||||
|
if (picked) { newScanDir = picked; addExtraScanDir(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function addExtraScanDir() {
|
function addExtraScanDir() {
|
||||||
@@ -414,9 +424,11 @@
|
|||||||
catch (e: any) { pathsFieldError = { ...pathsFieldError, dl: e?.message ?? "Failed" }; }
|
catch (e: any) { pathsFieldError = { ...pathsFieldError, dl: e?.message ?? "Failed" }; }
|
||||||
}}>Create</button>
|
}}>Create</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button class="s-btn s-btn-accent" onclick={savePaths} disabled={pathsSaving}>
|
{#if downloadsPathInput.trim() !== confirmedDownloadsPath}
|
||||||
{pathsSaved ? "Saved ✓" : pathsSaving ? "Saving…" : "Save"}
|
<button class="s-btn s-btn-accent" onclick={savePaths} disabled={pathsSaving}>
|
||||||
</button>
|
{pathsSaved ? "Saved ✓" : pathsSaving ? "Saving…" : "Save"}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -465,6 +477,9 @@
|
|||||||
bind:value={localSourcePathInput} placeholder="Optional" spellcheck="false"
|
bind:value={localSourcePathInput} placeholder="Optional" spellcheck="false"
|
||||||
onkeydown={(e) => e.key === "Enter" && savePaths()}
|
onkeydown={(e) => e.key === "Enter" && savePaths()}
|
||||||
oninput={() => { pathsFieldError = { ...pathsFieldError, loc: undefined }; }} />
|
oninput={() => { pathsFieldError = { ...pathsFieldError, loc: undefined }; }} />
|
||||||
|
{#if !isExternalServer}
|
||||||
|
<button class="s-btn" onclick={browseLocalSourceFolder}>Browse</button>
|
||||||
|
{/if}
|
||||||
{#if pathsFieldError.loc && !isExternalServer}
|
{#if pathsFieldError.loc && !isExternalServer}
|
||||||
<button class="s-btn" onclick={async () => {
|
<button class="s-btn" onclick={async () => {
|
||||||
try { await createDirectory(localSourcePathInput.trim()); pathsFieldError = { ...pathsFieldError, loc: undefined }; }
|
try { await createDirectory(localSourcePathInput.trim()); pathsFieldError = { ...pathsFieldError, loc: undefined }; }
|
||||||
@@ -494,16 +509,13 @@
|
|||||||
<div class="s-btn-row">
|
<div class="s-btn-row">
|
||||||
<input class="s-input mono" bind:value={newScanDir} placeholder="/path/to/dir" spellcheck="false"
|
<input class="s-input mono" bind:value={newScanDir} placeholder="/path/to/dir" spellcheck="false"
|
||||||
onkeydown={(e) => e.key === "Enter" && addExtraScanDir()} />
|
onkeydown={(e) => e.key === "Enter" && addExtraScanDir()} />
|
||||||
<button class="s-btn" onclick={addExtraScanDir} disabled={!newScanDir.trim() || extraScanDirs.includes(newScanDir.trim())}>Add</button>
|
{#if !isExternalServer}
|
||||||
|
<button class="s-btn" onclick={browseExtraScanDir}>Browse</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="s-row">
|
|
||||||
<div class="s-row-info"></div>
|
|
||||||
<button class="s-btn s-btn-accent" onclick={savePaths} disabled={pathsSaving}>
|
|
||||||
{pathsSaved ? "Saved ✓" : pathsSaving ? "Saving…" : "Save"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
{#if boot.unsupportedMode}
|
{#if boot.unsupportedMode}
|
||||||
<div class="auth-overlay">
|
<div class="auth-overlay">
|
||||||
<div class="auth-card">
|
<div class="auth-card anim-scale-in">
|
||||||
<img src={logoUrl} alt="Moku" class="auth-logo" />
|
<img src={logoUrl} alt="Moku" class="auth-logo" />
|
||||||
<p class="auth-title">moku</p>
|
<p class="auth-title">moku</p>
|
||||||
<span class="auth-mode-badge auth-mode-badge--warn">{
|
<span class="auth-mode-badge auth-mode-badge--warn">{
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if boot.loginRequired}
|
{:else if boot.loginRequired}
|
||||||
<div class="auth-overlay">
|
<div class="auth-overlay">
|
||||||
<div class="auth-card">
|
<div class="auth-card anim-scale-in">
|
||||||
<img src={logoUrl} alt="Moku" class="auth-logo" />
|
<img src={logoUrl} alt="Moku" class="auth-logo" />
|
||||||
<p class="auth-title">moku</p>
|
<p class="auth-title">moku</p>
|
||||||
<span class="auth-mode-badge">Basic Auth</span>
|
<span class="auth-mode-badge">Basic Auth</span>
|
||||||
@@ -62,8 +62,7 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.auth-overlay { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center; justify-content: center; pointer-events: none; }
|
.auth-overlay { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center; justify-content: center; pointer-events: none; }
|
||||||
.auth-card { pointer-events: auto; width: min(280px, calc(100vw - 48px)); background: var(--bg-surface); border: 1px solid var(--border-base); border-radius: var(--radius-xl); padding: var(--sp-6) var(--sp-5); display: flex; flex-direction: column; align-items: center; gap: var(--sp-3); box-shadow: 0 32px 80px rgba(0,0,0,0.75); animation: authIn 0.28s cubic-bezier(0.16,1,0.3,1) both; text-align: center; }
|
.auth-card { pointer-events: auto; width: min(280px, calc(100vw - 48px)); background: var(--bg-surface); border: 1px solid var(--border-base); border-radius: var(--radius-xl); padding: var(--sp-6) var(--sp-5); display: flex; flex-direction: column; align-items: center; gap: var(--sp-3); box-shadow: 0 32px 80px rgba(0,0,0,0.75); text-align: center; }
|
||||||
@keyframes authIn { from { opacity: 0; transform: translateY(10px) scale(0.97); } to { opacity: 1; transform: none; } }
|
|
||||||
|
|
||||||
.auth-logo { width: 56px; height: 56px; border-radius: 14px; display: block; }
|
.auth-logo { width: 56px; height: 56px; border-radius: 14px; display: block; }
|
||||||
.auth-title { font-family: var(--font-ui); font-size: 11px; font-weight: 500; letter-spacing: 0.26em; text-transform: uppercase; color: var(--text-secondary); margin: -6px 0 0; user-select: none; }
|
.auth-title { font-family: var(--font-ui); font-size: 11px; font-weight: 500; letter-spacing: 0.26em; text-transform: uppercase; color: var(--text-secondary); margin: -6px 0 0; user-select: none; }
|
||||||
|
|||||||
@@ -42,8 +42,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:global(*, *::before, *::after) { scrollbar-width: none; }
|
|
||||||
:global(*::-webkit-scrollbar) { display: none; }
|
|
||||||
.frame { display: flex; padding: 6px 15px 15px; width: 100%; height: 100%; box-sizing: border-box; overflow: hidden; }
|
.frame { display: flex; padding: 6px 15px 15px; width: 100%; height: 100%; box-sizing: border-box; overflow: hidden; }
|
||||||
.shell { display: flex; flex: 1; border-radius: 14px; overflow: hidden; border: 1px solid var(--border-dim); background: var(--bg-base); min-height: 0; min-width: 0; }
|
.shell { display: flex; flex: 1; border-radius: 14px; overflow: hidden; border: 1px solid var(--border-dim); background: var(--bg-base); min-height: 0; min-width: 0; }
|
||||||
.main { flex: 1; overflow: hidden; background: var(--bg-surface); transform: translateZ(0); contain: layout style; min-width: 0; }
|
.main { flex: 1; overflow: hidden; background: var(--bg-surface); transform: translateZ(0); contain: layout style; min-width: 0; }
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root anim-fade-in">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="heading">History</span>
|
<span class="heading">History</span>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
@@ -197,7 +197,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; animation: fadeIn 0.14s ease both; }
|
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
||||||
|
|
||||||
.header { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-4) var(--sp-6); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
|
.header { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-4) var(--sp-6); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
|
||||||
.heading { font-family: var(--font-ui); font-size: var(--text-xs); font-weight: var(--weight-normal); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; }
|
.heading { font-family: var(--font-ui); font-size: var(--text-xs); font-weight: var(--weight-normal); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; }
|
||||||
@@ -256,5 +256,4 @@
|
|||||||
.empty-text { font-size: var(--text-base); color: var(--text-muted); }
|
.empty-text { font-size: var(--text-base); color: var(--text-muted); }
|
||||||
.empty-hint { font-size: var(--text-sm); color: var(--text-faint); }
|
.empty-hint { font-size: var(--text-sm); color: var(--text-faint); }
|
||||||
|
|
||||||
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -416,7 +416,7 @@
|
|||||||
<div class="bottom-area" style="z-index:1">
|
<div class="bottom-area" style="z-index:1">
|
||||||
<div class="status-slot" class:status-slot-hide={lockEnabled && pinVisible}>
|
<div class="status-slot" class:status-slot-hide={lockEnabled && pinVisible}>
|
||||||
{#if failed || notConfigured}
|
{#if failed || notConfigured}
|
||||||
<div class="error-box">
|
<div class="error-box anim-fade-up">
|
||||||
<p class="error-label">{failed ? "Could not reach server" : "Server not configured"}</p>
|
<p class="error-label">{failed ? "Could not reach server" : "Server not configured"}</p>
|
||||||
<div class="error-actions">
|
<div class="error-actions">
|
||||||
<button class="err-btn" onclick={() => onRetry?.()}>Retry</button>
|
<button class="err-btn" onclick={() => onRetry?.()}>Retry</button>
|
||||||
@@ -455,14 +455,13 @@
|
|||||||
@keyframes spOut { from { opacity:1; transform:scale(1) } to { opacity:0; transform:scale(0.96) } }
|
@keyframes spOut { from { opacity:1; transform:scale(1) } to { opacity:0; transform:scale(0.96) } }
|
||||||
@keyframes logoBreathe { 0%,100% { transform:scale(1); filter:drop-shadow(0 0 0px rgba(255,255,255,0)) } 50% { transform:scale(1.04); filter:drop-shadow(0 0 18px rgba(255,255,255,0.12)) } }
|
@keyframes logoBreathe { 0%,100% { transform:scale(1); filter:drop-shadow(0 0 0px rgba(255,255,255,0)) } 50% { transform:scale(1.04); filter:drop-shadow(0 0 18px rgba(255,255,255,0.12)) } }
|
||||||
@keyframes hintFade { 0%,100% { opacity:0.35 } 50% { opacity:0.7 } }
|
@keyframes hintFade { 0%,100% { opacity:0.35 } 50% { opacity:0.7 } }
|
||||||
@keyframes errIn { from { opacity:0; transform:translateY(4px) } to { opacity:1; transform:translateY(0) } }
|
|
||||||
@keyframes pinShake { 0%,100% { transform:translateX(0) } 20%,60% { transform:translateX(-6px) } 40%,80% { transform:translateX(6px) } }
|
@keyframes pinShake { 0%,100% { transform:translateX(0) } 20%,60% { transform:translateX(-6px) } 40%,80% { transform:translateX(6px) } }
|
||||||
|
|
||||||
.logo-glow { position: absolute; inset: -20px; border-radius: 50%; background: radial-gradient(circle, rgba(255,255,255,0.06) 0%, transparent 70%); animation: logoBreathe 4s ease-in-out infinite; }
|
.logo-glow { position: absolute; inset: -20px; border-radius: 50%; background: radial-gradient(circle, rgba(255,255,255,0.06) 0%, transparent 70%); animation: logoBreathe 4s ease-in-out infinite; }
|
||||||
.logo-breathe { animation: logoBreathe 4s ease-in-out infinite; }
|
.logo-breathe { animation: logoBreathe 4s ease-in-out infinite; }
|
||||||
.hint { font-family: var(--font-ui); font-size: 10px; color: var(--text-faint); letter-spacing: 0.22em; text-transform: uppercase; margin: 0; user-select: none; animation: hintFade 3.5s ease-in-out infinite; }
|
.hint { font-family: var(--font-ui); font-size: 10px; color: var(--text-faint); letter-spacing: 0.22em; text-transform: uppercase; margin: 0; user-select: none; animation: hintFade 3.5s ease-in-out infinite; }
|
||||||
|
|
||||||
.error-box { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 16px 20px; border-radius: var(--radius-lg); background: var(--bg-surface); border: 1px solid var(--border-base); min-width: 200px; text-align: center; animation: errIn 0.25s cubic-bezier(0,0,0.2,1) both; }
|
.error-box { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 16px 20px; border-radius: var(--radius-lg); background: var(--bg-surface); border: 1px solid var(--border-base); min-width: 200px; text-align: center; }
|
||||||
.error-label { font-family: var(--font-ui); font-size: 11px; font-weight: 500; color: var(--text-muted); letter-spacing: 0.06em; margin: 0; }
|
.error-label { font-family: var(--font-ui); font-size: 11px; font-weight: 500; color: var(--text-muted); letter-spacing: 0.06em; margin: 0; }
|
||||||
.error-actions { display: flex; gap: 6px; }
|
.error-actions { display: flex; gap: 6px; }
|
||||||
.err-btn { padding: 5px 14px; border-radius: var(--radius-md); border: 1px solid var(--border-base); background: transparent; color: var(--text-muted); cursor: pointer; font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.04em; transition: border-color 0.15s, color 0.15s; }
|
.err-btn { padding: 5px 14px; border-radius: var(--radius-md); border: 1px solid var(--border-base); background: transparent; color: var(--text-muted); cursor: pointer; font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.04em; transition: border-color 0.15s, color 0.15s; }
|
||||||
|
|||||||
Reference in New Issue
Block a user