mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Chore: Port over Home & Fix Suwayomi-Server Detection on Web
This commit is contained in:
@@ -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++
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
})
|
||||
Reference in New Issue
Block a user