mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 04:41:23 +08:00
feat: open-source 小浣熊阅读 for iOS
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
final class ReadingActivityModel: ObservableObject {
|
||||
@Published private(set) var bookmarks: [Bookmark] = []
|
||||
@Published private(set) var readRecords: [ReadRecord] = []
|
||||
@Published private(set) var errorMessage: String?
|
||||
|
||||
private let repository: any ReadingActivityRepository
|
||||
private let service = ReadingActivityService()
|
||||
private let deviceID: String
|
||||
private var sessions: [String: Date] = [:]
|
||||
|
||||
init(repository: any ReadingActivityRepository, deviceID: String) {
|
||||
self.repository = repository
|
||||
self.deviceID = deviceID
|
||||
}
|
||||
|
||||
static func live() -> ReadingActivityModel {
|
||||
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent("Legado", isDirectory: true)
|
||||
let key = "legado.reading.device-id"
|
||||
let defaults = UserDefaults.standard
|
||||
let identifier = defaults.string(forKey: key) ?? UUID().uuidString
|
||||
defaults.set(identifier, forKey: key)
|
||||
let bookmarksURL = base.appendingPathComponent("bookmarks.json")
|
||||
let recordsURL = base.appendingPathComponent("read-records.json")
|
||||
let repository: any ReadingActivityRepository
|
||||
if let database = try? LegadoDatabase(url: base.appendingPathComponent("legado.sqlite3")) {
|
||||
repository = SQLiteReadingActivityRepository(
|
||||
database: database,
|
||||
legacyBookmarksURL: bookmarksURL,
|
||||
legacyRecordsURL: recordsURL
|
||||
)
|
||||
} else {
|
||||
repository = FileReadingActivityRepository(bookmarksURL: bookmarksURL, recordsURL: recordsURL)
|
||||
}
|
||||
return ReadingActivityModel(repository: repository, deviceID: identifier)
|
||||
}
|
||||
|
||||
static func demo(bookmarks: [Bookmark] = [], records: [ReadRecord] = []) -> ReadingActivityModel {
|
||||
ReadingActivityModel(
|
||||
repository: InMemoryReadingActivityRepository(bookmarks: bookmarks, records: records),
|
||||
deviceID: "ios-simulator"
|
||||
)
|
||||
}
|
||||
|
||||
var summaries: [ReadRecordSummary] { service.summaries(readRecords) }
|
||||
var statistics: ReadingStatistics { service.statistics(readRecords) }
|
||||
|
||||
func summaries(searchKey: String, sortMode: Int) -> [ReadRecordSummary] {
|
||||
service.displayedSummaries(readRecords, searchKey: searchKey, sortMode: sortMode)
|
||||
}
|
||||
|
||||
func load() async {
|
||||
do {
|
||||
async let storedBookmarks = repository.loadBookmarks()
|
||||
async let storedRecords = repository.loadReadRecords()
|
||||
bookmarks = service.sortedBookmarks(try await storedBookmarks)
|
||||
readRecords = try await storedRecords
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
func bookmarks(for book: Book) -> [Bookmark] {
|
||||
bookmarks.filter { $0.bookName == book.name && $0.bookAuthor == book.author }
|
||||
.sorted {
|
||||
if $0.chapterIndex == $1.chapterIndex { return $0.chapterPosition < $1.chapterPosition }
|
||||
return $0.chapterIndex < $1.chapterIndex
|
||||
}
|
||||
}
|
||||
|
||||
func addBookmark(book: Book, chapter: BookChapter, position: Int, content: String) async {
|
||||
var timestamp = Int64(Date().timeIntervalSince1970 * 1_000)
|
||||
while bookmarks.contains(where: { $0.time == timestamp }) { timestamp += 1 }
|
||||
var bookmark = Bookmark(time: timestamp, bookName: book.name, bookAuthor: book.author)
|
||||
bookmark.chapterIndex = chapter.index
|
||||
bookmark.chapterPosition = max(position, 0)
|
||||
bookmark.chapterName = chapter.title
|
||||
bookmark.bookText = excerpt(content, around: position)
|
||||
var updated = bookmarks
|
||||
updated.append(bookmark)
|
||||
do {
|
||||
try await repository.saveBookmarks(updated)
|
||||
bookmarks = service.sortedBookmarks(updated)
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
func removeBookmark(_ bookmark: Bookmark) async {
|
||||
let updated = bookmarks.filter { $0.id != bookmark.id }
|
||||
do {
|
||||
try await repository.saveBookmarks(updated)
|
||||
bookmarks = updated
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
func updateBookmark(_ bookmark: Bookmark) async {
|
||||
var updated = bookmarks
|
||||
if let index = updated.firstIndex(where: { $0.id == bookmark.id }) {
|
||||
updated[index] = bookmark
|
||||
} else {
|
||||
updated.append(bookmark)
|
||||
}
|
||||
do {
|
||||
try await repository.saveBookmarks(updated)
|
||||
bookmarks = service.sortedBookmarks(updated)
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
func beginReading(_ book: Book, now: Date = Date()) {
|
||||
sessions[book.bookUrl] = sessions[book.bookUrl] ?? now
|
||||
}
|
||||
|
||||
func checkpoint(_ book: Book, now: Date = Date()) async {
|
||||
guard let start = sessions[book.bookUrl] else {
|
||||
sessions[book.bookUrl] = now
|
||||
return
|
||||
}
|
||||
sessions[book.bookUrl] = now
|
||||
await record(book: book, elapsed: now.timeIntervalSince(start), now: now)
|
||||
}
|
||||
|
||||
func endReading(_ book: Book, now: Date = Date()) async {
|
||||
guard let start = sessions.removeValue(forKey: book.bookUrl) else { return }
|
||||
await record(book: book, elapsed: now.timeIntervalSince(start), now: now)
|
||||
}
|
||||
|
||||
func cancelReading(_ book: Book) {
|
||||
sessions.removeValue(forKey: book.bookUrl)
|
||||
}
|
||||
|
||||
func removeHistory(bookName: String) async {
|
||||
let updated = readRecords.filter { $0.bookName != bookName }
|
||||
do {
|
||||
try await repository.saveReadRecords(updated)
|
||||
readRecords = updated
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
func clearHistory() async {
|
||||
do {
|
||||
try await repository.saveReadRecords([])
|
||||
readRecords = []
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
func clearError() { errorMessage = nil }
|
||||
|
||||
private func record(book: Book, elapsed: TimeInterval, now: Date) async {
|
||||
let milliseconds = Int64(max(elapsed, 0) * 1_000)
|
||||
guard milliseconds > 0 else { return }
|
||||
let updated = service.addingReadTime(
|
||||
milliseconds,
|
||||
deviceID: deviceID,
|
||||
bookName: book.name,
|
||||
now: Int64(now.timeIntervalSince1970 * 1_000),
|
||||
to: readRecords
|
||||
)
|
||||
readRecords = updated
|
||||
do {
|
||||
try await repository.saveReadRecords(updated)
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
private func excerpt(_ content: String, around position: Int) -> String {
|
||||
guard !content.isEmpty else { return "" }
|
||||
let center = min(max(position, 0), content.count)
|
||||
let start = max(center - 24, 0)
|
||||
let end = min(center + 96, content.count)
|
||||
let lower = content.index(content.startIndex, offsetBy: start)
|
||||
let upper = content.index(content.startIndex, offsetBy: end)
|
||||
return String(content[lower..<upper]).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user