fix: linter issues
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Sheet,
|
||||
@@ -158,8 +158,6 @@ export function FiltersSheet({
|
||||
filters,
|
||||
refetch,
|
||||
}: FiltersSheetProps) {
|
||||
const [filterElements, setFilterElements] = useState<ReactNode>(null);
|
||||
|
||||
const setFilterWithKey = (key: string, newValue: AnyFilterValue) =>
|
||||
setValues(fValues =>
|
||||
!fValues
|
||||
@@ -186,15 +184,13 @@ export function FiltersSheet({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFilterElements(renderFilters(filters, values, setFilterWithKey));
|
||||
}, [values, filters]);
|
||||
|
||||
const handleApply = () => {
|
||||
refetch();
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const filterElements = renderFilters(filters, values, setFilterWithKey);
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent side="right" className="w-full sm:max-w-md overflow-y-auto">
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { useAppStore } from '@/store';
|
||||
import { usePluginCustomAssets } from '@/hooks/usePluginCustomAssets';
|
||||
|
||||
export default function ParseChapterSection() {
|
||||
const ParseChapterSection = React.memo(function ParseChapterSection() {
|
||||
const plugin = useAppStore(state => state.plugin);
|
||||
const parseChapterPath = useAppStore(state => state.parseChapterPath);
|
||||
const shouldAutoSubmitChapter = useAppStore(
|
||||
@@ -30,6 +30,15 @@ export default function ParseChapterSection() {
|
||||
const [fetchError, setFetchError] = useState('');
|
||||
const [showRawHtml, setShowRawHtml] = useState(false);
|
||||
const lastProcessedPath = useRef<string>();
|
||||
const [prevPluginId, setPrevPluginId] = useState<string | undefined>();
|
||||
|
||||
if (plugin?.id !== prevPluginId) {
|
||||
setPrevPluginId(plugin?.id);
|
||||
setChapterPath('');
|
||||
setChapterText('');
|
||||
setFetchError('');
|
||||
setShowRawHtml(false);
|
||||
}
|
||||
|
||||
const { customCSSLoaded, customJSLoaded, customCSSError, customJSError } =
|
||||
usePluginCustomAssets(plugin, chapterText);
|
||||
@@ -56,7 +65,7 @@ export default function ParseChapterSection() {
|
||||
await fetchChapterByPath(chapterPath);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && chapterPath.trim()) {
|
||||
fetchChapter();
|
||||
}
|
||||
@@ -128,7 +137,7 @@ export default function ParseChapterSection() {
|
||||
placeholder="Enter chapter path..."
|
||||
value={chapterPath}
|
||||
onChange={e => setChapterPath(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="flex-1"
|
||||
disabled={!plugin}
|
||||
/>
|
||||
@@ -327,4 +336,6 @@ export default function ParseChapterSection() {
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ParseChapterSection;
|
||||
|
||||
@@ -27,7 +27,7 @@ type ParseNovelSectionProps = {
|
||||
onNavigateToParseChapter?: () => void;
|
||||
};
|
||||
|
||||
export default function ParseNovelSection({
|
||||
const ParseNovelSection = React.memo(function ParseNovelSection({
|
||||
onNavigateToParseChapter,
|
||||
}: ParseNovelSectionProps) {
|
||||
const plugin = useAppStore(state => state.plugin);
|
||||
@@ -46,6 +46,16 @@ export default function ParseNovelSection({
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [fetchError, setFetchError] = useState('');
|
||||
const lastProcessedPath = useRef<string>();
|
||||
const [prevPluginId, setPrevPluginId] = useState<string | undefined>();
|
||||
|
||||
if (plugin?.id !== prevPluginId) {
|
||||
setPrevPluginId(plugin?.id);
|
||||
setNovelPath('');
|
||||
setSourceNovel(undefined);
|
||||
setChapters([]);
|
||||
setCurrentPage(1);
|
||||
setFetchError('');
|
||||
}
|
||||
|
||||
const { exportEpub, isExporting } = useEpubExport({
|
||||
plugin: plugin || null,
|
||||
@@ -100,7 +110,7 @@ export default function ParseNovelSection({
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && novelPath.trim()) {
|
||||
fetchNovel();
|
||||
}
|
||||
@@ -168,7 +178,7 @@ export default function ParseNovelSection({
|
||||
placeholder="Enter novel path..."
|
||||
value={novelPath}
|
||||
onChange={e => setNovelPath(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="flex-1"
|
||||
disabled={!plugin}
|
||||
/>
|
||||
@@ -551,4 +561,6 @@ export default function ParseNovelSection({
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ParseNovelSection;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Filter, BookOpen } from 'lucide-react';
|
||||
|
||||
import { FiltersSheet } from '@/components/filters/filters-sheet';
|
||||
@@ -16,7 +16,7 @@ type PopularNovelsSectionProps = {
|
||||
onNavigateToParseNovel?: () => void;
|
||||
};
|
||||
|
||||
export default function PopularNovelsSection({
|
||||
const PopularNovelsSection = React.memo(function PopularNovelsSection({
|
||||
onNavigateToParseNovel,
|
||||
}: PopularNovelsSectionProps) {
|
||||
const plugin = useAppStore(state => state.plugin);
|
||||
@@ -30,14 +30,38 @@ export default function PopularNovelsSection({
|
||||
const [filterValues, setFilterValues] = useState<
|
||||
FilterToValues<Filters> | undefined
|
||||
>();
|
||||
const [prevPluginId, setPrevPluginId] = useState<string | undefined>();
|
||||
|
||||
const fetchNovelsByIndex = async (index: number) => {
|
||||
if (plugin?.id !== prevPluginId) {
|
||||
setPrevPluginId(plugin?.id);
|
||||
setCurrentIndex(0);
|
||||
setMaxIndex(0);
|
||||
setNovels([]);
|
||||
|
||||
if (plugin?.filters) {
|
||||
const filters: FilterToValues<typeof plugin.filters> = {};
|
||||
for (const fKey in plugin.filters) {
|
||||
filters[fKey as keyof typeof filters] = {
|
||||
type: plugin.filters[fKey].type,
|
||||
value: plugin.filters[fKey].value,
|
||||
};
|
||||
}
|
||||
setFilterValues(filters);
|
||||
} else {
|
||||
setFilterValues(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchNovelsByIndex = async (
|
||||
index: number,
|
||||
latestOverride?: boolean,
|
||||
) => {
|
||||
if (plugin && index) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const fetchedNovels = await plugin.popularNovels(index, {
|
||||
filters: filterValues || {},
|
||||
showLatestNovels: isLatest,
|
||||
showLatestNovels: latestOverride ?? isLatest,
|
||||
});
|
||||
if (fetchedNovels.length !== 0) {
|
||||
setCurrentIndex(index);
|
||||
@@ -54,31 +78,13 @@ export default function PopularNovelsSection({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (plugin) {
|
||||
setCurrentIndex(1);
|
||||
setMaxIndex(1);
|
||||
fetchNovelsByIndex(1);
|
||||
}
|
||||
}, [isLatest]);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset when changing plugins
|
||||
const handleIsLatestChange = (latest: boolean) => {
|
||||
if (isLatest === latest) return;
|
||||
setIsLatest(latest);
|
||||
setCurrentIndex(0);
|
||||
setMaxIndex(0);
|
||||
setNovels([]);
|
||||
|
||||
if (plugin?.filters) {
|
||||
const filters: FilterToValues<typeof plugin.filters> = {};
|
||||
for (const fKey in plugin.filters) {
|
||||
filters[fKey as keyof typeof filters] = {
|
||||
type: plugin.filters[fKey].type,
|
||||
value: plugin.filters[fKey].value,
|
||||
};
|
||||
}
|
||||
setFilterValues(filters);
|
||||
}
|
||||
}, [plugin]);
|
||||
};
|
||||
|
||||
const handleParseNovel = (path: string) => {
|
||||
setParseNovelPath(path, true);
|
||||
@@ -136,7 +142,7 @@ export default function PopularNovelsSection({
|
||||
isLatest === (option === 'Latest') ? 'default' : 'outline'
|
||||
}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setIsLatest(option === 'Latest')}
|
||||
onClick={() => handleIsLatestChange(option === 'Latest')}
|
||||
>
|
||||
{option}
|
||||
</Badge>
|
||||
@@ -149,7 +155,7 @@ export default function PopularNovelsSection({
|
||||
min="1"
|
||||
max={maxIndex}
|
||||
value={currentIndex}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange={e => {
|
||||
const page = parseInt(e.target.value);
|
||||
if (page > 0 && page <= maxIndex) {
|
||||
fetchNovelsByIndex(page);
|
||||
@@ -216,4 +222,6 @@ export default function PopularNovelsSection({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default PopularNovelsSection;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Search as SearchIcon } from 'lucide-react';
|
||||
|
||||
import { NovelCard } from '@/components/novel-card';
|
||||
@@ -14,7 +14,7 @@ type SearchNovelsSectionProps = {
|
||||
onNavigateToParseNovel?: () => void;
|
||||
};
|
||||
|
||||
export default function SearchNovelsSection({
|
||||
const SearchNovelsSection = React.memo(function SearchNovelsSection({
|
||||
onNavigateToParseNovel,
|
||||
}: SearchNovelsSectionProps) {
|
||||
const plugin = useAppStore(state => state.plugin);
|
||||
@@ -24,6 +24,15 @@ export default function SearchNovelsSection({
|
||||
const [novels, setNovels] = useState<Plugin.NovelItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fetchError, setFetchError] = useState('');
|
||||
const [prevPluginId, setPrevPluginId] = useState<string | undefined>();
|
||||
|
||||
if (plugin?.id !== prevPluginId) {
|
||||
setPrevPluginId(plugin?.id);
|
||||
setCurrentPage(1);
|
||||
setNovels([]);
|
||||
setSearchTerm('');
|
||||
setFetchError('');
|
||||
}
|
||||
|
||||
const fetchNovels = async (page: number) => {
|
||||
if (plugin && searchTerm.trim()) {
|
||||
@@ -44,14 +53,7 @@ export default function SearchNovelsSection({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
setNovels([]);
|
||||
setSearchTerm('');
|
||||
setFetchError('');
|
||||
}, [plugin]);
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && searchTerm.trim()) {
|
||||
fetchNovels(1);
|
||||
}
|
||||
@@ -86,7 +88,7 @@ export default function SearchNovelsSection({
|
||||
placeholder="Enter search term..."
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="flex-1"
|
||||
disabled={!plugin}
|
||||
/>
|
||||
@@ -159,4 +161,6 @@ export default function SearchNovelsSection({
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default SearchNovelsSection;
|
||||
|
||||
+138
-124
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { CheckedState } from '@radix-ui/react-checkbox';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
@@ -16,59 +16,75 @@ import {
|
||||
import useDebounce from '@/hooks/useDebounce';
|
||||
import { FetchMode } from '@/types/types';
|
||||
|
||||
export default function SettingsSection() {
|
||||
const [cookies, setCookies] = useState('');
|
||||
const debouncedCookies = useDebounce(cookies, 500);
|
||||
const [fetchMode, setFetchMode] = useState<FetchMode>(FetchMode.PROXY);
|
||||
const [useUserAgent, setUseUserAgent] = useState<CheckedState>(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [alertVisible, setAlertVisible] = useState(false);
|
||||
const FETCH_MODES = {
|
||||
[FetchMode.PROXY]: 'Proxy',
|
||||
[FetchMode.NODE_FETCH]: 'Node Fetch',
|
||||
[FetchMode.CURL]: 'Curl',
|
||||
};
|
||||
|
||||
const SettingsSection = React.memo(function SettingsSection() {
|
||||
const [settings, setSettings] = useState({
|
||||
cookies: '',
|
||||
fetchMode: FetchMode.PROXY,
|
||||
useUserAgent: true as CheckedState,
|
||||
});
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'saved'>('idle');
|
||||
const init = useRef(false);
|
||||
const lastSaved = useRef<typeof settings | null>(null);
|
||||
const debouncedCookies = useDebounce(settings.cookies, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (alertVisible) {
|
||||
const id = setTimeout(() => setAlertVisible(false), 2000);
|
||||
return () => clearTimeout(id);
|
||||
}
|
||||
}, [alertVisible]);
|
||||
fetch('settings')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const loaded = {
|
||||
cookies: data.cookies || '',
|
||||
fetchMode: data.fetchMode ?? FetchMode.PROXY,
|
||||
useUserAgent: data.useUserAgent ?? true,
|
||||
};
|
||||
setSettings(loaded);
|
||||
lastSaved.current = loaded;
|
||||
init.current = true;
|
||||
})
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
if (!init.current || debouncedCookies !== settings.cookies) return;
|
||||
const current = { ...settings, cookies: debouncedCookies };
|
||||
|
||||
if (JSON.stringify(lastSaved.current) === JSON.stringify(current)) return;
|
||||
|
||||
setStatus('loading');
|
||||
fetch('settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
cookies: debouncedCookies,
|
||||
fetchMode: fetchMode,
|
||||
useUserAgent: useUserAgent === true,
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(current),
|
||||
})
|
||||
.then(() => setAlertVisible(true))
|
||||
.catch(error => console.error('Failed to save settings:', error))
|
||||
.finally(() => setLoading(false));
|
||||
}, [debouncedCookies, fetchMode, useUserAgent]);
|
||||
.then(() => {
|
||||
lastSaved.current = current;
|
||||
setStatus('saved');
|
||||
setTimeout(() => setStatus('idle'), 2000);
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [
|
||||
debouncedCookies,
|
||||
settings.fetchMode,
|
||||
settings.useUserAgent,
|
||||
settings.cookies,
|
||||
]);
|
||||
|
||||
const getFetchModeLabel = (mode: FetchMode) => {
|
||||
switch (mode) {
|
||||
case FetchMode.PROXY:
|
||||
return 'Proxy';
|
||||
case FetchMode.NODE_FETCH:
|
||||
return 'Node Fetch';
|
||||
case FetchMode.CURL:
|
||||
return 'Curl';
|
||||
default:
|
||||
return 'Proxy';
|
||||
}
|
||||
};
|
||||
const update = <K extends keyof typeof settings>(
|
||||
k: K,
|
||||
v: (typeof settings)[K],
|
||||
) => setSettings(settings => ({ ...settings, [k]: v }));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6 relative">
|
||||
{alertVisible && (
|
||||
{status === 'saved' && (
|
||||
<div className="absolute top-4 right-4 z-10 bg-green-500/90 text-white px-4 py-2 rounded-md flex items-center gap-2 shadow-lg animate-in fade-in slide-in-from-top-2">
|
||||
<Check className="w-4 h-4" />
|
||||
Settings updated
|
||||
<Check className="w-4 h-4" /> Settings updated
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -79,86 +95,61 @@ export default function SettingsSection() {
|
||||
Settings are automatically saved
|
||||
</p>
|
||||
</div>
|
||||
{loading && (
|
||||
{status === 'loading' && (
|
||||
<div className="text-sm text-muted-foreground">Saving...</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Request Configuration Section */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="h-px flex-1 bg-border"></div>
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
Request Configuration
|
||||
</h3>
|
||||
<div className="h-px flex-1 bg-border"></div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* User Agent */}
|
||||
<div className="space-y-2">
|
||||
<Label className="font-semibold text-foreground">
|
||||
Browser User Agent
|
||||
</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
value={navigator.userAgent}
|
||||
disabled
|
||||
className="font-mono text-xs flex-1 opacity-60"
|
||||
title={navigator.userAgent}
|
||||
/>
|
||||
<div className="flex items-center gap-2 whitespace-nowrap">
|
||||
<Checkbox
|
||||
id="use-ua"
|
||||
checked={useUserAgent}
|
||||
onCheckedChange={setUseUserAgent}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="use-ua"
|
||||
className="text-sm text-foreground cursor-pointer"
|
||||
>
|
||||
Use
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enable to send your browser's user agent with requests
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Cookies */}
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="cookies"
|
||||
className="font-semibold text-foreground"
|
||||
>
|
||||
Cookies
|
||||
</Label>
|
||||
<Section title="Request Configuration">
|
||||
<div className="space-y-2">
|
||||
<Label className="font-semibold text-foreground">
|
||||
Browser User Agent
|
||||
</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
id="cookies"
|
||||
value={cookies}
|
||||
onChange={e => setCookies(e.target.value.trim())}
|
||||
placeholder="Enter cookies (optional)..."
|
||||
className="font-mono text-xs"
|
||||
value={navigator.userAgent}
|
||||
disabled
|
||||
className="font-mono text-xs flex-1 opacity-60"
|
||||
title={navigator.userAgent}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Additional cookies to send with requests (optional)
|
||||
</p>
|
||||
<div className="flex items-center gap-2 whitespace-nowrap">
|
||||
<Checkbox
|
||||
id="use-ua"
|
||||
checked={settings.useUserAgent}
|
||||
onCheckedChange={v => update('useUserAgent', v)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="use-ua"
|
||||
className="text-sm text-foreground cursor-pointer"
|
||||
>
|
||||
Use
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fetch Settings Section */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="h-px flex-1 bg-border"></div>
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
Fetch Settings
|
||||
</h3>
|
||||
<div className="h-px flex-1 bg-border"></div>
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="cookies"
|
||||
className="font-semibold text-foreground"
|
||||
>
|
||||
Cookies
|
||||
</Label>
|
||||
<Input
|
||||
id="cookies"
|
||||
value={settings.cookies}
|
||||
onChange={e => update('cookies', e.target.value.trim())}
|
||||
placeholder="Enter cookies (optional)..."
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Additional cookies to send with requests (optional)
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Fetch Settings">
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="fetch-mode"
|
||||
@@ -167,33 +158,56 @@ export default function SettingsSection() {
|
||||
Fetch Mode
|
||||
</Label>
|
||||
<Select
|
||||
value={fetchMode.toString()}
|
||||
onValueChange={value =>
|
||||
setFetchMode(parseInt(value) as FetchMode)
|
||||
}
|
||||
value={settings.fetchMode.toString()}
|
||||
onValueChange={v => update('fetchMode', parseInt(v))}
|
||||
>
|
||||
<SelectTrigger id="fetch-mode">
|
||||
<SelectValue>{getFetchModeLabel(fetchMode)}</SelectValue>
|
||||
<SelectValue>
|
||||
{
|
||||
FETCH_MODES[
|
||||
settings.fetchMode as keyof typeof FETCH_MODES
|
||||
]
|
||||
}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={FetchMode.PROXY.toString()}>
|
||||
Proxy
|
||||
</SelectItem>
|
||||
<SelectItem value={FetchMode.NODE_FETCH.toString()}>
|
||||
Node Fetch
|
||||
</SelectItem>
|
||||
<SelectItem value={FetchMode.CURL.toString()}>
|
||||
Curl
|
||||
</SelectItem>
|
||||
{Object.entries(FETCH_MODES).map(([v, l]) => (
|
||||
<SelectItem key={v} value={v}>
|
||||
{l}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Select the method used to fetch data from sources
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function Section({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
<div className="space-y-6">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsSection;
|
||||
|
||||
+19
-16
@@ -25,22 +25,25 @@ const badgeVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'span'> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : 'span';
|
||||
type BadgeProps = React.ComponentPropsWithoutRef<'span'> &
|
||||
VariantProps<typeof badgeVariants> & {
|
||||
asChild?: boolean;
|
||||
};
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
||||
({ className, variant, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'span';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Badge.displayName = 'Badge';
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
@@ -36,25 +36,25 @@ const buttonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> &
|
||||
type ButtonProps = React.ComponentPropsWithoutRef<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
};
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
||||
|
||||
+78
-59
@@ -2,9 +2,10 @@ import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
const Card = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
@@ -12,74 +13,92 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
Card.displayName = 'Card';
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
const CardTitle = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="card-title"
|
||||
className={cn('leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="card-description"
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const CardAction = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardAction.displayName = 'CardAction';
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn('px-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="card-content"
|
||||
className={cn('px-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<'div'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="card-footer"
|
||||
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export {
|
||||
Card,
|
||||
|
||||
@@ -6,27 +6,27 @@ import { CheckIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
'peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
'peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none"
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
|
||||
+19
-15
@@ -2,20 +2,24 @@ import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
|
||||
+15
-15
@@ -3,20 +3,20 @@ import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
|
||||
+119
-131
@@ -4,60 +4,47 @@ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
}
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||
}
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||
}
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = 'default',
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: 'sm' | 'default';
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
||||
size?: 'sm' | 'default';
|
||||
}
|
||||
>(({ className, size = 'default', children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = 'popper',
|
||||
align = 'center',
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(
|
||||
(
|
||||
{ className, children, position = 'popper', align = 'center', ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||
@@ -82,94 +69,95 @@ function SelectContent({
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
data-slot="select-label"
|
||||
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
}
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
data-slot="select-separator"
|
||||
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
);
|
||||
}
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
);
|
||||
}
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
const Skeleton = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('animate-pulse rounded-md bg-muted/50', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
Skeleton.displayName = 'Skeleton';
|
||||
|
||||
export { Skeleton };
|
||||
|
||||
+58
-54
@@ -3,62 +3,66 @@ import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn('flex flex-col gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const Tabs = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="tabs"
|
||||
className={cn('flex flex-col gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Tabs.displayName = TabsPrimitive.Root.displayName;
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow,border-color] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm data-[state=active]:border-b-2 data-[state=active]:border-accent [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow,border-color] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm data-[state=active]:border-b-2 data-[state=active]:border-accent [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn('flex-1 outline-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="tabs-content"
|
||||
forceMount
|
||||
className={cn(
|
||||
'flex-1 outline-none data-[state=inactive]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
|
||||
@@ -5,35 +5,26 @@ import { cn } from '@/lib/utils';
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return <TooltipPrimitive.Root {...props} />;
|
||||
}
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger {...props} />;
|
||||
}
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
@@ -3,12 +3,12 @@ import { toast } from 'sonner';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { createEpub, downloadBlob } from '@/lib/epub';
|
||||
|
||||
interface UseEpubExportOptions {
|
||||
type UseEpubExportOptions = {
|
||||
plugin: Plugin.PluginBase | null;
|
||||
sourceNovel: (Plugin.SourceNovel & { totalPages?: number }) | undefined;
|
||||
chapters: Plugin.ChapterItem[];
|
||||
novelPath: string;
|
||||
}
|
||||
};
|
||||
|
||||
export function useEpubExport({
|
||||
plugin,
|
||||
@@ -69,11 +69,12 @@ export function useEpubExport({
|
||||
description: `0/${allChapters.length} chapters processed`,
|
||||
});
|
||||
|
||||
const chapterContents: Array<{
|
||||
type Chapter = {
|
||||
title: string;
|
||||
content: string;
|
||||
path: string;
|
||||
}> = [];
|
||||
};
|
||||
const chapterContents: Chapter[] = [];
|
||||
|
||||
for (let i = 0; i < allChapters.length; i++) {
|
||||
const chapter = allChapters[i];
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
|
||||
interface UsePluginCustomAssetsReturn {
|
||||
type UsePluginCustomAssetsReturn = {
|
||||
customCSSLoaded: boolean;
|
||||
customJSLoaded: boolean;
|
||||
customCSSError: boolean;
|
||||
customJSError: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to load and manage plugin custom CSS and JS assets
|
||||
|
||||
+5
-5
@@ -1,19 +1,19 @@
|
||||
import JSZip from 'jszip';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
// import { Plugin } from '@/types/plugin'; // apparently unused
|
||||
|
||||
export interface EpubOptions {
|
||||
export type EpubOptions = {
|
||||
title: string;
|
||||
author?: string;
|
||||
description?: string;
|
||||
cover?: string;
|
||||
language?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ChapterData {
|
||||
export type ChapterData = {
|
||||
title: string;
|
||||
content: string;
|
||||
path: string;
|
||||
}
|
||||
};
|
||||
|
||||
function sanitizeHtml(html: string): string {
|
||||
// Remove script tags and their content
|
||||
|
||||
+6
-4
@@ -2,7 +2,7 @@
|
||||
|
||||
import { parse as parseProto } from 'protobufjs';
|
||||
|
||||
type FetchInit = {
|
||||
export type FetchInit = {
|
||||
headers?: Record<string, string | undefined> | Headers;
|
||||
method?: string;
|
||||
body?: FormData | string;
|
||||
@@ -104,7 +104,7 @@ type ProtoRequestInit = {
|
||||
// merged .proto file
|
||||
proto: string;
|
||||
requestType: string;
|
||||
requestData?: any;
|
||||
requestData?: Record<string, unknown>;
|
||||
responseType: string;
|
||||
};
|
||||
|
||||
@@ -117,11 +117,13 @@ export const fetchProto = async function <ReturnType>(
|
||||
) {
|
||||
const protoRoot = parseProto(protoInit.proto).root;
|
||||
const RequestMessge = protoRoot.lookupType(protoInit.requestType);
|
||||
if (RequestMessge.verify(protoInit.requestData)) {
|
||||
if (RequestMessge.verify(protoInit.requestData || {})) {
|
||||
throw new Error('Invalid Proto');
|
||||
}
|
||||
// encode request data
|
||||
const encodedrequest = RequestMessge.encode(protoInit.requestData).finish();
|
||||
const encodedrequest = RequestMessge.encode(
|
||||
protoInit.requestData || {},
|
||||
).finish();
|
||||
const requestLength = BigInt(encodedrequest.length);
|
||||
const headers = new Uint8Array(
|
||||
Array(5)
|
||||
|
||||
+16
-8
@@ -1,5 +1,11 @@
|
||||
export type StorageItem<T = unknown> = {
|
||||
created: Date;
|
||||
value: T;
|
||||
expires?: number;
|
||||
};
|
||||
|
||||
class Storage {
|
||||
private db: Record<string, { created: Date; value: any; expires?: number }>;
|
||||
private db: Record<string, StorageItem>;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the Storage class.
|
||||
@@ -12,10 +18,10 @@ class Storage {
|
||||
* Sets a key-value pair in storage.
|
||||
*
|
||||
* @param {string} key - The key to set.
|
||||
* @param {any} value - The value to set.
|
||||
* @param {T} value - The value to set.
|
||||
* @param {Date | number} [expires] - Optional expiry date or time in milliseconds.
|
||||
*/
|
||||
set(key: string, value: any, expires?: Date | number): void {
|
||||
set<T>(key: string, value: T, expires?: Date | number): void {
|
||||
this.db[key] = {
|
||||
created: new Date(),
|
||||
value,
|
||||
@@ -28,10 +34,12 @@ class Storage {
|
||||
*
|
||||
* @param {string} key - The key to retrieve the value for.
|
||||
* @param {boolean} [raw] - Optional flag to return the raw stored item.
|
||||
* @returns {any} The stored value or undefined if key is not found.
|
||||
* @returns The stored value or undefined if key is not found.
|
||||
*/
|
||||
get(key: string, raw?: boolean): any {
|
||||
const item = this.db[key];
|
||||
get<T = unknown>(key: string, raw: true): StorageItem<T> | undefined;
|
||||
get<T = unknown>(key: string, raw?: false): T | undefined;
|
||||
get<T = unknown>(key: string, raw?: boolean): T | StorageItem<T> | undefined {
|
||||
const item = this.db[key] as StorageItem<T> | undefined;
|
||||
if (item?.expires && Date.now() > item.expires) {
|
||||
this.delete(key);
|
||||
return undefined;
|
||||
@@ -69,7 +77,7 @@ class Storage {
|
||||
export const storage = new Storage();
|
||||
|
||||
/*
|
||||
These parameters cannot be implemented in `test-web`.
|
||||
These parameters cannot be implemented in `test-web`.
|
||||
They are generated in the browser when js-scripts are executed
|
||||
Read more
|
||||
|
||||
@@ -80,7 +88,7 @@ https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
|
||||
/**
|
||||
* Represents the structure of a storage object with string keys and values.
|
||||
*/
|
||||
type StorageObject = Record<string, any>;
|
||||
type StorageObject = Record<string, string>;
|
||||
|
||||
/**
|
||||
* Represents a simplified version of the browser's localStorage.
|
||||
|
||||
+7
-1
@@ -2,4 +2,10 @@
|
||||
* Backward compatibility for 3.0.0 - Re-exports from new location
|
||||
* TODO: Remove in 4.0.0
|
||||
*/
|
||||
export { fetchApi, fetchText, fetchProto, fetchFile } from '../lib/fetch';
|
||||
export {
|
||||
fetchApi,
|
||||
fetchText,
|
||||
fetchProto,
|
||||
fetchFile,
|
||||
type FetchInit,
|
||||
} from '../lib/fetch';
|
||||
|
||||
@@ -8,5 +8,6 @@ export type {
|
||||
Filter,
|
||||
FilterOption,
|
||||
FilterToValues,
|
||||
FilterValueWithType,
|
||||
AnyFilterValue,
|
||||
} from '../types/filters';
|
||||
|
||||
+81
-47
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
|
||||
import { BookOpen, Search, Settings, Zap } from 'lucide-react';
|
||||
import PluginHeader from '../components/plugin-header';
|
||||
@@ -14,62 +14,96 @@ import ParseNovelSection from '@/components/parse-novel';
|
||||
import SettingsSection from '@/components/settings';
|
||||
import ParseChapterSection from '@/components/parse-chapter';
|
||||
|
||||
function Home() {
|
||||
function PluginSidebar() {
|
||||
const { plugin, selectPlugin } = useAppStore(state => state);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const [activeTab, setActiveTab] = useState('popular');
|
||||
|
||||
const filteredPlugins = useMemo(
|
||||
() =>
|
||||
plugins.filter(plugin =>
|
||||
plugin.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
plugins.filter(p =>
|
||||
p.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
),
|
||||
[searchQuery],
|
||||
);
|
||||
|
||||
return (
|
||||
<aside className="w-64 border-r border-border bg-background flex flex-col">
|
||||
<div className="p-6 flex-shrink-0 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Plugins
|
||||
</h2>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{filteredPlugins.length} / {plugins.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search plugin..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
className="pl-10 h-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-6 pb-6">
|
||||
<div className="space-y-2">
|
||||
{plugins.map(p => {
|
||||
if (!p.icon) {
|
||||
throw new Error(`Plugin ${p.name} is missing icon path`);
|
||||
}
|
||||
const isVisible = p.name
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase());
|
||||
return (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => selectPlugin(p)}
|
||||
className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors flex items-center gap-3 ${
|
||||
p.id === plugin?.id
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-foreground hover:bg-muted'
|
||||
} ${isVisible ? '' : 'hidden'}`}
|
||||
>
|
||||
<img
|
||||
src={`/static/${p.icon}`}
|
||||
alt={p.name}
|
||||
className="w-6 h-6 rounded-sm shrink-0 object-contain"
|
||||
onError={() => {
|
||||
throw new Error(
|
||||
`Icon not found for plugin: ${p.name} at /static/${p.icon}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span className="truncate">{p.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function Home() {
|
||||
const { plugin } = useAppStore(state => state);
|
||||
|
||||
const [activeTab, setActiveTab] = useState('popular');
|
||||
|
||||
const handleNavigateToParseNovel = useCallback(() => {
|
||||
setActiveTab('parse-novel');
|
||||
}, []);
|
||||
|
||||
const handleNavigateToParseChapter = useCallback(() => {
|
||||
setActiveTab('parse-chapter');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<PluginHeader selectedPlugin={plugin} />
|
||||
<div className="flex h-[calc(100vh-64px)]">
|
||||
<aside className="w-64 border-r border-border bg-background flex flex-col">
|
||||
<div className="p-6 flex-shrink-0 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Plugins
|
||||
</h2>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{filteredPlugins.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search plugin..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
className="pl-10 h-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-6 pb-6">
|
||||
<div className="space-y-2">
|
||||
{filteredPlugins.map(filteredPlugin => (
|
||||
<button
|
||||
key={filteredPlugin.id}
|
||||
onClick={() => selectPlugin(filteredPlugin)}
|
||||
className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors ${
|
||||
filteredPlugin.id === plugin?.id
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-foreground hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
{filteredPlugin.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<PluginSidebar />
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
@@ -126,19 +160,19 @@ function Home() {
|
||||
|
||||
<TabsContent value="popular" className="space-y-6">
|
||||
<PopularNovelsSection
|
||||
onNavigateToParseNovel={() => setActiveTab('parse-novel')}
|
||||
onNavigateToParseNovel={handleNavigateToParseNovel}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="search" className="space-y-6">
|
||||
<SearchNovelsSection
|
||||
onNavigateToParseNovel={() => setActiveTab('parse-novel')}
|
||||
onNavigateToParseNovel={handleNavigateToParseNovel}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="parse-novel" className="space-y-6">
|
||||
<ParseNovelSection
|
||||
onNavigateToParseChapter={() => setActiveTab('parse-chapter')}
|
||||
onNavigateToParseChapter={handleNavigateToParseChapter}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user