Chore: Port over Home & Fix Suwayomi-Server Detection on Web

This commit is contained in:
Youwes09
2026-05-24 12:09:29 -05:00
parent 6c39ef538f
commit ae5d9748c7
42 changed files with 3195 additions and 1342 deletions
+42
View File
@@ -0,0 +1,42 @@
export interface HistoryEntry {
mangaId: number
mangaTitle: string
thumbnailUrl: string
chapterId: number
chapterName: string
chapterNumber: number
pageNumber: number
readAt: number
}
export interface ReadingStats {
currentStreakDays: number
totalChaptersRead: number
totalMinutesRead: number
totalMangaRead: number
longestStreakDays: number
}
export const homeState = $state({
history: [] as HistoryEntry[],
dailyReadCounts: {} as Record<string, number>,
stats: {
currentStreakDays: 0,
totalChaptersRead: 0,
totalMinutesRead: 0,
totalMangaRead: 0,
longestStreakDays: 0,
} as ReadingStats,
heroSlots: [null, null, null, null] as [number | null, number | null, number | null, number | null],
})
export function setHeroSlot(i: 1 | 2 | 3, mangaId: number | null) {
homeState.heroSlots[i] = mangaId
}
export function recordRead(entry: HistoryEntry) {
homeState.history = [entry, ...homeState.history.filter(e => e.chapterId !== entry.chapterId)]
const dateStr = new Date(entry.readAt).toISOString().slice(0, 10)
homeState.dailyReadCounts[dateStr] = (homeState.dailyReadCounts[dateStr] ?? 0) + 1
homeState.stats.totalChaptersRead++
}
+80 -44
View File
@@ -2,52 +2,88 @@ import type { Manga } from '$lib/types'
import type { MangaStatus } from '$lib/server-adapters/types'
export type LibrarySortOption = 'alphabetical' | 'unread' | 'lastRead' | 'dateAdded'
export type LibraryTab = 'saved' | 'downloaded'
export const libraryState = $state({
items: [] as Manga[],
searchResults: [] as Manga[],
loading: false,
error: null as string | null,
filter: {
status: 'all' as MangaStatus | 'all',
tags: [] as string[],
unread: false,
query: '',
},
sort: 'alphabetical' as LibrarySortOption,
sortDesc: false,
view: 'grid' as 'grid' | 'list',
selected: new Set<string>(),
})
class LibraryState {
items = $state<Manga[]>([])
loading = $state(false)
error = $state<string | null>(null)
refreshing = $state(false)
export const filteredItems = $derived.by(() => {
let result = libraryState.items
tab = $state<LibraryTab>('saved')
sort = $state<LibrarySortOption>('alphabetical')
sortDesc = $state(false)
if (libraryState.filter.unread) {
result = result.filter(m => m.unreadCount > 0)
}
if (libraryState.filter.status !== 'all') {
result = result.filter(m => m.status === libraryState.filter.status)
}
if (libraryState.filter.tags.length > 0) {
result = result.filter(m =>
libraryState.filter.tags.every(tag => m.tags?.includes(tag))
)
}
if (libraryState.filter.query) {
const q = libraryState.filter.query.toLowerCase()
result = result.filter(m => m.title.toLowerCase().includes(q))
}
const sorted = [...result].sort((a, b) => {
switch (libraryState.sort) {
case 'unread': return (b.unreadCount ?? 0) - (a.unreadCount ?? 0)
case 'lastRead': return (b.lastReadAt ?? 0) - (a.lastReadAt ?? 0)
case 'dateAdded': return (b.addedAt ?? 0) - (a.addedAt ?? 0)
case 'alphabetical':
default: return a.title.localeCompare(b.title)
}
filter = $state({
status: 'all' as MangaStatus | 'all',
unread: false,
downloaded: false,
bookmarked: false,
query: '',
})
return libraryState.sortDesc ? sorted.reverse() : sorted
})
selected = $state(new Set<number>())
selectMode = $state(false)
filteredItems = $derived.by(() => {
let result = this.tab === 'downloaded'
? this.items.filter(m => (m.downloadCount ?? 0) > 0)
: this.items.filter(m => m.inLibrary)
if (this.filter.unread) result = result.filter(m => (m.unreadCount ?? 0) > 0)
if (this.filter.downloaded) result = result.filter(m => (m.downloadCount ?? 0) > 0)
if (this.filter.bookmarked) result = result.filter(m => (m.bookmarkCount ?? 0) > 0)
if (this.filter.status !== 'all') {
result = result.filter(
m => m.status?.toUpperCase().replace(/\s+/g, '_') === this.filter.status
)
}
if (this.filter.query) {
const q = this.filter.query.toLowerCase()
result = result.filter(m => m.title.toLowerCase().includes(q))
}
const sorted = [...result].sort((a, b) => {
switch (this.sort) {
case 'unread': return (b.unreadCount ?? 0) - (a.unreadCount ?? 0)
case 'lastRead': return (b.lastReadAt ?? 0) - (a.lastReadAt ?? 0)
case 'dateAdded': return (b.addedAt ?? 0) - (a.addedAt ?? 0)
default: return a.title.localeCompare(b.title)
}
})
return this.sortDesc ? sorted.reverse() : sorted
})
get hasActiveFilters() {
return this.filter.status !== 'all'
|| this.filter.unread
|| this.filter.downloaded
|| this.filter.bookmarked
}
enterSelect(id?: number) {
this.selectMode = true
if (id !== undefined) this.selected = new Set([id])
}
exitSelect() {
this.selectMode = false
this.selected = new Set()
}
toggleSelect(id: number) {
const next = new Set(this.selected)
if (next.has(id)) next.delete(id); else next.add(id)
this.selected = next
if (next.size === 0) this.exitSelect()
}
selectAll() {
this.selected = new Set(this.filteredItems.map(m => m.id))
}
}
export const libraryState = new LibraryState()
+22 -30
View File
@@ -1,36 +1,28 @@
import type { Manga, Chapter } from '$lib/types'
export const seriesState = $state({
current: null as Manga | null,
loading: false,
error: null as string | null,
class SeriesState {
current = $state<Manga | null>(null)
loading = $state(false)
error = $state<string | null>(null)
chapters: [] as Chapter[],
chaptersLoading: false,
chaptersError: null as string | null,
chapters = $state<Chapter[]>([])
chaptersLoading = $state(false)
chaptersError = $state<string | null>(null)
chapterFilter: {
unread: false,
downloaded: false,
query: '',
},
chapterSortDesc: true,
})
chapterSortDesc = $state(true)
chapterFilter = $state({ unread: false, downloaded: false, query: '' })
export const filteredChapters = $derived.by(() => {
let result = seriesState.chapters
filteredChapters = $derived.by(() => {
let result = this.chapters
if (this.chapterFilter.unread) result = result.filter(c => !c.read)
if (this.chapterFilter.downloaded) result = result.filter(c => c.downloaded)
if (this.chapterFilter.query) {
const q = this.chapterFilter.query.toLowerCase()
result = result.filter(c => c.name.toLowerCase().includes(q))
}
const sorted = [...result].sort((a, b) => a.chapterNumber - b.chapterNumber)
return this.chapterSortDesc ? sorted.reverse() : sorted
})
}
if (seriesState.chapterFilter.unread) {
result = result.filter(c => !c.read)
}
if (seriesState.chapterFilter.downloaded) {
result = result.filter(c => c.downloaded)
}
if (seriesState.chapterFilter.query) {
const q = seriesState.chapterFilter.query.toLowerCase()
result = result.filter(c => c.name.toLowerCase().includes(q))
}
const sorted = [...result].sort((a, b) => a.chapterNumber - b.chapterNumber)
return seriesState.chapterSortDesc ? sorted.reverse() : sorted
})
export const seriesState = new SeriesState()
+13 -5
View File
@@ -1,8 +1,16 @@
import type { Tracker } from '$lib/types'
export const trackingState = $state({
trackers: [] as Tracker[],
loading: false,
error: null as string | null,
syncing: false,
})
trackers: [] as Tracker[],
loading: false,
error: null as string | null,
syncing: false,
records: [] as unknown[],
recordsLoading: false,
recordsError: null as string | null,
searchResults: [] as unknown[],
searchLoading: false,
searchError: null as string | null,
})