From 48290e5fb59487029d9f91e8a195ff91137a5108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=A4=E3=81=9D=E3=82=99=E3=81=86?= Date: Sun, 4 Jul 2021 17:53:49 +0800 Subject: [PATCH 1/9] feat: DataBase definition & controller --- EhPanda.xcodeproj/project.pbxproj | 53 ++++++++- EhPanda/DataFlow/AppState.swift | 103 +++++++++--------- EhPanda/Database/MOProtocol.swift | 87 +++++++++++++++ .../MangaDetailMO+CoreDataClass.swift | 54 +++++++++ .../MangaDetailMO+CoreDataProperties.swift | 33 ++++++ EhPanda/Database/MangaMO+CoreDataClass.swift | 43 ++++++++ .../Database/MangaMO+CoreDataProperties.swift | 25 +++++ .../Model.xcdatamodel/contents | 41 +++++++ EhPanda/Database/Persistence.swift | 83 ++++++++++++++ 9 files changed, 468 insertions(+), 54 deletions(-) create mode 100755 EhPanda/Database/MOProtocol.swift create mode 100644 EhPanda/Database/MangaDetailMO+CoreDataClass.swift create mode 100644 EhPanda/Database/MangaDetailMO+CoreDataProperties.swift create mode 100644 EhPanda/Database/MangaMO+CoreDataClass.swift create mode 100644 EhPanda/Database/MangaMO+CoreDataProperties.swift create mode 100644 EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents create mode 100644 EhPanda/Database/Persistence.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 0a51a35f..24f9153c 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ AB018DD2268756D000EB0EA9 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = AB018DD1268756D000EB0EA9 /* Kanna */; }; AB19D619266E5C6700BA752A /* TTProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = AB19D618266E5C6700BA752A /* TTProgressHUD */; }; + AB2CED64268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB2CED63268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift */; }; AB2F72BF2685A8B00088DECA /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = AB2F72BE2685A8B00088DECA /* Kingfisher */; }; AB38A0CB25CA993D00764D64 /* ColorCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB38A0CA25CA993D00764D64 /* ColorCodable.swift */; }; AB40CFDC25983EC200D1DC9A /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB40CFDB25983EC200D1DC9A /* FileStorage.swift */; }; @@ -23,6 +24,8 @@ AB47FDB425BC859E0007765D /* Weird-ipad@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB47FDB125BC859E0007765D /* Weird-ipad@2x.png */; }; AB47FDB525BC859E0007765D /* Weird-ipad-pro@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB47FDB225BC859E0007765D /* Weird-ipad-pro@2x.png */; }; AB47FDB625BC859E0007765D /* Weird-ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = AB47FDB325BC859E0007765D /* Weird-ipad.png */; }; + AB4FD2C1268AB83300A95968 /* MangaDetailMO+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4FD2C0268AB83300A95968 /* MangaDetailMO+CoreDataProperties.swift */; }; + AB4FD2C5268B097F00A95968 /* MOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4FD2C4268B097F00A95968 /* MOProtocol.swift */; }; AB6DE897268822390087C579 /* LogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6DE896268822390087C579 /* LogsView.swift */; }; AB6DE89A268861790087C579 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = AB6DE899268861790087C579 /* SwiftyBeaver */; }; AB7E6B3025D24FE00035CC68 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = AB7E6B3225D24FE00035CC68 /* InfoPlist.strings */; }; @@ -36,6 +39,10 @@ ABC3C7892593699B00E0C11B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC3C76D2593699A00E0C11B /* Defaults.swift */; }; ABC3C78F2593699B00E0C11B /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC3C7762593699A00E0C11B /* ViewModifiers.swift */; }; ABC3C7962593699B00E0C11B /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC3C7802593699A00E0C11B /* Models.swift */; }; + ABC681F326898D46007BBD69 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ABC681F126898D46007BBD69 /* Model.xcdatamodeld */; }; + ABCA93BE26918DE100A98BC6 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCA93BD26918DE100A98BC6 /* Persistence.swift */; }; + ABCA93C02691925900A98BC6 /* MangaMO+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCA93BF2691925900A98BC6 /* MangaMO+CoreDataClass.swift */; }; + ABCA93C22691929D00A98BC6 /* MangaDetailMO+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCA93C12691929D00A98BC6 /* MangaDetailMO+CoreDataClass.swift */; }; ABCD2F0A259763FC008E5A20 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD2F09259763FC008E5A20 /* Request.swift */; }; ABCD2F0E25976B95008E5A20 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD2F0D25976B95008E5A20 /* Parser.swift */; }; ABD5FDD4263D05110021A4C6 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = ABD5FDD3263D05110021A4C6 /* .swiftlint.yml */; }; @@ -77,6 +84,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + AB2CED63268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaMO+CoreDataProperties.swift"; sourceTree = ""; }; AB38A0CA25CA993D00764D64 /* ColorCodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorCodable.swift; sourceTree = ""; }; AB40CFDB25983EC200D1DC9A /* FileStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileStorage.swift; sourceTree = ""; }; AB40CFDE25983EDF00D1DC9A /* FileHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHelper.swift; sourceTree = ""; }; @@ -90,6 +98,8 @@ AB47FDB125BC859E0007765D /* Weird-ipad@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Weird-ipad@2x.png"; sourceTree = ""; }; AB47FDB225BC859E0007765D /* Weird-ipad-pro@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Weird-ipad-pro@2x.png"; sourceTree = ""; }; AB47FDB325BC859E0007765D /* Weird-ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Weird-ipad.png"; sourceTree = ""; }; + AB4FD2C0268AB83300A95968 /* MangaDetailMO+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaDetailMO+CoreDataProperties.swift"; sourceTree = ""; }; + AB4FD2C4268B097F00A95968 /* MOProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MOProtocol.swift; sourceTree = ""; }; AB6DE896268822390087C579 /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = ""; }; AB7E6B3125D24FE00035CC68 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; AB7E6B3425D24FE40035CC68 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; @@ -107,6 +117,10 @@ ABC3C76E2593699A00E0C11B /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; ABC3C7762593699A00E0C11B /* ViewModifiers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = ""; }; ABC3C7802593699A00E0C11B /* Models.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + ABC681F226898D46007BBD69 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; + ABCA93BD26918DE100A98BC6 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; + ABCA93BF2691925900A98BC6 /* MangaMO+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaMO+CoreDataClass.swift"; sourceTree = ""; }; + ABCA93C12691929D00A98BC6 /* MangaDetailMO+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaDetailMO+CoreDataClass.swift"; sourceTree = ""; }; ABCD2F09259763FC008E5A20 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; ABCD2F0D25976B95008E5A20 /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; ABD5FDD3263D05110021A4C6 /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = SOURCE_ROOT; }; @@ -194,6 +208,20 @@ path = AltIcons; sourceTree = ""; }; + AB821BEF268A09AC009B2381 /* Database */ = { + isa = PBXGroup; + children = ( + ABC681F126898D46007BBD69 /* Model.xcdatamodeld */, + ABCA93BD26918DE100A98BC6 /* Persistence.swift */, + AB4FD2C4268B097F00A95968 /* MOProtocol.swift */, + ABCA93BF2691925900A98BC6 /* MangaMO+CoreDataClass.swift */, + AB2CED63268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift */, + ABCA93C12691929D00A98BC6 /* MangaDetailMO+CoreDataClass.swift */, + AB4FD2C0268AB83300A95968 /* MangaDetailMO+CoreDataProperties.swift */, + ); + path = Database; + sourceTree = ""; + }; ABC3C74B2593696C00E0C11B = { isa = PBXGroup; children = ( @@ -217,6 +245,7 @@ ABF45AB325F3312F00ECB568 /* DataFlow */, ABC3C77B2593699A00E0C11B /* Network */, ABC3C77F2593699A00E0C11B /* Models */, + AB821BEF268A09AC009B2381 /* Database */, ABF45ABF25F3313D00ECB568 /* View */, ABD5FDD3263D05110021A4C6 /* .swiftlint.yml */, ABF53F4725A306D200AB5918 /* EhPanda.entitlements */, @@ -477,6 +506,7 @@ ABF45AE925F3313D00ECB568 /* AlertView.swift in Sources */, ABF45ABB25F3312F00ECB568 /* AppError.swift in Sources */, ABF45AE025F3313D00ECB568 /* HomeView.swift in Sources */, + ABCA93BE26918DE100A98BC6 /* Persistence.swift in Sources */, ABF45AEF25F3313D00ECB568 /* TorrentsView.swift in Sources */, ABF45AB925F3312F00ECB568 /* AppState.swift in Sources */, ABF45AF625F3313D00ECB568 /* AppearanceSettingView.swift in Sources */, @@ -484,15 +514,19 @@ ABC3C7892593699B00E0C11B /* Defaults.swift in Sources */, ABF45AF125F3313D00ECB568 /* AssociatedView.swift in Sources */, ABF45ADF25F3313D00ECB568 /* FilterView.swift in Sources */, + AB4FD2C5268B097F00A95968 /* MOProtocol.swift in Sources */, AB40CFDF25983EDF00D1DC9A /* FileHelper.swift in Sources */, ABC3C78F2593699B00E0C11B /* ViewModifiers.swift in Sources */, ABF45AF825F3313D00ECB568 /* EhPandaView.swift in Sources */, ABC3C7862593699B00E0C11B /* Utility.swift in Sources */, ABF45AE725F3313D00ECB568 /* RatingView.swift in Sources */, + AB2CED64268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift in Sources */, ABCD2F0E25976B95008E5A20 /* Parser.swift in Sources */, ABF45AF725F3313D00ECB568 /* SettingView.swift in Sources */, ABF45AEA25F3313D00ECB568 /* Placeholder.swift in Sources */, + ABC681F326898D46007BBD69 /* Model.xcdatamodeld in Sources */, ABF45AE125F3313D00ECB568 /* Home.swift in Sources */, + AB4FD2C1268AB83300A95968 /* MangaDetailMO+CoreDataProperties.swift in Sources */, AB6DE897268822390087C579 /* LogsView.swift in Sources */, ABF45AF025F3313D00ECB568 /* CommentView.swift in Sources */, ABF45AE325F3313D00ECB568 /* SlideMenu.swift in Sources */, @@ -504,9 +538,11 @@ ABF45AF225F3313D00ECB568 /* GeneralSettingView.swift in Sources */, ABF45ABA25F3312F00ECB568 /* AppCommand.swift in Sources */, ABF45AE425F3313D00ECB568 /* TagCloudView.swift in Sources */, + ABCA93C22691929D00A98BC6 /* MangaDetailMO+CoreDataClass.swift in Sources */, ABF45AEE25F3313D00ECB568 /* ArchiveView.swift in Sources */, ABF45AEC25F3313D00ECB568 /* ContentView.swift in Sources */, ABEA1FE625A9B40B002966B9 /* Setting.swift in Sources */, + ABCA93C02691925900A98BC6 /* MangaMO+CoreDataClass.swift in Sources */, ABC3C7962593699B00E0C11B /* Models.swift in Sources */, ABC3C7872593699B00E0C11B /* EhPandaApp.swift in Sources */, ABF313A525B1AB6600D47A2F /* Misc.swift in Sources */, @@ -674,7 +710,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = EhPanda/EhPanda.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 9SKQ7QTZ74; ENABLE_PREVIEWS = YES; @@ -701,7 +737,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = EhPanda/EhPanda.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 9SKQ7QTZ74; ENABLE_PREVIEWS = YES; @@ -814,6 +850,19 @@ productName = SDWebImageSwiftUI; }; /* End XCSwiftPackageProductDependency section */ + +/* Begin XCVersionGroup section */ + ABC681F126898D46007BBD69 /* Model.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + ABC681F226898D46007BBD69 /* Model.xcdatamodel */, + ); + currentVersion = ABC681F226898D46007BBD69 /* Model.xcdatamodel */; + path = Model.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = ABC3C74C2593696C00E0C11B /* Project object */; } diff --git a/EhPanda/DataFlow/AppState.swift b/EhPanda/DataFlow/AppState.swift index 281e9eb2..23b3aa73 100644 --- a/EhPanda/DataFlow/AppState.swift +++ b/EhPanda/DataFlow/AppState.swift @@ -310,82 +310,81 @@ extension AppState { extension AppState { // MARK: CachedList struct CachedList { - @FileStorage(directory: .cachesDirectory, fileName: "cachedList.json") - var items: [String: Manga]? - func hasCached(gid: String) -> Bool { - items?[gid] != nil + PersistenceController.checkExistence(entityName: "MangaMO", gid: gid) } mutating func cache(mangas: [Manga]) { - if items == nil { - items = Dictionary(uniqueKeysWithValues: mangas.map { ($0.id, $0) }) - return - } - - for manga in mangas { - if items?[manga.gid] == nil { - items?[manga.gid] = manga - } else { - items?[manga.gid]?.title = manga.title - items?[manga.gid]?.rating = manga.rating - items?[manga.gid]?.tags = manga.tags - items?[manga.gid]?.language = manga.language - } - } + PersistenceController.add(items: mangas) +// if items == nil { +// items = Dictionary(uniqueKeysWithValues: mangas.map { ($0.id, $0) }) +// return +// } +// +// for manga in mangas { +// if items?[manga.gid] == nil { +// items?[manga.gid] = manga +// } else { +// items?[manga.gid]?.title = manga.title +// items?[manga.gid]?.rating = manga.rating +// items?[manga.gid]?.tags = manga.tags +// items?[manga.gid]?.language = manga.language +// } +// } } mutating func insertDetail(gid: String, detail: MangaDetail) { - items?[gid]?.detail = detail + PersistenceController.update(gid: gid, detail: detail) +// items?[gid]?.detail = detail } mutating func insertArchive(gid: String, archive: MangaArchive) { - items?[gid]?.detail?.archive = archive +// items?[gid]?.detail?.archive = archive } mutating func insertTorrents(gid: String, torrents: [MangaTorrent]) { - items?[gid]?.detail?.torrents = torrents +// items?[gid]?.detail?.torrents = torrents } mutating func updateDetail(gid: String, detail: MangaDetail) { - items?[gid]?.detail?.isFavored = detail.isFavored - items?[gid]?.detail?.archiveURL = detail.archiveURL - items?[gid]?.detail?.detailTags = detail.detailTags - items?[gid]?.detail?.comments = detail.comments - items?[gid]?.detail?.jpnTitle = detail.jpnTitle - items?[gid]?.detail?.likeCount = detail.likeCount - items?[gid]?.detail?.pageCount = detail.pageCount - items?[gid]?.detail?.sizeCount = detail.sizeCount - items?[gid]?.detail?.sizeType = detail.sizeType - items?[gid]?.detail?.rating = detail.rating - items?[gid]?.detail?.ratingCount = detail.ratingCount - items?[gid]?.detail?.torrentCount = detail.torrentCount +// items?[gid]?.detail?.isFavored = detail.isFavored +// items?[gid]?.detail?.archiveURL = detail.archiveURL +// items?[gid]?.detail?.detailTags = detail.detailTags +// items?[gid]?.detail?.comments = detail.comments +// items?[gid]?.detail?.jpnTitle = detail.jpnTitle +// items?[gid]?.detail?.likeCount = detail.likeCount +// items?[gid]?.detail?.pageCount = detail.pageCount +// items?[gid]?.detail?.sizeCount = detail.sizeCount +// items?[gid]?.detail?.sizeType = detail.sizeType +// items?[gid]?.detail?.rating = detail.rating +// items?[gid]?.detail?.ratingCount = detail.ratingCount +// items?[gid]?.detail?.torrentCount = detail.torrentCount } mutating func insertAlterImages(gid: String, images: [MangaAlterData]) { - items?[gid]?.detail?.alterImages = images +// items?[gid]?.detail?.alterImages = images } mutating func updateComments(gid: String, comments: [MangaComment]) { - items?[gid]?.detail?.comments = comments +// items?[gid]?.detail?.comments = comments } mutating func insertContents(gid: String, pageNum: PageNumber, contents: [MangaContent]) { - items?[gid]?.detail?.currentPageNum = pageNum.current - items?[gid]?.detail?.pageNumMaximum = pageNum.maximum - - if items?[gid]?.contents == nil { - items?[gid]?.contents = contents.sorted { $0.tag < $1.tag } - } else { - contents.forEach { content in - if items?[gid]?.contents?.contains(content) == false { - items?[gid]?.contents?.append(content) - } - } - items?[gid]?.contents?.sort { $0.tag < $1.tag } - } +// items?[gid]?.detail?.currentPageNum = pageNum.current +// items?[gid]?.detail?.pageNumMaximum = pageNum.maximum +// +// if items?[gid]?.contents == nil { +// items?[gid]?.contents = contents.sorted { $0.tag < $1.tag } +// } else { +// contents.forEach { content in +// if items?[gid]?.contents?.contains(content) == false { +// items?[gid]?.contents?.append(content) +// } +// } +// items?[gid]?.contents?.sort { $0.tag < $1.tag } +// } } mutating func insertAspectBox(gid: String, box: [Int: CGFloat]) { - items?[gid]?.detail?.aspectBox = box +// items?[gid]?.detail?.aspectBox = box } mutating func insertReadingProgress(gid: String, progress: Int) { - items?[gid]?.detail?.readingProgress = progress +// items?[gid]?.detail?.readingProgress = progress } mutating func updateUserRating(gid: String, rating: Float) { - items?[gid]?.detail?.userRating = rating +// items?[gid]?.detail?.userRating = rating } } } diff --git a/EhPanda/Database/MOProtocol.swift b/EhPanda/Database/MOProtocol.swift new file mode 100755 index 00000000..283fed1a --- /dev/null +++ b/EhPanda/Database/MOProtocol.swift @@ -0,0 +1,87 @@ +// +// ManagedObjectProtocol.swift +// Created by swifting.io Team +// + +import CoreData +import SwiftyBeaver + +protocol ManagedObjectProtocol { + associatedtype Entity + func toEntity() -> Entity +} +protocol ManagedObjectConvertible { + associatedtype ManagedObject: NSManagedObject, ManagedObjectProtocol + func toManagedObject(in context: NSManagedObjectContext) -> ManagedObject +} + +extension ManagedObjectProtocol where Self: NSManagedObject { + + static func getOrCreateSingle( + with id: String, + and key: String = "gid", + from context: NSManagedObjectContext + ) -> Self { + let result = single( + with: id, + from: context + ) ?? insertNew(in: context) + result.setValue(id, forKey: key) + + return result + } + + static func single( + from context: NSManagedObjectContext, + with predicate: NSPredicate?, + sortDescriptors: [NSSortDescriptor]? + ) -> Self? { + fetch( + from: context, + with: predicate, + sortDescriptors: sortDescriptors, + fetchLimit: 1 + )?.first + } + + static func single( + with id: String, + and key: String = "gid", + from context: NSManagedObjectContext + ) -> Self? { + let predicate = NSPredicate(format: "%@ == %@", key, id) + return single(from: context, with: predicate, sortDescriptors: nil) + } + + static func insertNew(in context: NSManagedObjectContext) -> Self { + Self(context: context) + } + + static func fetch( + from context: NSManagedObjectContext, + with predicate: NSPredicate?, + sortDescriptors: [NSSortDescriptor]?, + fetchLimit: Int? + ) -> [Self]? { + + let fetchRequest = Self.fetchRequest() + fetchRequest.sortDescriptors = sortDescriptors + fetchRequest.predicate = predicate + fetchRequest.returnsObjectsAsFaults = false + + if let fetchLimit = fetchLimit { + fetchRequest.fetchLimit = fetchLimit + } + + var result: [Self]? + context.performAndWait { () -> Void in + do { + result = try context.fetch(fetchRequest) as? [Self] + } catch { + result = nil + SwiftyBeaver.error("CoreData fetch error \(error)") + } + } + return result + } +} diff --git a/EhPanda/Database/MangaDetailMO+CoreDataClass.swift b/EhPanda/Database/MangaDetailMO+CoreDataClass.swift new file mode 100644 index 00000000..230ce5e6 --- /dev/null +++ b/EhPanda/Database/MangaDetailMO+CoreDataClass.swift @@ -0,0 +1,54 @@ +// +// MangaDetailMO+CoreDataClass.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/07/04. +// + +import CoreData + +public class MangaDetailMO: NSManagedObject {} + +extension MangaDetailMO: ManagedObjectProtocol { + func toEntity() -> MangaDetail { + MangaDetail( + isFavored: isFavored, alterImagesURL: nil, + alterImages: [], torrents: [], comments: [], + previews: [], title: title, rating: rating, + ratingCount: ratingCount, detailTags: [], + category: Category(rawValue: category)!, + language: Language(rawValue: language)!, + uploader: uploader, publishedDate: publishedDate, + coverURL: coverURL, likeCount: likeCount, + pageCount: pageCount, sizeCount: sizeCount, + sizeType: sizeType, torrentCount: Int(torrentCount) + ) + } +} +extension MangaDetail: ManagedObjectConvertible { + func toManagedObject(in context: NSManagedObjectContext) -> MangaDetailMO { + let mangaDetailMO = MangaDetailMO(context: context) +// let mangaDetailMO = MangaDetailMO.getOrCreateSingle( +// with: "gid", from: context +// ) + mangaDetailMO.archiveURL = archiveURL + mangaDetailMO.category = category.rawValue + mangaDetailMO.coverURL = coverURL + mangaDetailMO.isFavored = isFavored + mangaDetailMO.jpnTitle = jpnTitle + mangaDetailMO.language = language.rawValue + mangaDetailMO.likeCount = likeCount + mangaDetailMO.pageCount = pageCount + mangaDetailMO.publishedDate = publishedDate + mangaDetailMO.rating = rating + mangaDetailMO.ratingCount = ratingCount + mangaDetailMO.sizeCount = sizeCount + mangaDetailMO.sizeType = sizeType + mangaDetailMO.title = title + mangaDetailMO.torrentCount = Int64(torrentCount) + mangaDetailMO.uploader = uploader + mangaDetailMO.userRating = userRating ?? 0.0 + + return mangaDetailMO + } +} diff --git a/EhPanda/Database/MangaDetailMO+CoreDataProperties.swift b/EhPanda/Database/MangaDetailMO+CoreDataProperties.swift new file mode 100644 index 00000000..f3813937 --- /dev/null +++ b/EhPanda/Database/MangaDetailMO+CoreDataProperties.swift @@ -0,0 +1,33 @@ +// +// MangaDetailMO+CoreDataProperties.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/06/29. +// + +import CoreData + +extension MangaDetailMO: Identifiable { + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "MangaDetailMO") + } + + @NSManaged public var archiveURL: String? + @NSManaged public var category: String + @NSManaged public var coverURL: String + @NSManaged public var gid: String + @NSManaged public var isFavored: Bool + @NSManaged public var jpnTitle: String? + @NSManaged public var language: String + @NSManaged public var likeCount: String + @NSManaged public var pageCount: String + @NSManaged public var publishedDate: Date + @NSManaged public var rating: Float + @NSManaged public var ratingCount: String + @NSManaged public var sizeCount: String + @NSManaged public var sizeType: String + @NSManaged public var title: String + @NSManaged public var torrentCount: Int64 + @NSManaged public var uploader: String + @NSManaged public var userRating: Float +} diff --git a/EhPanda/Database/MangaMO+CoreDataClass.swift b/EhPanda/Database/MangaMO+CoreDataClass.swift new file mode 100644 index 00000000..c99586b8 --- /dev/null +++ b/EhPanda/Database/MangaMO+CoreDataClass.swift @@ -0,0 +1,43 @@ +// +// MangaMO+CoreDataClass.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/07/04. +// + +import CoreData + +public class MangaMO: NSManagedObject {} + +extension MangaMO: ManagedObjectProtocol { + func toEntity() -> Manga { + Manga( + detail: nil, contents: [], gid: gid, token: token, + title: title, rating: rating, tags: [], + category: Category(rawValue: category)!, + language: Language(rawValue: language ?? ""), + uploader: uploader, publishedDate: publishedDate, + coverURL: coverURL, detailURL: detailURL, + lastOpenTime: nil + ) + } +} +extension Manga: ManagedObjectConvertible { + func toManagedObject(in context: NSManagedObjectContext) -> MangaMO { + let mangaMO = MangaMO(context: context) +// let mangaMO = MangaMO.getOrCreateSingle( +// with: gid, from: context +// ) + mangaMO.category = category.rawValue + mangaMO.coverURL = coverURL + mangaMO.detailURL = detailURL + mangaMO.language = language?.rawValue + mangaMO.publishedDate = publishedDate + mangaMO.rating = rating + mangaMO.title = title + mangaMO.token = token + mangaMO.uploader = uploader + + return mangaMO + } +} diff --git a/EhPanda/Database/MangaMO+CoreDataProperties.swift b/EhPanda/Database/MangaMO+CoreDataProperties.swift new file mode 100644 index 00000000..f18d9022 --- /dev/null +++ b/EhPanda/Database/MangaMO+CoreDataProperties.swift @@ -0,0 +1,25 @@ +// +// MangaMO+CoreDataProperties.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/06/29. +// + +import CoreData + +extension MangaMO: Identifiable { + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "MangaMO") + } + + @NSManaged public var category: String + @NSManaged public var coverURL: String + @NSManaged public var detailURL: String + @NSManaged public var gid: String + @NSManaged public var language: String? + @NSManaged public var publishedDate: Date + @NSManaged public var rating: Float + @NSManaged public var title: String + @NSManaged public var token: String + @NSManaged public var uploader: String? +} diff --git a/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents b/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents new file mode 100644 index 00000000..a6920f57 --- /dev/null +++ b/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EhPanda/Database/Persistence.swift b/EhPanda/Database/Persistence.swift new file mode 100644 index 00000000..f40a6c79 --- /dev/null +++ b/EhPanda/Database/Persistence.swift @@ -0,0 +1,83 @@ +// +// Persistence.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/07/04. +// + +import CoreData +import SwiftyBeaver + +struct PersistenceController { + static let shared = PersistenceController() + + let container: NSPersistentCloudKitContainer = { + let container = NSPersistentCloudKitContainer(name: "Model") + container.loadPersistentStores { + SwiftyBeaver.error($1 as Any) + } + return container + }() + + static func saveContext() { + let context = shared.container.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + SwiftyBeaver.error(error) + fatalError("Unresolved error \(error)") + } + } + } + + static func checkExistence(entityName: String, gid: String) -> Bool { + let request = NSFetchRequest(entityName: entityName) + request.fetchLimit = 1 + request.predicate = NSPredicate(format: "gid == %@", gid) + return (try? shared.container.viewContext.count(for: request)) ?? 0 > 0 + } + + static func fetch(entityName: String, gid: String) -> E? { + let request = NSFetchRequest(entityName: entityName) + request.fetchLimit = 1 + request.predicate = NSPredicate(format: "gid == %@", gid) + return try? shared.container.viewContext.fetch(request).first + } + + static func add(items: [Manga]) { + for item in items { + if let storedMangaMO: MangaMO = + fetch(entityName: "MangaMO", gid: item.gid) + { + storedMangaMO.title = item.title + storedMangaMO.rating = item.rating +// storedMangaMO.tags = item.tags + storedMangaMO.language = item.language?.rawValue + } else { + _ = item.toManagedObject(in: shared.container.viewContext) + } + } + saveContext() + } + + static func update(gid: String, detail: MangaDetail) { + if let storedMangaDetailMO: MangaDetailMO = + fetch(entityName: "MangaDetailMO", gid: gid) + { + storedMangaDetailMO.isFavored = detail.isFavored + storedMangaDetailMO.archiveURL = detail.archiveURL +// storedMangaDetailMO.detailTags = detail.detailTags +// storedMangaDetailMO.comments = detail.comments + storedMangaDetailMO.jpnTitle = detail.jpnTitle + storedMangaDetailMO.likeCount = detail.likeCount + storedMangaDetailMO.pageCount = detail.pageCount + storedMangaDetailMO.sizeCount = detail.sizeCount + storedMangaDetailMO.sizeType = detail.sizeType + storedMangaDetailMO.rating = detail.rating + storedMangaDetailMO.ratingCount = detail.ratingCount + storedMangaDetailMO.torrentCount = Int64(detail.torrentCount) + } + saveContext() + } +} From 19fef85fc8d461ec58765d85c95ee9512f1b4d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=A4=E3=81=9D=E3=82=99=E3=81=86?= Date: Sun, 4 Jul 2021 21:25:52 +0800 Subject: [PATCH 2/9] feat: Restore home & detail function --- EhPanda.xcodeproj/project.pbxproj | 4 - EhPanda/App/Tools/Parser.swift | 3 +- EhPanda/DataFlow/AppAction.swift | 2 +- EhPanda/DataFlow/AppCommand.swift | 7 +- EhPanda/DataFlow/Store.swift | 226 +++++++++--------- EhPanda/Database/MOProtocol.swift | 87 ------- .../MangaDetailMO+CoreDataClass.swift | 4 +- EhPanda/Database/MangaMO+CoreDataClass.swift | 2 + .../Model.xcdatamodel/contents | 10 +- EhPanda/Database/Persistence.swift | 21 +- EhPanda/Models/Models.swift | 2 + EhPanda/Network/Request.swift | 8 +- EhPanda/View/Content/ContentView.swift | 8 +- EhPanda/View/Detail/ArchiveView.swift | 7 +- EhPanda/View/Detail/CommentView.swift | 4 +- EhPanda/View/Detail/DetailView.swift | 14 +- EhPanda/View/Detail/TorrentsView.swift | 36 ++- 17 files changed, 207 insertions(+), 238 deletions(-) delete mode 100755 EhPanda/Database/MOProtocol.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 24f9153c..1ce0b954 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -25,7 +25,6 @@ AB47FDB525BC859E0007765D /* Weird-ipad-pro@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB47FDB225BC859E0007765D /* Weird-ipad-pro@2x.png */; }; AB47FDB625BC859E0007765D /* Weird-ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = AB47FDB325BC859E0007765D /* Weird-ipad.png */; }; AB4FD2C1268AB83300A95968 /* MangaDetailMO+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4FD2C0268AB83300A95968 /* MangaDetailMO+CoreDataProperties.swift */; }; - AB4FD2C5268B097F00A95968 /* MOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4FD2C4268B097F00A95968 /* MOProtocol.swift */; }; AB6DE897268822390087C579 /* LogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6DE896268822390087C579 /* LogsView.swift */; }; AB6DE89A268861790087C579 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = AB6DE899268861790087C579 /* SwiftyBeaver */; }; AB7E6B3025D24FE00035CC68 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = AB7E6B3225D24FE00035CC68 /* InfoPlist.strings */; }; @@ -99,7 +98,6 @@ AB47FDB225BC859E0007765D /* Weird-ipad-pro@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Weird-ipad-pro@2x.png"; sourceTree = ""; }; AB47FDB325BC859E0007765D /* Weird-ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Weird-ipad.png"; sourceTree = ""; }; AB4FD2C0268AB83300A95968 /* MangaDetailMO+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaDetailMO+CoreDataProperties.swift"; sourceTree = ""; }; - AB4FD2C4268B097F00A95968 /* MOProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MOProtocol.swift; sourceTree = ""; }; AB6DE896268822390087C579 /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = ""; }; AB7E6B3125D24FE00035CC68 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; AB7E6B3425D24FE40035CC68 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; @@ -213,7 +211,6 @@ children = ( ABC681F126898D46007BBD69 /* Model.xcdatamodeld */, ABCA93BD26918DE100A98BC6 /* Persistence.swift */, - AB4FD2C4268B097F00A95968 /* MOProtocol.swift */, ABCA93BF2691925900A98BC6 /* MangaMO+CoreDataClass.swift */, AB2CED63268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift */, ABCA93C12691929D00A98BC6 /* MangaDetailMO+CoreDataClass.swift */, @@ -514,7 +511,6 @@ ABC3C7892593699B00E0C11B /* Defaults.swift in Sources */, ABF45AF125F3313D00ECB568 /* AssociatedView.swift in Sources */, ABF45ADF25F3313D00ECB568 /* FilterView.swift in Sources */, - AB4FD2C5268B097F00A95968 /* MOProtocol.swift in Sources */, AB40CFDF25983EDF00D1DC9A /* FileHelper.swift in Sources */, ABC3C78F2593699B00E0C11B /* ViewModifiers.swift in Sources */, ABF45AF825F3313D00ECB568 /* EhPandaView.swift in Sources */, diff --git a/EhPanda/App/Tools/Parser.swift b/EhPanda/App/Tools/Parser.swift index 5c040d9a..947d55a6 100644 --- a/EhPanda/App/Tools/Parser.swift +++ b/EhPanda/App/Tools/Parser.swift @@ -99,7 +99,7 @@ struct Parser { } // MARK: Detail - static func parseMangaDetail(doc: HTMLDocument) throws -> MangaDetail { + static func parseMangaDetail(doc: HTMLDocument, gid: String) throws -> MangaDetail { func parseCoverURL(node: XMLElement?) throws -> String { guard let coverHTML = node?.at_xpath("//div [@id='gd1']")?.innerHTML, let rangeA = coverHTML.range(of: "url("), @@ -293,6 +293,7 @@ struct Parser { torrents: [], comments: parseComments(doc: doc), previews: previews, + gid: gid, title: engTitle, jpnTitle: jpnTitle, rating: rating, diff --git a/EhPanda/DataFlow/AppAction.swift b/EhPanda/DataFlow/AppAction.swift index b8d97447..4a591eeb 100644 --- a/EhPanda/DataFlow/AppAction.swift +++ b/EhPanda/DataFlow/AppAction.swift @@ -68,7 +68,7 @@ enum AppAction { case fetchFavoritesItemsDone(carriedValue: FavoritesIndex, result: Result<(PageNumber, [Manga]), AppError>) case fetchMoreFavoritesItems(index: Int) case fetchMoreFavoritesItemsDone(carriedValue: FavoritesIndex, result: Result<(PageNumber, [Manga]), AppError>) - case fetchMangaDetail(gid: String) + case fetchMangaDetail(gid: String, detailURL: String) case fetchMangaDetailDone(result: Result<(Identity, MangaDetail, APIKey), AppError>) case fetchMangaArchive(gid: String) case fetchMangaArchiveDone(result: Result<(Identity, MangaArchive, CurrentGP?, CurrentCredits?), AppError>) diff --git a/EhPanda/DataFlow/AppCommand.swift b/EhPanda/DataFlow/AppCommand.swift index 9a9b4d45..7b9f66bc 100644 --- a/EhPanda/DataFlow/AppCommand.swift +++ b/EhPanda/DataFlow/AppCommand.swift @@ -292,7 +292,7 @@ struct FetchMangaDetailCommand: AppCommand { func execute(in store: Store) { let token = SubscriptionToken() - MangaDetailRequest(detailURL: detailURL) + MangaDetailRequest(gid: gid, detailURL: detailURL) .publisher .receive(on: DispatchQueue.main) .sink { completion in @@ -341,11 +341,12 @@ struct FetchMangaArchiveCommand: AppCommand { } struct FetchMangaArchiveFundsCommand: AppCommand { + let gid: String let detailURL: String func execute(in store: Store) { let sToken = SubscriptionToken() - MangaArchiveFundsRequest(detailURL: detailURL) + MangaArchiveFundsRequest(gid: gid, detailURL: detailURL) .publisher .receive(on: DispatchQueue.main) .sink { completion in @@ -460,7 +461,7 @@ struct UpdateMangaDetailCommand: AppCommand { func execute(in store: Store) { let token = SubscriptionToken() - MangaDetailRequest(detailURL: detailURL) + MangaDetailRequest(gid: gid, detailURL: detailURL) .publisher .receive(on: DispatchQueue.main) .sink { completion in diff --git a/EhPanda/DataFlow/Store.swift b/EhPanda/DataFlow/Store.swift index bebc4d4e..1fdf0994 100644 --- a/EhPanda/DataFlow/Store.swift +++ b/EhPanda/DataFlow/Store.swift @@ -43,7 +43,9 @@ final class Store: ObservableObject { case .replaceUser(let user): appState.settings.user = user case .clearCachedList: - appState.cachedList.items = nil + break + // debugMark +// appState.cachedList.items = nil case .clearHistoryItems: appState.homeInfo.historyItems = nil case .initializeStates: @@ -70,8 +72,10 @@ final class Store: ObservableObject { case .updateAppIconType(let iconType): appState.settings.setting?.appIconType = iconType case .updateHistoryItems(let gid): - let item = appState.cachedList.items?[gid] - appState.homeInfo.insertHistoryItem(manga: item) + // debugMark + break +// let item = appState.cachedList.items?[gid] +// appState.homeInfo.insertHistoryItem(manga: item) case .updateHistoryKeywords(let text): appState.homeInfo.insertHistoryKeyword(text: text) case .clearHistoryKeywords: @@ -490,13 +494,12 @@ final class Store: ObservableObject { appState.homeInfo.moreFavoritesLoading[carriedValue] = true } - case .fetchMangaDetail(let gid): + case .fetchMangaDetail(let gid, let detailURL): appState.detailInfo.mangaDetailLoadFailed = false if appState.detailInfo.mangaDetailLoading { break } appState.detailInfo.mangaDetailLoading = true - let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" appCommand = FetchMangaDetailCommand(gid: gid, detailURL: detailURL) case .fetchMangaDetailDone(let result): appState.detailInfo.mangaDetailLoading = false @@ -514,9 +517,9 @@ final class Store: ObservableObject { if appState.detailInfo.mangaArchiveLoading { break } appState.detailInfo.mangaArchiveLoading = true - - let archiveURL = appState.cachedList.items?[gid]?.detail?.archiveURL ?? "" - appCommand = FetchMangaArchiveCommand(gid: gid, archiveURL: archiveURL) + // debugMark +// let archiveURL = appState.cachedList.items?[gid]?.detail?.archiveURL ?? "" +// appCommand = FetchMangaArchiveCommand(gid: gid, archiveURL: archiveURL) case .fetchMangaArchiveDone(let result): appState.detailInfo.mangaArchiveLoading = false @@ -541,9 +544,9 @@ final class Store: ObservableObject { case .fetchMangaArchiveFunds(let gid): if appState.detailInfo.mangaArchiveFundsLoading { break } appState.detailInfo.mangaArchiveFundsLoading = true - - let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" - appCommand = FetchMangaArchiveFundsCommand(detailURL: detailURL) + // debugMark +// let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" +// appCommand = FetchMangaArchiveFundsCommand(detailURL: detailURL) case .fetchMangaArchiveFundsDone(let result): appState.detailInfo.mangaArchiveFundsLoading = false @@ -561,9 +564,9 @@ final class Store: ObservableObject { if appState.detailInfo.mangaTorrentsLoading { break } appState.detailInfo.mangaTorrentsLoading = true - - let token = appState.cachedList.items?[gid]?.token ?? "" - appCommand = FetchMangaTorrentsCommand(gid: gid, token: token) + // debugMark +// let token = appState.cachedList.items?[gid]?.token ?? "" +// appCommand = FetchMangaTorrentsCommand(gid: gid, token: token) case .fetchMangaTorrentsDone(let result): appState.detailInfo.mangaTorrentsLoading = false @@ -655,9 +658,9 @@ final class Store: ObservableObject { case .fetchAlterImages(let gid): if appState.detailInfo.alterImagesLoading { break } appState.detailInfo.alterImagesLoading = true - - let alterImagesURL = appState.cachedList.items?[gid]?.detail?.alterImagesURL ?? "" - appCommand = FetchAlterImagesCommand(gid: gid, alterImagesURL: alterImagesURL) + // debugMark +// let alterImagesURL = appState.cachedList.items?[gid]?.detail?.alterImagesURL ?? "" +// appCommand = FetchAlterImagesCommand(gid: gid, alterImagesURL: alterImagesURL) case .fetchAlterImagesDone(let result): appState.detailInfo.alterImagesLoading = false @@ -669,9 +672,9 @@ final class Store: ObservableObject { case .updateMangaDetail(let gid): if appState.detailInfo.mangaDetailUpdating { break } appState.detailInfo.mangaDetailUpdating = true - - let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" - appCommand = UpdateMangaDetailCommand(gid: gid, detailURL: detailURL) + // debugMark +// let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" +// appCommand = UpdateMangaDetailCommand(gid: gid, detailURL: detailURL) case .updateMangaDetailDone(let result): appState.detailInfo.mangaDetailUpdating = false @@ -682,9 +685,9 @@ final class Store: ObservableObject { case .updateMangaComments(let gid): if appState.detailInfo.mangaCommentsUpdating { break } appState.detailInfo.mangaCommentsUpdating = true - - let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" - appCommand = UpdateMangaCommentsCommand(gid: gid, detailURL: detailURL) + // debugMark +// let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" +// appCommand = UpdateMangaCommentsCommand(gid: gid, detailURL: detailURL) case .updateMangaCommentsDone(result: let result): appState.detailInfo.mangaCommentsUpdating = false @@ -697,11 +700,11 @@ final class Store: ObservableObject { if appState.contentInfo.mangaContentsLoading { break } appState.contentInfo.mangaContentsLoading = true - - appState.cachedList.items?[gid]?.detail?.currentPageNum = 0 - - let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" - appCommand = FetchMangaContentsCommand(gid: gid, detailURL: detailURL) + // debugMark +// appState.cachedList.items?[gid]?.detail?.currentPageNum = 0 +// +// let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" +// appCommand = FetchMangaContentsCommand(gid: gid, detailURL: detailURL) case .fetchMangaContentsDone(let result): appState.contentInfo.mangaContentsLoading = false @@ -718,38 +721,38 @@ final class Store: ObservableObject { case .fetchMoreMangaContents(let gid): appState.contentInfo.moreMangaContentsLoadFailed = false - - guard let manga = appState.cachedList.items?[gid], - let detail = manga.detail - else { break } - - let currentNum = detail.currentPageNum - let maximumNum = detail.pageNumMaximum - if currentNum + 1 >= maximumNum { break } - - if appState.contentInfo.moreMangaContentsLoading { break } - appState.contentInfo.moreMangaContentsLoading = true - - let detailURL = manga.detailURL - let pageNum = currentNum + 1 - let pageCount = manga.contents?.count ?? 0 - appCommand = FetchMoreMangaContentsCommand( - gid: gid, - detailURL: detailURL, - pageNum: pageNum, - pageCount: pageCount - ) - - if pageCount >= Int(detail.pageCount) ?? 0 { - SwiftyBeaver.error( - "MangaContents overflow", - context: [ - "detailURL": manga.detailURL, - "pageLimit": detail.pageCount, - "pageCurrentAmount": pageCount - ] - ) - } + // debugMark +// guard let manga = appState.cachedList.items?[gid], +// let detail = manga.detail +// else { break } +// +// let currentNum = detail.currentPageNum +// let maximumNum = detail.pageNumMaximum +// if currentNum + 1 >= maximumNum { break } +// +// if appState.contentInfo.moreMangaContentsLoading { break } +// appState.contentInfo.moreMangaContentsLoading = true +// +// let detailURL = manga.detailURL +// let pageNum = currentNum + 1 +// let pageCount = manga.contents?.count ?? 0 +// appCommand = FetchMoreMangaContentsCommand( +// gid: gid, +// detailURL: detailURL, +// pageNum: pageNum, +// pageCount: pageCount +// ) +// +// if pageCount >= Int(detail.pageCount) ?? 0 { +// SwiftyBeaver.error( +// "MangaContents overflow", +// context: [ +// "detailURL": manga.detailURL, +// "pageLimit": detail.pageCount, +// "pageCurrentAmount": pageCount +// ] +// ) +// } case .fetchMoreMangaContentsDone(let result): appState.contentInfo.moreMangaContentsLoading = false @@ -766,8 +769,9 @@ final class Store: ObservableObject { // MARK: Account Ops case .addFavorite(let gid, let favIndex): - let token = appState.cachedList.items?[gid]?.token ?? "" - appCommand = AddFavoriteCommand(gid: gid, token: token, favIndex: favIndex) + break// debugMark +// let token = appState.cachedList.items?[gid]?.token ?? "" +// appCommand = AddFavoriteCommand(gid: gid, token: token, favIndex: favIndex) case .deleteFavorite(let gid): appCommand = DeleteFavoriteCommand(gid: gid) @@ -776,9 +780,9 @@ final class Store: ObservableObject { if appState.detailInfo.downloadCommandSending { break } appState.detailInfo.downloadCommandSending = true - - let archiveURL = appState.cachedList.items?[gid]?.detail?.archiveURL ?? "" - appCommand = SendDownloadCommand(gid: gid, archiveURL: archiveURL, resolution: resolution) + // debugMark +// let archiveURL = appState.cachedList.items?[gid]?.detail?.archiveURL ?? "" +// appCommand = SendDownloadCommand(gid: gid, archiveURL: archiveURL, resolution: resolution) case .sendDownloadCommandDone(let result): appState.detailInfo.downloadCommandSending = false @@ -795,54 +799,58 @@ final class Store: ObservableObject { appState.detailInfo.downloadCommandResponse = result case .rate(let gid, let rating): - guard let apiuidString = appState.settings.user?.apiuid, - let apikey = appState.settings.user?.apikey, - let token = appState.cachedList.items?[gid]?.token, - let apiuid = Int(apiuidString), - let gid = Int(gid) - else { break } - - appState.cachedList.updateUserRating( - gid: String(gid), rating: Float(rating) / 2.0 - ) - - appCommand = RateCommand( - apiuid: apiuid, - apikey: apikey, - gid: gid, - token: token, - rating: rating - ) + break // debugMark +// guard let apiuidString = appState.settings.user?.apiuid, +// let apikey = appState.settings.user?.apikey, +// let token = appState.cachedList.items?[gid]?.token, +// let apiuid = Int(apiuidString), +// let gid = Int(gid) +// else { break } +// +// appState.cachedList.updateUserRating( +// gid: String(gid), rating: Float(rating) / 2.0 +// ) +// +// appCommand = RateCommand( +// apiuid: apiuid, +// apikey: apikey, +// gid: gid, +// token: token, +// rating: rating +// ) case .comment(let gid, let content): - let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" - appCommand = CommentCommand(gid: gid, content: content, detailURL: detailURL) + break // debugMark +// let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" +// appCommand = CommentCommand(gid: gid, content: content, detailURL: detailURL) case .editComment(let gid, let commentID, let content): - let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" - - appCommand = EditCommentCommand( - gid: gid, - commentID: commentID, - content: content, - detailURL: detailURL - ) + break // debugMark +// let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" +// +// appCommand = EditCommentCommand( +// gid: gid, +// commentID: commentID, +// content: content, +// detailURL: detailURL +// ) case .voteComment(let gid, let commentID, let vote): - guard let apiuidString = appState.settings.user?.apiuid, - let apikey = appState.settings.user?.apikey, - let token = appState.cachedList.items?[gid]?.token, - let commentID = Int(commentID), - let apiuid = Int(apiuidString), - let gid = Int(gid) - else { break } - - appCommand = VoteCommentCommand( - apiuid: apiuid, - apikey: apikey, - gid: gid, - token: token, - commentID: commentID, - commentVote: vote - ) + break // debugMark +// guard let apiuidString = appState.settings.user?.apiuid, +// let apikey = appState.settings.user?.apikey, +// let token = appState.cachedList.items?[gid]?.token, +// let commentID = Int(commentID), +// let apiuid = Int(apiuidString), +// let gid = Int(gid) +// else { break } +// +// appCommand = VoteCommentCommand( +// apiuid: apiuid, +// apikey: apikey, +// gid: gid, +// token: token, +// commentID: commentID, +// commentVote: vote +// ) } return (appState, appCommand) diff --git a/EhPanda/Database/MOProtocol.swift b/EhPanda/Database/MOProtocol.swift deleted file mode 100755 index 283fed1a..00000000 --- a/EhPanda/Database/MOProtocol.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// ManagedObjectProtocol.swift -// Created by swifting.io Team -// - -import CoreData -import SwiftyBeaver - -protocol ManagedObjectProtocol { - associatedtype Entity - func toEntity() -> Entity -} -protocol ManagedObjectConvertible { - associatedtype ManagedObject: NSManagedObject, ManagedObjectProtocol - func toManagedObject(in context: NSManagedObjectContext) -> ManagedObject -} - -extension ManagedObjectProtocol where Self: NSManagedObject { - - static func getOrCreateSingle( - with id: String, - and key: String = "gid", - from context: NSManagedObjectContext - ) -> Self { - let result = single( - with: id, - from: context - ) ?? insertNew(in: context) - result.setValue(id, forKey: key) - - return result - } - - static func single( - from context: NSManagedObjectContext, - with predicate: NSPredicate?, - sortDescriptors: [NSSortDescriptor]? - ) -> Self? { - fetch( - from: context, - with: predicate, - sortDescriptors: sortDescriptors, - fetchLimit: 1 - )?.first - } - - static func single( - with id: String, - and key: String = "gid", - from context: NSManagedObjectContext - ) -> Self? { - let predicate = NSPredicate(format: "%@ == %@", key, id) - return single(from: context, with: predicate, sortDescriptors: nil) - } - - static func insertNew(in context: NSManagedObjectContext) -> Self { - Self(context: context) - } - - static func fetch( - from context: NSManagedObjectContext, - with predicate: NSPredicate?, - sortDescriptors: [NSSortDescriptor]?, - fetchLimit: Int? - ) -> [Self]? { - - let fetchRequest = Self.fetchRequest() - fetchRequest.sortDescriptors = sortDescriptors - fetchRequest.predicate = predicate - fetchRequest.returnsObjectsAsFaults = false - - if let fetchLimit = fetchLimit { - fetchRequest.fetchLimit = fetchLimit - } - - var result: [Self]? - context.performAndWait { () -> Void in - do { - result = try context.fetch(fetchRequest) as? [Self] - } catch { - result = nil - SwiftyBeaver.error("CoreData fetch error \(error)") - } - } - return result - } -} diff --git a/EhPanda/Database/MangaDetailMO+CoreDataClass.swift b/EhPanda/Database/MangaDetailMO+CoreDataClass.swift index 230ce5e6..946118c1 100644 --- a/EhPanda/Database/MangaDetailMO+CoreDataClass.swift +++ b/EhPanda/Database/MangaDetailMO+CoreDataClass.swift @@ -14,7 +14,7 @@ extension MangaDetailMO: ManagedObjectProtocol { MangaDetail( isFavored: isFavored, alterImagesURL: nil, alterImages: [], torrents: [], comments: [], - previews: [], title: title, rating: rating, + previews: [], gid: gid, title: title, rating: rating, ratingCount: ratingCount, detailTags: [], category: Category(rawValue: category)!, language: Language(rawValue: language)!, @@ -26,11 +26,13 @@ extension MangaDetailMO: ManagedObjectProtocol { } } extension MangaDetail: ManagedObjectConvertible { + @discardableResult func toManagedObject(in context: NSManagedObjectContext) -> MangaDetailMO { let mangaDetailMO = MangaDetailMO(context: context) // let mangaDetailMO = MangaDetailMO.getOrCreateSingle( // with: "gid", from: context // ) + mangaDetailMO.gid = gid mangaDetailMO.archiveURL = archiveURL mangaDetailMO.category = category.rawValue mangaDetailMO.coverURL = coverURL diff --git a/EhPanda/Database/MangaMO+CoreDataClass.swift b/EhPanda/Database/MangaMO+CoreDataClass.swift index c99586b8..55de8995 100644 --- a/EhPanda/Database/MangaMO+CoreDataClass.swift +++ b/EhPanda/Database/MangaMO+CoreDataClass.swift @@ -23,11 +23,13 @@ extension MangaMO: ManagedObjectProtocol { } } extension Manga: ManagedObjectConvertible { + @discardableResult func toManagedObject(in context: NSManagedObjectContext) -> MangaMO { let mangaMO = MangaMO(context: context) // let mangaMO = MangaMO.getOrCreateSingle( // with: gid, from: context // ) + mangaMO.gid = gid mangaMO.category = category.rawValue mangaMO.coverURL = coverURL mangaMO.detailURL = detailURL diff --git a/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents b/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents index a6920f57..862b58f9 100644 --- a/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,6 +1,6 @@ - - + + @@ -20,7 +20,7 @@ - + @@ -32,10 +32,10 @@ - + - + \ No newline at end of file diff --git a/EhPanda/Database/Persistence.swift b/EhPanda/Database/Persistence.swift index f40a6c79..3809dd3e 100644 --- a/EhPanda/Database/Persistence.swift +++ b/EhPanda/Database/Persistence.swift @@ -14,7 +14,9 @@ struct PersistenceController { let container: NSPersistentCloudKitContainer = { let container = NSPersistentCloudKitContainer(name: "Model") container.loadPersistentStores { - SwiftyBeaver.error($1 as Any) + if let error = $1 { + SwiftyBeaver.error(error as Any) + } } return container }() @@ -55,7 +57,7 @@ struct PersistenceController { // storedMangaMO.tags = item.tags storedMangaMO.language = item.language?.rawValue } else { - _ = item.toManagedObject(in: shared.container.viewContext) + item.toManagedObject(in: shared.container.viewContext) } } saveContext() @@ -77,7 +79,22 @@ struct PersistenceController { storedMangaDetailMO.rating = detail.rating storedMangaDetailMO.ratingCount = detail.ratingCount storedMangaDetailMO.torrentCount = Int64(detail.torrentCount) + } else { + detail.toManagedObject(in: shared.container.viewContext) } saveContext() } } + +// MARK: Protocol Definition +protocol ManagedObjectProtocol { + associatedtype Entity + func toEntity() -> Entity +} + +protocol ManagedObjectConvertible { + associatedtype ManagedObject: NSManagedObject, ManagedObjectProtocol + + @discardableResult + func toManagedObject(in context: NSManagedObjectContext) -> ManagedObject +} diff --git a/EhPanda/Models/Models.swift b/EhPanda/Models/Models.swift index 8478bb17..9bb80c7d 100644 --- a/EhPanda/Models/Models.swift +++ b/EhPanda/Models/Models.swift @@ -68,6 +68,7 @@ struct MangaDetail: Codable { torrents: [], comments: [], previews: [], + gid: "", title: "", rating: 0.0, ratingCount: "", @@ -98,6 +99,7 @@ struct MangaDetail: Codable { var comments: [MangaComment] let previews: [MangaPreview] + let gid: String var title: String var jpnTitle: String? var rating: Float diff --git a/EhPanda/Network/Request.swift b/EhPanda/Network/Request.swift index 90ddd1d6..2fd27cf8 100644 --- a/EhPanda/Network/Request.swift +++ b/EhPanda/Network/Request.swift @@ -104,7 +104,7 @@ struct MangaItemReverseRequest { URLSession.shared .dataTaskPublisher(for: detailURL.safeURL()) .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .compactMap { getManga(from: try? Parser.parseMangaDetail(doc: $0)) } + .compactMap { getManga(from: try? Parser.parseMangaDetail(doc: $0, gid: gid)) } .mapError(mapAppError) .eraseToAnyPublisher() } @@ -274,6 +274,7 @@ struct MoreFavoritesItemsRequest { } struct MangaDetailRequest { + let gid: String let detailURL: String var publisher: AnyPublisher<(MangaDetail, APIKey), AppError> { @@ -286,7 +287,7 @@ struct MangaDetailRequest { .safeURL() ) .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap { try (Parser.parseMangaDetail(doc: $0), Parser.parseAPIKey(doc: $0))} + .tryMap { try (Parser.parseMangaDetail(doc: $0, gid: gid), Parser.parseAPIKey(doc: $0))} .mapError(mapAppError) .eraseToAnyPublisher() } @@ -368,6 +369,7 @@ struct MangaArchiveRequest { } struct MangaArchiveFundsRequest { + let gid: String let detailURL: String var alterDetailURL: String { @@ -388,7 +390,7 @@ struct MangaArchiveFundsRequest { .dataTaskPublisher(for: url.safeURL()) .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .compactMap { try? Parser - .parseMangaDetail(doc: $0) + .parseMangaDetail(doc: $0, gid: gid) .archiveURL } .mapError(mapAppError) diff --git a/EhPanda/View/Content/ContentView.swift b/EhPanda/View/Content/ContentView.swift index d6bae8db..8a329152 100644 --- a/EhPanda/View/Content/ContentView.swift +++ b/EhPanda/View/Content/ContentView.swift @@ -144,10 +144,14 @@ struct ContentView: View, StoreAccessor { private extension ContentView { // MARK: Properties var mangaDetail: MangaDetail? { - cachedList.items?[gid]?.detail + nil + // debugMark +// cachedList.items?[gid]?.detail } var mangaContents: [MangaContent]? { - cachedList.items?[gid]?.contents + [] + // debugMark +// cachedList.items?[gid]?.contents } var moreLoadingFlag: Bool { contentInfo.moreMangaContentsLoading diff --git a/EhPanda/View/Detail/ArchiveView.swift b/EhPanda/View/Detail/ArchiveView.swift index 37f071a6..8d9a7d66 100644 --- a/EhPanda/View/Detail/ArchiveView.swift +++ b/EhPanda/View/Detail/ArchiveView.swift @@ -101,7 +101,9 @@ private extension ArchiveView { $store.appState.detailInfo } var mangaDetail: MangaDetail? { - cachedList.items?[gid]?.detail + nil + // debugMark +// cachedList.items?[gid]?.detail } var archive: MangaArchive? { mangaDetail?.archive @@ -331,7 +333,8 @@ struct ArchiveView_Previews: PreviewProvider { manga.detail?.archive = archive store.appState.settings.user = user store.appState.environment.isPreview = true - store.appState.cachedList.items?["mangaForTest"] = manga + // debugMark +// store.appState.cachedList.items?["mangaForTest"] = manga return ArchiveView(gid: "mangaForTest") .environmentObject(store) diff --git a/EhPanda/View/Detail/CommentView.swift b/EhPanda/View/Detail/CommentView.swift index 70989bbe..f3a37c3e 100644 --- a/EhPanda/View/Detail/CommentView.swift +++ b/EhPanda/View/Detail/CommentView.swift @@ -122,7 +122,9 @@ struct CommentView: View, StoreAccessor { // MARK: Private Extension private extension CommentView { var comments: [MangaComment] { - store.appState.cachedList.items?[gid]?.detail?.comments ?? [] + [] + // debugMark +// store.appState.cachedList.items?[gid]?.detail?.comments ?? [] } var environmentBinding: Binding { diff --git a/EhPanda/View/Detail/DetailView.swift b/EhPanda/View/Detail/DetailView.swift index 6b049c51..d3d9ed4d 100644 --- a/EhPanda/View/Detail/DetailView.swift +++ b/EhPanda/View/Detail/DetailView.swift @@ -90,7 +90,7 @@ struct DetailView: View, StoreAccessor { case .archive: ArchiveView(gid: gid) case .torrents: - TorrentsView(gid: gid) + TorrentsView(gid: gid, token: manga.token) case .comment: DraftCommentView( content: commentContentBinding, @@ -153,10 +153,16 @@ private extension DetailView { detailInfoBinding.commentContent } var manga: Manga { - cachedList.items?[gid] ?? Manga.empty + let mangaMO: MangaMO? = PersistenceController.fetch( + entityName: "MangaMO", gid: gid + ) + return mangaMO?.toEntity() ?? Manga.empty } var mangaDetail: MangaDetail? { - cachedList.items?[gid]?.detail + let mangaDetailMO: MangaDetailMO? = PersistenceController.fetch( + entityName: "MangaDetailMO", gid: gid + ) + return mangaDetailMO?.toEntity() } } @@ -226,7 +232,7 @@ private extension DetailView { store.dispatch(.updateViewControllersCount) } func fetchMangaDetail() { - store.dispatch(.fetchMangaDetail(gid: gid)) + store.dispatch(.fetchMangaDetail(gid: gid, detailURL: manga.detailURL)) } func updateMangaDetail() { store.dispatch(.updateMangaDetail(gid: gid)) diff --git a/EhPanda/View/Detail/TorrentsView.swift b/EhPanda/View/Detail/TorrentsView.swift index 711544bd..3157c1c4 100644 --- a/EhPanda/View/Detail/TorrentsView.swift +++ b/EhPanda/View/Detail/TorrentsView.swift @@ -15,10 +15,15 @@ struct TorrentsView: View, StoreAccessor { @State private var hudVisible = false @State private var hudConfig = TTProgressHUDConfig() - private var gid: String + @State private var loadingFlag = false + @State private var torrents = [MangaTorrent]() - init(gid: String) { + private let gid: String + private let token: String + + init(gid: String, token: String) { self.gid = gid + self.token = token } var body: some View { @@ -34,7 +39,7 @@ struct TorrentsView: View, StoreAccessor { } TTProgressHUD($hudVisible, config: hudConfig) } - } else if detailInfo.mangaTorrentsLoading { + } else if loadingFlag { LoadingView() } else { NetworkErrorView(retryAction: fetchMangaTorrents) @@ -47,13 +52,6 @@ struct TorrentsView: View, StoreAccessor { } private extension TorrentsView { - var mangaDetail: MangaDetail? { - cachedList.items?[gid]?.detail - } - var torrents: [MangaTorrent] { - mangaDetail?.torrents ?? [] - } - func onTorrentRowTap(magnet: String) { saveToPasteboard(value: magnet) showCopiedHUD() @@ -71,9 +69,21 @@ private extension TorrentsView { } func fetchMangaTorrents() { - DispatchQueue.main.async { - store.dispatch(.fetchMangaTorrents(gid: gid)) - } + if loadingFlag { return } + loadingFlag = true + + let sToken = SubscriptionToken() + MangaTorrentsRequest(gid: gid, token: token) + .publisher + .receive(on: DispatchQueue.main) + .sink { _ in + loadingFlag = false + sToken.unseal() + } receiveValue: { + torrents = $0 + loadingFlag = false + } + .seal(in: sToken) } } From 852f4106b62f182ff9679182550d5b2c2fd3c219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=A4=E3=81=9D=E3=82=99=E3=81=86?= Date: Mon, 5 Jul 2021 11:39:11 +0800 Subject: [PATCH 3/9] feat: Restore archive function --- EhPanda.xcodeproj/project.pbxproj | 4 + EhPanda/App/Utility.swift | 8 + EhPanda/DataFlow/AppAction.swift | 7 - EhPanda/DataFlow/AppCommand.swift | 81 ---------- EhPanda/DataFlow/AppState.swift | 31 ---- EhPanda/DataFlow/Store.swift | 74 --------- .../MangaDetailMO+CoreDataClass.swift | 8 +- EhPanda/Database/PersistenceAccessor.swift | 25 +++ EhPanda/View/Detail/ArchiveView.swift | 145 +++++++++++------- EhPanda/View/Detail/DetailView.swift | 19 +-- EhPanda/View/Detail/TorrentsView.swift | 2 +- EhPanda/View/Home/HomeView.swift | 12 +- 12 files changed, 140 insertions(+), 276 deletions(-) create mode 100644 EhPanda/Database/PersistenceAccessor.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 1ce0b954..9368c7ae 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ ABCA93BE26918DE100A98BC6 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCA93BD26918DE100A98BC6 /* Persistence.swift */; }; ABCA93C02691925900A98BC6 /* MangaMO+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCA93BF2691925900A98BC6 /* MangaMO+CoreDataClass.swift */; }; ABCA93C22691929D00A98BC6 /* MangaDetailMO+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCA93C12691929D00A98BC6 /* MangaDetailMO+CoreDataClass.swift */; }; + ABCA93C42692A0BF00A98BC6 /* PersistenceAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCA93C32692A0BF00A98BC6 /* PersistenceAccessor.swift */; }; ABCD2F0A259763FC008E5A20 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD2F09259763FC008E5A20 /* Request.swift */; }; ABCD2F0E25976B95008E5A20 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD2F0D25976B95008E5A20 /* Parser.swift */; }; ABD5FDD4263D05110021A4C6 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = ABD5FDD3263D05110021A4C6 /* .swiftlint.yml */; }; @@ -119,6 +120,7 @@ ABCA93BD26918DE100A98BC6 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; ABCA93BF2691925900A98BC6 /* MangaMO+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaMO+CoreDataClass.swift"; sourceTree = ""; }; ABCA93C12691929D00A98BC6 /* MangaDetailMO+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaDetailMO+CoreDataClass.swift"; sourceTree = ""; }; + ABCA93C32692A0BF00A98BC6 /* PersistenceAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceAccessor.swift; sourceTree = ""; }; ABCD2F09259763FC008E5A20 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; ABCD2F0D25976B95008E5A20 /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; ABD5FDD3263D05110021A4C6 /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = SOURCE_ROOT; }; @@ -211,6 +213,7 @@ children = ( ABC681F126898D46007BBD69 /* Model.xcdatamodeld */, ABCA93BD26918DE100A98BC6 /* Persistence.swift */, + ABCA93C32692A0BF00A98BC6 /* PersistenceAccessor.swift */, ABCA93BF2691925900A98BC6 /* MangaMO+CoreDataClass.swift */, AB2CED63268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift */, ABCA93C12691929D00A98BC6 /* MangaDetailMO+CoreDataClass.swift */, @@ -514,6 +517,7 @@ AB40CFDF25983EDF00D1DC9A /* FileHelper.swift in Sources */, ABC3C78F2593699B00E0C11B /* ViewModifiers.swift in Sources */, ABF45AF825F3313D00ECB568 /* EhPandaView.swift in Sources */, + ABCA93C42692A0BF00A98BC6 /* PersistenceAccessor.swift in Sources */, ABC3C7862593699B00E0C11B /* Utility.swift in Sources */, ABF45AE725F3313D00ECB568 /* RatingView.swift in Sources */, AB2CED64268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift in Sources */, diff --git a/EhPanda/App/Utility.swift b/EhPanda/App/Utility.swift index 60e50113..673ab0b3 100644 --- a/EhPanda/App/Utility.swift +++ b/EhPanda/App/Utility.swift @@ -253,6 +253,14 @@ func dispatchMainSync(execute work: () -> Void) { } } +func dispatchMainAsync(execute work: @escaping () -> Void) { + if Thread.isMainThread { + work() + } else { + DispatchQueue.main.async(execute: work) + } +} + func presentActivityVC(items: [Any]) { let activityVC = UIActivityViewController( activityItems: items, diff --git a/EhPanda/DataFlow/AppAction.swift b/EhPanda/DataFlow/AppAction.swift index 4a591eeb..a79a4e4a 100644 --- a/EhPanda/DataFlow/AppAction.swift +++ b/EhPanda/DataFlow/AppAction.swift @@ -26,7 +26,6 @@ enum AppAction { case clearHistoryKeywords case updateSearchKeyword(text: String) case updateViewControllersCount - case resetDownloadCommandResponse case replaceMangaCommentJumpID(gid: String?) case updateIsSlideMenuClosed(isClosed: Bool) @@ -70,12 +69,8 @@ enum AppAction { case fetchMoreFavoritesItemsDone(carriedValue: FavoritesIndex, result: Result<(PageNumber, [Manga]), AppError>) case fetchMangaDetail(gid: String, detailURL: String) case fetchMangaDetailDone(result: Result<(Identity, MangaDetail, APIKey), AppError>) - case fetchMangaArchive(gid: String) - case fetchMangaArchiveDone(result: Result<(Identity, MangaArchive, CurrentGP?, CurrentCredits?), AppError>) case fetchMangaArchiveFunds(gid: String) case fetchMangaArchiveFundsDone(result: Result<((CurrentGP, CurrentCredits)), AppError>) - case fetchMangaTorrents(gid: String) - case fetchMangaTorrentsDone(result: Result<(Identity, [MangaTorrent]), AppError>) case fetchAssociatedItems(depth: Int, keyword: AssociatedKeyword) case fetchAssociatedItemsDone(result: Result<(Depth, AssociatedKeyword, PageNumber, [Manga]), AppError>) case fetchMoreAssociatedItems(depth: Int, keyword: AssociatedKeyword) @@ -93,8 +88,6 @@ enum AppAction { case addFavorite(gid: String, favIndex: Int) case deleteFavorite(gid: String) - case sendDownloadCommand(gid: String, resolution: String) - case sendDownloadCommandDone(result: Resp?) case rate(gid: String, rating: Int) case comment(gid: String, content: String) case editComment(gid: String, commentID: String, content: String) diff --git a/EhPanda/DataFlow/AppCommand.swift b/EhPanda/DataFlow/AppCommand.swift index 7b9f66bc..b39f1ae4 100644 --- a/EhPanda/DataFlow/AppCommand.swift +++ b/EhPanda/DataFlow/AppCommand.swift @@ -310,36 +310,6 @@ struct FetchMangaDetailCommand: AppCommand { } } -struct FetchMangaArchiveCommand: AppCommand { - let gid: String - let archiveURL: String - - func execute(in store: Store) { - let sToken = SubscriptionToken() - MangaArchiveRequest(archiveURL: archiveURL) - .publisher - .receive(on: DispatchQueue.main) - .sink { completion in - if case .failure(let error) = completion { - store.dispatch(.fetchMangaArchiveDone(result: .failure(error))) - } - sToken.unseal() - } receiveValue: { archive in - if let arc = archive.0 { - store.dispatch(.fetchMangaArchiveDone(result: .success((gid, arc, archive.1, archive.2)))) - if archive.1 == nil - || archive.2 == nil - { - store.dispatch(.fetchMangaArchiveFunds(gid: gid)) - } - } else { - store.dispatch(.fetchMangaArchiveDone(result: .failure(.networkingFailed))) - } - } - .seal(in: sToken) - } -} - struct FetchMangaArchiveFundsCommand: AppCommand { let gid: String let detailURL: String @@ -365,31 +335,6 @@ struct FetchMangaArchiveFundsCommand: AppCommand { } } -struct FetchMangaTorrentsCommand: AppCommand { - let gid: String - let token: String - - func execute(in store: Store) { - let sToken = SubscriptionToken() - MangaTorrentsRequest(gid: gid, token: token) - .publisher - .receive(on: DispatchQueue.main) - .sink { completion in - if case .failure(let error) = completion { - store.dispatch(.fetchMangaTorrentsDone(result: .failure(error))) - } - sToken.unseal() - } receiveValue: { torrents in - if !torrents.isEmpty { - store.dispatch(.fetchMangaTorrentsDone(result: .success((gid, torrents)))) - } else { - store.dispatch(.fetchMangaTorrentsDone(result: .failure(.networkingFailed))) - } - } - .seal(in: sToken) - } -} - struct FetchAssociatedItemsCommand: AppCommand { let depth: Int let keyword: AssociatedKeyword @@ -603,32 +548,6 @@ struct DeleteFavoriteCommand: AppCommand { } } -struct SendDownloadCommand: AppCommand { - let gid: String - let archiveURL: String - let resolution: String - - func execute(in store: Store) { - let token = SubscriptionToken() - SendDownloadCommandRequest( - archiveURL: archiveURL, - resolution: resolution - ) - .publisher - .receive(on: DispatchQueue.main) - .sink { completion in - if case .failure = completion { - store.dispatch(.sendDownloadCommandDone(result: nil)) - } - token.unseal() - } receiveValue: { resp in - store.dispatch(.sendDownloadCommandDone(result: resp)) - store.dispatch(.fetchMangaArchiveFunds(gid: gid)) - } - .seal(in: token) - } -} - struct RateCommand: AppCommand { let apiuid: Int let apikey: String diff --git a/EhPanda/DataFlow/AppState.swift b/EhPanda/DataFlow/AppState.swift index 23b3aa73..d3ebd236 100644 --- a/EhPanda/DataFlow/AppState.swift +++ b/EhPanda/DataFlow/AppState.swift @@ -230,17 +230,8 @@ extension AppState { var mangaDetailLoading = false var mangaDetailLoadFailed = false - var mangaArchiveLoading = false - var mangaArchiveLoadFailed = false var mangaArchiveFundsLoading = false - var downloadCommandResponse: String? - var downloadCommandSending = false - var downloadCommandFailed = false - - var mangaTorrentsLoading = false - var mangaTorrentsLoadFailed = false - var associatedItems: [AssociatedItem] = [] var associatedItemsLoading = false var associatedItemsNotFound = false @@ -315,32 +306,10 @@ extension AppState { } mutating func cache(mangas: [Manga]) { PersistenceController.add(items: mangas) -// if items == nil { -// items = Dictionary(uniqueKeysWithValues: mangas.map { ($0.id, $0) }) -// return -// } -// -// for manga in mangas { -// if items?[manga.gid] == nil { -// items?[manga.gid] = manga -// } else { -// items?[manga.gid]?.title = manga.title -// items?[manga.gid]?.rating = manga.rating -// items?[manga.gid]?.tags = manga.tags -// items?[manga.gid]?.language = manga.language -// } -// } } mutating func insertDetail(gid: String, detail: MangaDetail) { PersistenceController.update(gid: gid, detail: detail) -// items?[gid]?.detail = detail - } - mutating func insertArchive(gid: String, archive: MangaArchive) { -// items?[gid]?.detail?.archive = archive - } - mutating func insertTorrents(gid: String, torrents: [MangaTorrent]) { -// items?[gid]?.detail?.torrents = torrents } mutating func updateDetail(gid: String, detail: MangaDetail) { // items?[gid]?.detail?.isFavored = detail.isFavored diff --git a/EhPanda/DataFlow/Store.swift b/EhPanda/DataFlow/Store.swift index 1fdf0994..1f04bf86 100644 --- a/EhPanda/DataFlow/Store.swift +++ b/EhPanda/DataFlow/Store.swift @@ -84,10 +84,6 @@ final class Store: ObservableObject { appState.homeInfo.searchKeyword = text case .updateViewControllersCount: appState.environment.viewControllersCount = viewControllersCount - case .resetDownloadCommandResponse: - appState.detailInfo.downloadCommandResponse = nil - appState.detailInfo.downloadCommandSending = false - appState.detailInfo.downloadCommandFailed = false case .replaceMangaCommentJumpID(let gid): appState.environment.mangaItemReverseID = gid case .updateIsSlideMenuClosed(let isClosed): @@ -512,35 +508,6 @@ final class Store: ObservableObject { appState.detailInfo.mangaDetailLoadFailed = true } - case .fetchMangaArchive(let gid): - appState.detailInfo.mangaArchiveLoadFailed = false - - if appState.detailInfo.mangaArchiveLoading { break } - appState.detailInfo.mangaArchiveLoading = true - // debugMark -// let archiveURL = appState.cachedList.items?[gid]?.detail?.archiveURL ?? "" -// appCommand = FetchMangaArchiveCommand(gid: gid, archiveURL: archiveURL) - case .fetchMangaArchiveDone(let result): - appState.detailInfo.mangaArchiveLoading = false - - switch result { - case .success(let archive): - appState.cachedList.insertArchive(gid: archive.0, archive: archive.1) - - if let currentGP = archive.2, - let currentCredits = archive.3 - { - appState.settings.update( - user: User( - currentGP: currentGP, - currentCredits: currentCredits - ) - ) - } - case .failure: - appState.detailInfo.mangaArchiveLoadFailed = true - } - case .fetchMangaArchiveFunds(let gid): if appState.detailInfo.mangaArchiveFundsLoading { break } appState.detailInfo.mangaArchiveFundsLoading = true @@ -559,24 +526,6 @@ final class Store: ObservableObject { ) } - case .fetchMangaTorrents(let gid): - appState.detailInfo.mangaTorrentsLoadFailed = false - - if appState.detailInfo.mangaTorrentsLoading { break } - appState.detailInfo.mangaTorrentsLoading = true - // debugMark -// let token = appState.cachedList.items?[gid]?.token ?? "" -// appCommand = FetchMangaTorrentsCommand(gid: gid, token: token) - case .fetchMangaTorrentsDone(let result): - appState.detailInfo.mangaTorrentsLoading = false - - switch result { - case .success(let torrents): - appState.cachedList.insertTorrents(gid: torrents.0, torrents: torrents.1) - case .failure: - appState.detailInfo.mangaTorrentsLoadFailed = true - } - case .fetchAssociatedItems(let depth, let keyword): appState.detailInfo.associatedItemsNotFound = false appState.detailInfo.associatedItemsLoadFailed = false @@ -775,29 +724,6 @@ final class Store: ObservableObject { case .deleteFavorite(let gid): appCommand = DeleteFavoriteCommand(gid: gid) - case .sendDownloadCommand(let gid, let resolution): - appState.detailInfo.downloadCommandFailed = false - - if appState.detailInfo.downloadCommandSending { break } - appState.detailInfo.downloadCommandSending = true - // debugMark -// let archiveURL = appState.cachedList.items?[gid]?.detail?.archiveURL ?? "" -// appCommand = SendDownloadCommand(gid: gid, archiveURL: archiveURL, resolution: resolution) - case .sendDownloadCommandDone(let result): - appState.detailInfo.downloadCommandSending = false - - switch result { - case Defaults.Response.hathClientNotFound, - Defaults.Response.hathClientNotOnline, - Defaults.Response.invalidResolution, - .none: - appState.detailInfo.downloadCommandFailed = true - default: - break - } - - appState.detailInfo.downloadCommandResponse = result - case .rate(let gid, let rating): break // debugMark // guard let apiuidString = appState.settings.user?.apiuid, diff --git a/EhPanda/Database/MangaDetailMO+CoreDataClass.swift b/EhPanda/Database/MangaDetailMO+CoreDataClass.swift index 946118c1..0988ef51 100644 --- a/EhPanda/Database/MangaDetailMO+CoreDataClass.swift +++ b/EhPanda/Database/MangaDetailMO+CoreDataClass.swift @@ -12,10 +12,10 @@ public class MangaDetailMO: NSManagedObject {} extension MangaDetailMO: ManagedObjectProtocol { func toEntity() -> MangaDetail { MangaDetail( - isFavored: isFavored, alterImagesURL: nil, - alterImages: [], torrents: [], comments: [], - previews: [], gid: gid, title: title, rating: rating, - ratingCount: ratingCount, detailTags: [], + isFavored: isFavored, archiveURL: archiveURL, + alterImagesURL: nil, alterImages: [], torrents: [], + comments: [], previews: [], gid: gid, title: title, + rating: rating, ratingCount: ratingCount, detailTags: [], category: Category(rawValue: category)!, language: Language(rawValue: language)!, uploader: uploader, publishedDate: publishedDate, diff --git a/EhPanda/Database/PersistenceAccessor.swift b/EhPanda/Database/PersistenceAccessor.swift new file mode 100644 index 00000000..771273d8 --- /dev/null +++ b/EhPanda/Database/PersistenceAccessor.swift @@ -0,0 +1,25 @@ +// +// PersistenceAccessor.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/07/05. +// + +protocol PersistenceAccessor { + var gid: String { get } +} + +extension PersistenceAccessor { + var manga: Manga { + let mangaMO: MangaMO? = PersistenceController.fetch( + entityName: "MangaMO", gid: gid + ) + return mangaMO?.toEntity() ?? Manga.empty + } + var mangaDetail: MangaDetail? { + let mangaDetailMO: MangaDetailMO? = PersistenceController.fetch( + entityName: "MangaDetailMO", gid: gid + ) + return mangaDetailMO?.toEntity() + } +} diff --git a/EhPanda/View/Detail/ArchiveView.swift b/EhPanda/View/Detail/ArchiveView.swift index 8d9a7d66..0ebf7f22 100644 --- a/EhPanda/View/Detail/ArchiveView.swift +++ b/EhPanda/View/Detail/ArchiveView.swift @@ -9,10 +9,16 @@ import SwiftUI import SwiftyBeaver import TTProgressHUD -struct ArchiveView: View, StoreAccessor { +struct ArchiveView: View, StoreAccessor, PersistenceAccessor { @EnvironmentObject var store: Store @State private var selection: ArchiveRes? + @State private var archive: MangaArchive? + @State private var response: String? + @State private var loadingFlag = false + @State private var sendingFlag = false + @State private var sendFailedFlag = false + @State private var hudVisible = false @State private var hudConfig = TTProgressHUDConfig() private var loadingHUDConfig = TTProgressHUDConfig( @@ -23,7 +29,7 @@ struct ArchiveView: View, StoreAccessor { GridItem(.adaptive(minimum: 150, maximum: 200)) ] - private let gid: String + let gid: String init(gid: String) { self.gid = gid @@ -70,26 +76,18 @@ struct ArchiveView: View, StoreAccessor { } .padding(.horizontal) TTProgressHUD( - detailInfoBinding.downloadCommandSending, + $sendingFlag, config: loadingHUDConfig ) TTProgressHUD($hudVisible, config: hudConfig) } - } else if detailInfo.mangaArchiveLoading { + } else if loadingFlag { LoadingView() } else { NetworkErrorView(retryAction: fetchMangaArchive) } } .navigationBarTitle("Archive") - .onChange( - of: detailInfo.downloadCommandSending, - perform: onRespChange - ) - .onChange( - of: hudVisible, - perform: onHUDVisibilityChange - ) } .task(fetchMangaArchive) } @@ -100,14 +98,6 @@ private extension ArchiveView { var detailInfoBinding: Binding { $store.appState.detailInfo } - var mangaDetail: MangaDetail? { - nil - // debugMark -// cachedList.items?[gid]?.detail - } - var archive: MangaArchive? { - mangaDetail?.archive - } var hathArchives: [MangaArchive.HathArchive] { archive?.hathArchives ?? [] } @@ -120,50 +110,93 @@ private extension ArchiveView { } } func onDownloadButtonTap() { - if let res = selection?.param { - store.dispatch(.sendDownloadCommand(gid: gid, resolution: res)) - impactFeedback(style: .soft) - } + fetchDownloadResponse() + impactFeedback(style: .soft) } - func onRespChange(value: E) { - if let sending = value as? Bool, sending == false { - let isSuccess = !detailInfo.downloadCommandFailed - let type: TTProgressHUDType = isSuccess ? .success : .error - let title = (isSuccess ? "Success" : "Error").localized() - let caption = detailInfo.downloadCommandResponse?.localized() - - switch type { - case .success: - notificFeedback(style: .success) - case .error: - notificFeedback(style: .error) - default: - SwiftyBeaver.verbose(type) - } + func performHUD() { + let isSuccess = !sendFailedFlag + let type: TTProgressHUDType = isSuccess ? .success : .error + let title = (isSuccess ? "Success" : "Error").localized() + let caption = response?.localized() - hudConfig = TTProgressHUDConfig( - type: type, - title: title, - caption: caption, - shouldAutoHide: true, - autoHideInterval: 2 - ) - hudVisible.toggle() - } - } - func onHUDVisibilityChange(value: E) { - if let isVisible = value as? Bool, isVisible == false { - store.dispatch(.resetDownloadCommandResponse) + switch type { + case .success: + notificFeedback(style: .success) + case .error: + notificFeedback(style: .error) + default: + break } + + hudConfig = TTProgressHUDConfig( + type: type, + title: title, + caption: caption, + shouldAutoHide: true, + autoHideInterval: 2 + ) + hudVisible = true } + // MARK: Networking func fetchMangaArchive() { - DispatchQueue.main.async { - store.dispatch(.fetchMangaArchive(gid: gid)) - if currentGP == nil || currentCredits == nil, isSameAccount { - store.dispatch(.fetchMangaArchiveFunds(gid: gid)) + guard let archiveURL = mangaDetail?.archiveURL, !loadingFlag + else { return } + loadingFlag = true + + let token = SubscriptionToken() + MangaArchiveRequest(archiveURL: archiveURL) + .publisher + .receive(on: DispatchQueue.main) + .sink { _ in + loadingFlag = false + token.unseal() + } receiveValue: { arc in + archive = arc.0 + if let galleryPoints = arc.1, let credits = arc.2 { + store.dispatch(.fetchMangaArchiveFundsDone( + result: .success((galleryPoints, credits))) + ) + } else if isSameAccount { + store.dispatch(.fetchMangaArchiveFunds(gid: gid)) + } + } + .seal(in: token) + } + func fetchDownloadResponse() { + sendFailedFlag = false + guard let archiveURL = mangaDetail?.archiveURL, + let resolution = selection, !sendingFlag + else { return } + sendingFlag = true + + let token = SubscriptionToken() + SendDownloadCommandRequest( + archiveURL: archiveURL, + resolution: resolution.param + ) + .publisher + .receive(on: DispatchQueue.main) + .sink { completion in + if case .failure = completion { + sendFailedFlag = true + } + sendingFlag = false + performHUD() + token.unseal() + } receiveValue: { resp in + switch resp { + case Defaults.Response.hathClientNotFound, + Defaults.Response.hathClientNotOnline, + Defaults.Response.invalidResolution, .none: + sendFailedFlag = true + default: + break } + response = resp + store.dispatch(.fetchMangaArchiveFunds(gid: gid)) } + .seal(in: token) } } diff --git a/EhPanda/View/Detail/DetailView.swift b/EhPanda/View/Detail/DetailView.swift index d3d9ed4d..2764840e 100644 --- a/EhPanda/View/Detail/DetailView.swift +++ b/EhPanda/View/Detail/DetailView.swift @@ -8,14 +8,14 @@ import SwiftUI import Kingfisher -struct DetailView: View, StoreAccessor { +struct DetailView: View, StoreAccessor, PersistenceAccessor { @EnvironmentObject var store: Store @Environment(\.colorScheme) private var colorScheme @State private var associatedKeyword = AssociatedKeyword() @State private var isNavLinkActive = false - private let gid: String + let gid: String private let depth: Int init(gid: String, depth: Int) { @@ -152,18 +152,6 @@ private extension DetailView { var commentContentBinding: Binding { detailInfoBinding.commentContent } - var manga: Manga { - let mangaMO: MangaMO? = PersistenceController.fetch( - entityName: "MangaMO", gid: gid - ) - return mangaMO?.toEntity() ?? Manga.empty - } - var mangaDetail: MangaDetail? { - let mangaDetailMO: MangaDetailMO? = PersistenceController.fetch( - entityName: "MangaDetailMO", gid: gid - ) - return mangaDetailMO?.toEntity() - } } // MARK: Private Methods @@ -237,9 +225,6 @@ private extension DetailView { func updateMangaDetail() { store.dispatch(.updateMangaDetail(gid: gid)) } - func fetchMangaTorrents() { - store.dispatch(.fetchMangaTorrents(gid: gid)) - } func updateHistoryItems() { DispatchQueue.main.async { if environment.homeListType != .history { diff --git a/EhPanda/View/Detail/TorrentsView.swift b/EhPanda/View/Detail/TorrentsView.swift index 3157c1c4..f847053f 100644 --- a/EhPanda/View/Detail/TorrentsView.swift +++ b/EhPanda/View/Detail/TorrentsView.swift @@ -68,6 +68,7 @@ private extension TorrentsView { hudVisible.toggle() } + // MARK: Networking func fetchMangaTorrents() { if loadingFlag { return } loadingFlag = true @@ -81,7 +82,6 @@ private extension TorrentsView { sToken.unseal() } receiveValue: { torrents = $0 - loadingFlag = false } .seal(in: sToken) } diff --git a/EhPanda/View/Home/HomeView.swift b/EhPanda/View/Home/HomeView.swift index dd30e91e..168e511d 100644 --- a/EhPanda/View/Home/HomeView.swift +++ b/EhPanda/View/Home/HomeView.swift @@ -441,13 +441,15 @@ private extension HomeView { return false } - if setting?.showNewDawnGreeting == true { - if let greeting = user?.greeting { - if verifyDate(with: greeting.updateTime) { + dispatchMainAsync { + if setting?.showNewDawnGreeting == true { + if let greeting = user?.greeting { + if verifyDate(with: greeting.updateTime) { + store.dispatch(.fetchGreeting) + } + } else { store.dispatch(.fetchGreeting) } - } else { - store.dispatch(.fetchGreeting) } } } From c3c344b12d54bbf1d701050e040e5d430fe5b06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=A4=E3=81=9D=E3=82=99=E3=81=86?= Date: Mon, 5 Jul 2021 14:25:52 +0800 Subject: [PATCH 4/9] feat: Restore detail update & all account associated function --- EhPanda/App/Tools/Parser.swift | 2 +- EhPanda/DataFlow/AppAction.swift | 4 +- EhPanda/DataFlow/AppCommand.swift | 4 +- EhPanda/DataFlow/AppState.swift | 21 +--- EhPanda/DataFlow/Store.swift | 122 ++++++++++----------- EhPanda/Database/Persistence.swift | 38 +++++-- EhPanda/Database/PersistenceAccessor.swift | 10 +- EhPanda/Models/Misc.swift | 1 - EhPanda/Network/Request.swift | 2 +- EhPanda/View/Detail/ArchiveView.swift | 3 +- 10 files changed, 98 insertions(+), 109 deletions(-) diff --git a/EhPanda/App/Tools/Parser.swift b/EhPanda/App/Tools/Parser.swift index 947d55a6..f082d9cd 100644 --- a/EhPanda/App/Tools/Parser.swift +++ b/EhPanda/App/Tools/Parser.swift @@ -857,7 +857,7 @@ extension Parser { } // MARK: DownloadCmdResp - static func parseDownloadCommandResponse(doc: HTMLDocument) throws -> Resp { + static func parseDownloadCommandResponse(doc: HTMLDocument) throws -> String { guard let dbNode = doc.at_xpath("//div [@id='db']") else { throw AppError.parseFailed } diff --git a/EhPanda/DataFlow/AppAction.swift b/EhPanda/DataFlow/AppAction.swift index a79a4e4a..7021f68d 100644 --- a/EhPanda/DataFlow/AppAction.swift +++ b/EhPanda/DataFlow/AppAction.swift @@ -68,7 +68,7 @@ enum AppAction { case fetchMoreFavoritesItems(index: Int) case fetchMoreFavoritesItemsDone(carriedValue: FavoritesIndex, result: Result<(PageNumber, [Manga]), AppError>) case fetchMangaDetail(gid: String, detailURL: String) - case fetchMangaDetailDone(result: Result<(Identity, MangaDetail, APIKey), AppError>) + case fetchMangaDetailDone(result: Result<(MangaDetail, APIKey), AppError>) case fetchMangaArchiveFunds(gid: String) case fetchMangaArchiveFundsDone(result: Result<((CurrentGP, CurrentCredits)), AppError>) case fetchAssociatedItems(depth: Int, keyword: AssociatedKeyword) @@ -80,7 +80,7 @@ enum AppAction { case updateMangaComments(gid: String) case updateMangaCommentsDone(result: Result<(Identity, [MangaComment]), AppError>) case updateMangaDetail(gid: String) - case updateMangaDetailDone(result: Result<(Identity, MangaDetail), AppError>) + case updateMangaDetailDone(result: Result) case fetchMangaContents(gid: String) case fetchMangaContentsDone(result: Result<(Identity, PageNumber, [MangaContent]), AppError>) case fetchMoreMangaContents(gid: String) diff --git a/EhPanda/DataFlow/AppCommand.swift b/EhPanda/DataFlow/AppCommand.swift index b39f1ae4..0c17d445 100644 --- a/EhPanda/DataFlow/AppCommand.swift +++ b/EhPanda/DataFlow/AppCommand.swift @@ -301,7 +301,7 @@ struct FetchMangaDetailCommand: AppCommand { } token.unseal() } receiveValue: { detail in - store.dispatch(.fetchMangaDetailDone(result: .success((gid, detail.0, detail.1)))) + store.dispatch(.fetchMangaDetailDone(result: .success((detail.0, detail.1)))) if detail.0.previews.isEmpty == true { store.dispatch(.fetchAlterImages(gid: gid)) } @@ -415,7 +415,7 @@ struct UpdateMangaDetailCommand: AppCommand { } token.unseal() } receiveValue: { detail in - store.dispatch(.updateMangaDetailDone(result: .success((gid, detail.0)))) + store.dispatch(.updateMangaDetailDone(result: .success(detail.0))) } .seal(in: token) } diff --git a/EhPanda/DataFlow/AppState.swift b/EhPanda/DataFlow/AppState.swift index d3ebd236..71cf0c06 100644 --- a/EhPanda/DataFlow/AppState.swift +++ b/EhPanda/DataFlow/AppState.swift @@ -305,25 +305,10 @@ extension AppState { PersistenceController.checkExistence(entityName: "MangaMO", gid: gid) } mutating func cache(mangas: [Manga]) { - PersistenceController.add(items: mangas) + PersistenceController.add(mangas: mangas) } - - mutating func insertDetail(gid: String, detail: MangaDetail) { - PersistenceController.update(gid: gid, detail: detail) - } - mutating func updateDetail(gid: String, detail: MangaDetail) { -// items?[gid]?.detail?.isFavored = detail.isFavored -// items?[gid]?.detail?.archiveURL = detail.archiveURL -// items?[gid]?.detail?.detailTags = detail.detailTags -// items?[gid]?.detail?.comments = detail.comments -// items?[gid]?.detail?.jpnTitle = detail.jpnTitle -// items?[gid]?.detail?.likeCount = detail.likeCount -// items?[gid]?.detail?.pageCount = detail.pageCount -// items?[gid]?.detail?.sizeCount = detail.sizeCount -// items?[gid]?.detail?.sizeType = detail.sizeType -// items?[gid]?.detail?.rating = detail.rating -// items?[gid]?.detail?.ratingCount = detail.ratingCount -// items?[gid]?.detail?.torrentCount = detail.torrentCount + mutating func cache(detail: MangaDetail) { + PersistenceController.add(detail: detail) } mutating func insertAlterImages(gid: String, images: [MangaAlterData]) { // items?[gid]?.detail?.alterImages = images diff --git a/EhPanda/DataFlow/Store.swift b/EhPanda/DataFlow/Store.swift index 1f04bf86..8845fd1e 100644 --- a/EhPanda/DataFlow/Store.swift +++ b/EhPanda/DataFlow/Store.swift @@ -502,8 +502,8 @@ final class Store: ObservableObject { switch result { case .success(let detail): - appState.settings.user?.apikey = detail.2 - appState.cachedList.insertDetail(gid: detail.0, detail: detail.1) + appState.settings.user?.apikey = detail.1 + appState.cachedList.cache(detail: detail.0) case .failure: appState.detailInfo.mangaDetailLoadFailed = true } @@ -511,9 +511,8 @@ final class Store: ObservableObject { case .fetchMangaArchiveFunds(let gid): if appState.detailInfo.mangaArchiveFundsLoading { break } appState.detailInfo.mangaArchiveFundsLoading = true - // debugMark -// let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" -// appCommand = FetchMangaArchiveFundsCommand(detailURL: detailURL) + let detailURL = PersistenceController.fetchManga(gid: gid)?.detailURL ?? "" + appCommand = FetchMangaArchiveFundsCommand(gid: gid, detailURL: detailURL) case .fetchMangaArchiveFundsDone(let result): appState.detailInfo.mangaArchiveFundsLoading = false @@ -608,7 +607,7 @@ final class Store: ObservableObject { if appState.detailInfo.alterImagesLoading { break } appState.detailInfo.alterImagesLoading = true // debugMark -// let alterImagesURL = appState.cachedList.items?[gid]?.detail?.alterImagesURL ?? "" +// let alterImagesURL = PersistenceController.fetchManga(gid: gid)?.detail?.alterImagesURL ?? "" // appCommand = FetchAlterImagesCommand(gid: gid, alterImagesURL: alterImagesURL) case .fetchAlterImagesDone(let result): @@ -621,21 +620,21 @@ final class Store: ObservableObject { case .updateMangaDetail(let gid): if appState.detailInfo.mangaDetailUpdating { break } appState.detailInfo.mangaDetailUpdating = true - // debugMark -// let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" -// appCommand = UpdateMangaDetailCommand(gid: gid, detailURL: detailURL) + + let detailURL = PersistenceController.fetchManga(gid: gid)?.detailURL ?? "" + appCommand = UpdateMangaDetailCommand(gid: gid, detailURL: detailURL) case .updateMangaDetailDone(let result): appState.detailInfo.mangaDetailUpdating = false if case .success(let detail) = result { - appState.cachedList.updateDetail(gid: detail.0, detail: detail.1) + appState.cachedList.cache(detail: detail) } case .updateMangaComments(let gid): if appState.detailInfo.mangaCommentsUpdating { break } appState.detailInfo.mangaCommentsUpdating = true // debugMark -// let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" +// let detailURL = PersistenceController.fetchManga(gid: gid)?.detailURL ?? "" // appCommand = UpdateMangaCommentsCommand(gid: gid, detailURL: detailURL) case .updateMangaCommentsDone(result: let result): appState.detailInfo.mangaCommentsUpdating = false @@ -650,9 +649,9 @@ final class Store: ObservableObject { if appState.contentInfo.mangaContentsLoading { break } appState.contentInfo.mangaContentsLoading = true // debugMark -// appState.cachedList.items?[gid]?.detail?.currentPageNum = 0 +// PersistenceController.fetchManga(gid: gid)?.detail?.currentPageNum = 0 // -// let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" +// let detailURL = PersistenceController.fetchManga(gid: gid)?.detailURL ?? "" // appCommand = FetchMangaContentsCommand(gid: gid, detailURL: detailURL) case .fetchMangaContentsDone(let result): appState.contentInfo.mangaContentsLoading = false @@ -718,65 +717,60 @@ final class Store: ObservableObject { // MARK: Account Ops case .addFavorite(let gid, let favIndex): - break// debugMark -// let token = appState.cachedList.items?[gid]?.token ?? "" -// appCommand = AddFavoriteCommand(gid: gid, token: token, favIndex: favIndex) + let token = PersistenceController.fetchManga(gid: gid)?.token ?? "" + appCommand = AddFavoriteCommand(gid: gid, token: token, favIndex: favIndex) case .deleteFavorite(let gid): appCommand = DeleteFavoriteCommand(gid: gid) case .rate(let gid, let rating): - break // debugMark -// guard let apiuidString = appState.settings.user?.apiuid, -// let apikey = appState.settings.user?.apikey, -// let token = appState.cachedList.items?[gid]?.token, -// let apiuid = Int(apiuidString), -// let gid = Int(gid) -// else { break } -// -// appState.cachedList.updateUserRating( -// gid: String(gid), rating: Float(rating) / 2.0 -// ) -// -// appCommand = RateCommand( -// apiuid: apiuid, -// apikey: apikey, -// gid: gid, -// token: token, -// rating: rating -// ) + guard let apiuidString = appState.settings.user?.apiuid, + let apikey = appState.settings.user?.apikey, + let token = PersistenceController.fetchManga(gid: gid)?.token, + let apiuid = Int(apiuidString), + let gid = Int(gid) + else { break } + + appState.cachedList.updateUserRating( + gid: String(gid), rating: Float(rating) / 2.0 + ) + + appCommand = RateCommand( + apiuid: apiuid, + apikey: apikey, + gid: gid, + token: token, + rating: rating + ) case .comment(let gid, let content): - break // debugMark -// let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" -// appCommand = CommentCommand(gid: gid, content: content, detailURL: detailURL) + let detailURL = PersistenceController.fetchManga(gid: gid)?.detailURL ?? "" + appCommand = CommentCommand(gid: gid, content: content, detailURL: detailURL) case .editComment(let gid, let commentID, let content): - break // debugMark -// let detailURL = appState.cachedList.items?[gid]?.detailURL ?? "" -// -// appCommand = EditCommentCommand( -// gid: gid, -// commentID: commentID, -// content: content, -// detailURL: detailURL -// ) + let detailURL = PersistenceController.fetchManga(gid: gid)?.detailURL ?? "" + + appCommand = EditCommentCommand( + gid: gid, + commentID: commentID, + content: content, + detailURL: detailURL + ) case .voteComment(let gid, let commentID, let vote): - break // debugMark -// guard let apiuidString = appState.settings.user?.apiuid, -// let apikey = appState.settings.user?.apikey, -// let token = appState.cachedList.items?[gid]?.token, -// let commentID = Int(commentID), -// let apiuid = Int(apiuidString), -// let gid = Int(gid) -// else { break } -// -// appCommand = VoteCommentCommand( -// apiuid: apiuid, -// apikey: apikey, -// gid: gid, -// token: token, -// commentID: commentID, -// commentVote: vote -// ) + guard let apiuidString = appState.settings.user?.apiuid, + let apikey = appState.settings.user?.apikey, + let token = PersistenceController.fetchManga(gid: gid)?.token, + let commentID = Int(commentID), + let apiuid = Int(apiuidString), + let gid = Int(gid) + else { break } + + appCommand = VoteCommentCommand( + apiuid: apiuid, + apikey: apikey, + gid: gid, + token: token, + commentID: commentID, + commentVote: vote + ) } return (appState, appCommand) diff --git a/EhPanda/Database/Persistence.swift b/EhPanda/Database/Persistence.swift index 3809dd3e..1429de23 100644 --- a/EhPanda/Database/Persistence.swift +++ b/EhPanda/Database/Persistence.swift @@ -47,25 +47,25 @@ struct PersistenceController { return try? shared.container.viewContext.fetch(request).first } - static func add(items: [Manga]) { - for item in items { + static func add(mangas: [Manga]) { + for manga in mangas { if let storedMangaMO: MangaMO = - fetch(entityName: "MangaMO", gid: item.gid) + fetch(entityName: "MangaMO", gid: manga.gid) { - storedMangaMO.title = item.title - storedMangaMO.rating = item.rating -// storedMangaMO.tags = item.tags - storedMangaMO.language = item.language?.rawValue + storedMangaMO.title = manga.title + storedMangaMO.rating = manga.rating +// storedMangaMO.tags = manga.tags + storedMangaMO.language = manga.language?.rawValue } else { - item.toManagedObject(in: shared.container.viewContext) + manga.toManagedObject(in: shared.container.viewContext) } } saveContext() } - static func update(gid: String, detail: MangaDetail) { + static func add(detail: MangaDetail) { if let storedMangaDetailMO: MangaDetailMO = - fetch(entityName: "MangaDetailMO", gid: gid) + fetch(entityName: "MangaDetailMO", gid: detail.gid) { storedMangaDetailMO.isFavored = detail.isFavored storedMangaDetailMO.archiveURL = detail.archiveURL @@ -86,6 +86,24 @@ struct PersistenceController { } } +extension PersistenceController { + static func fetchManga(gid: String) -> Manga? { + let mangaMO: MangaMO? = PersistenceController.fetch( + entityName: "MangaMO", gid: gid + ) + return mangaMO?.toEntity() + } + static func fetchMangaNonNil(gid: String) -> Manga { + fetchManga(gid: gid) ?? Manga.empty + } + static func fetchMangaDetail(gid: String) -> MangaDetail? { + let mangaDetailMO: MangaDetailMO? = PersistenceController.fetch( + entityName: "MangaDetailMO", gid: gid + ) + return mangaDetailMO?.toEntity() + } +} + // MARK: Protocol Definition protocol ManagedObjectProtocol { associatedtype Entity diff --git a/EhPanda/Database/PersistenceAccessor.swift b/EhPanda/Database/PersistenceAccessor.swift index 771273d8..a37fb9fc 100644 --- a/EhPanda/Database/PersistenceAccessor.swift +++ b/EhPanda/Database/PersistenceAccessor.swift @@ -11,15 +11,9 @@ protocol PersistenceAccessor { extension PersistenceAccessor { var manga: Manga { - let mangaMO: MangaMO? = PersistenceController.fetch( - entityName: "MangaMO", gid: gid - ) - return mangaMO?.toEntity() ?? Manga.empty + PersistenceController.fetchMangaNonNil(gid: gid) } var mangaDetail: MangaDetail? { - let mangaDetailMO: MangaDetailMO? = PersistenceController.fetch( - entityName: "MangaDetailMO", gid: gid - ) - return mangaDetailMO?.toEntity() + PersistenceController.fetchMangaDetail(gid: gid) } } diff --git a/EhPanda/Models/Misc.swift b/EhPanda/Models/Misc.swift index 6f7d9086..79ddc8b5 100644 --- a/EhPanda/Models/Misc.swift +++ b/EhPanda/Models/Misc.swift @@ -15,7 +15,6 @@ typealias Identity = String typealias APIKey = String typealias CurrentGP = String typealias CurrentCredits = String -typealias Resp = String struct PageNumber { var current = 0 diff --git a/EhPanda/Network/Request.swift b/EhPanda/Network/Request.swift index 2fd27cf8..c72633e5 100644 --- a/EhPanda/Network/Request.swift +++ b/EhPanda/Network/Request.swift @@ -552,7 +552,7 @@ struct SendDownloadCommandRequest { let archiveURL: String let resolution: String - var publisher: AnyPublisher { + var publisher: AnyPublisher { let parameters: [String: String] = [ "hathdl_xres": resolution ] diff --git a/EhPanda/View/Detail/ArchiveView.swift b/EhPanda/View/Detail/ArchiveView.swift index 0ebf7f22..f36fc5f5 100644 --- a/EhPanda/View/Detail/ArchiveView.swift +++ b/EhPanda/View/Detail/ArchiveView.swift @@ -22,8 +22,7 @@ struct ArchiveView: View, StoreAccessor, PersistenceAccessor { @State private var hudVisible = false @State private var hudConfig = TTProgressHUDConfig() private var loadingHUDConfig = TTProgressHUDConfig( - type: .loading, - title: "Communicating...".localized() + type: .loading, title: "Communicating...".localized() ) private let gridItems = [ GridItem(.adaptive(minimum: 150, maximum: 200)) From d4bf1ed9f0295a165f66202e2675f5d38bd8a231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=A4=E3=81=9D=E3=82=99=E3=81=86?= Date: Fri, 9 Jul 2021 18:00:11 +0800 Subject: [PATCH 5/9] feat: Restore tags, previews, comments function --- EhPanda/App/Extensions.swift | 12 +++++ EhPanda/DataFlow/AppAction.swift | 8 +-- EhPanda/DataFlow/AppCommand.swift | 54 +++---------------- EhPanda/DataFlow/AppState.swift | 3 -- EhPanda/DataFlow/Store.swift | 33 ++---------- .../MangaDetailMO+CoreDataClass.swift | 14 ++--- .../MangaDetailMO+CoreDataProperties.swift | 4 +- EhPanda/Database/MangaMO+CoreDataClass.swift | 4 +- .../Model.xcdatamodel/contents | 12 +++-- EhPanda/Database/Persistence.swift | 46 +++++++++++++--- EhPanda/View/Detail/ArchiveView.swift | 6 +-- EhPanda/View/Detail/CommentView.swift | 10 ++-- EhPanda/View/Detail/DetailView.swift | 17 +++--- 13 files changed, 97 insertions(+), 126 deletions(-) diff --git a/EhPanda/App/Extensions.swift b/EhPanda/App/Extensions.swift index 8c1e9ddb..a0f49a63 100644 --- a/EhPanda/App/Extensions.swift +++ b/EhPanda/App/Extensions.swift @@ -43,6 +43,18 @@ extension Array where Element: Publisher { } } +extension Array where Element: Codable { + func toNSData() -> NSData? { + try? JSONEncoder().encode(self) as NSData + } +} + +extension NSData { + func toArray() -> [E]? { + try? JSONDecoder().decode([E].self, from: self as Data) + } +} + extension Float { func fixedRating() -> Float { let lowerbound = Int(self) diff --git a/EhPanda/DataFlow/AppAction.swift b/EhPanda/DataFlow/AppAction.swift index 7021f68d..1f5f4dd7 100644 --- a/EhPanda/DataFlow/AppAction.swift +++ b/EhPanda/DataFlow/AppAction.swift @@ -67,8 +67,8 @@ enum AppAction { case fetchFavoritesItemsDone(carriedValue: FavoritesIndex, result: Result<(PageNumber, [Manga]), AppError>) case fetchMoreFavoritesItems(index: Int) case fetchMoreFavoritesItemsDone(carriedValue: FavoritesIndex, result: Result<(PageNumber, [Manga]), AppError>) - case fetchMangaDetail(gid: String, detailURL: String) - case fetchMangaDetailDone(result: Result<(MangaDetail, APIKey), AppError>) + case fetchMangaDetail(gid: String) + case fetchMangaDetailDone(result: Result<(MangaDetail, APIKey?), AppError>) case fetchMangaArchiveFunds(gid: String) case fetchMangaArchiveFundsDone(result: Result<((CurrentGP, CurrentCredits)), AppError>) case fetchAssociatedItems(depth: Int, keyword: AssociatedKeyword) @@ -77,10 +77,6 @@ enum AppAction { case fetchMoreAssociatedItemsDone(result: Result<(Depth, AssociatedKeyword, PageNumber, [Manga]), AppError>) case fetchAlterImages(gid: String) case fetchAlterImagesDone(result: Result<(Identity, [MangaAlterData]), AppError>) - case updateMangaComments(gid: String) - case updateMangaCommentsDone(result: Result<(Identity, [MangaComment]), AppError>) - case updateMangaDetail(gid: String) - case updateMangaDetailDone(result: Result) case fetchMangaContents(gid: String) case fetchMangaContentsDone(result: Result<(Identity, PageNumber, [MangaContent]), AppError>) case fetchMoreMangaContents(gid: String) diff --git a/EhPanda/DataFlow/AppCommand.swift b/EhPanda/DataFlow/AppCommand.swift index 0c17d445..49ab0de2 100644 --- a/EhPanda/DataFlow/AppCommand.swift +++ b/EhPanda/DataFlow/AppCommand.swift @@ -400,48 +400,6 @@ struct FetchAlterImagesCommand: AppCommand { } } -struct UpdateMangaDetailCommand: AppCommand { - let gid: String - let detailURL: String - - func execute(in store: Store) { - let token = SubscriptionToken() - MangaDetailRequest(gid: gid, detailURL: detailURL) - .publisher - .receive(on: DispatchQueue.main) - .sink { completion in - if case .failure(let error) = completion { - store.dispatch(.updateMangaDetailDone(result: .failure(error))) - } - token.unseal() - } receiveValue: { detail in - store.dispatch(.updateMangaDetailDone(result: .success(detail.0))) - } - .seal(in: token) - } -} - -struct UpdateMangaCommentsCommand: AppCommand { - let gid: String - let detailURL: String - - func execute(in store: Store) { - let token = SubscriptionToken() - MangaCommentsRequest(detailURL: detailURL) - .publisher - .receive(on: DispatchQueue.main) - .sink { completion in - if case .failure(let error) = completion { - store.dispatch(.updateMangaCommentsDone(result: .failure(error))) - } - token.unseal() - } receiveValue: { comments in - store.dispatch(.updateMangaCommentsDone(result: .success((gid, comments)))) - } - .seal(in: token) - } -} - struct FetchMangaContentsCommand: AppCommand { let gid: String let detailURL: String @@ -522,7 +480,7 @@ struct AddFavoriteCommand: AppCommand { .receive(on: DispatchQueue.main) .sink { completion in if case .finished = completion { - store.dispatch(.updateMangaDetail(gid: gid)) + store.dispatch(.fetchMangaDetail(gid: gid)) } sToken.unseal() } receiveValue: { _ in } @@ -540,7 +498,7 @@ struct DeleteFavoriteCommand: AppCommand { .receive(on: DispatchQueue.main) .sink { completion in if case .finished = completion { - store.dispatch(.updateMangaDetail(gid: gid)) + store.dispatch(.fetchMangaDetail(gid: gid)) } sToken.unseal() } receiveValue: { _ in } @@ -568,7 +526,7 @@ struct RateCommand: AppCommand { .receive(on: DispatchQueue.main) .sink { completion in if case .finished = completion { - store.dispatch(.updateMangaDetail(gid: String(gid))) + store.dispatch(.fetchMangaDetail(gid: String(gid))) } sToken.unseal() } receiveValue: { _ in } @@ -588,7 +546,7 @@ struct CommentCommand: AppCommand { .receive(on: DispatchQueue.main) .sink { completion in if case .finished = completion { - store.dispatch(.updateMangaDetail(gid: gid)) + store.dispatch(.fetchMangaDetail(gid: gid)) } token.unseal() } receiveValue: { _ in } @@ -613,7 +571,7 @@ struct EditCommentCommand: AppCommand { .receive(on: DispatchQueue.main) .sink { completion in if case .finished = completion { - store.dispatch(.updateMangaDetail(gid: gid)) + store.dispatch(.fetchMangaDetail(gid: gid)) } token.unseal() } receiveValue: { _ in } @@ -643,7 +601,7 @@ struct VoteCommentCommand: AppCommand { .receive(on: DispatchQueue.main) .sink { completion in if case .finished = completion { - store.dispatch(.updateMangaDetail(gid: String(gid))) + store.dispatch(.fetchMangaDetail(gid: String(gid))) } sToken.unseal() } receiveValue: { _ in } diff --git a/EhPanda/DataFlow/AppState.swift b/EhPanda/DataFlow/AppState.swift index 71cf0c06..a9ae7b72 100644 --- a/EhPanda/DataFlow/AppState.swift +++ b/EhPanda/DataFlow/AppState.swift @@ -312,9 +312,6 @@ extension AppState { } mutating func insertAlterImages(gid: String, images: [MangaAlterData]) { // items?[gid]?.detail?.alterImages = images - } - mutating func updateComments(gid: String, comments: [MangaComment]) { -// items?[gid]?.detail?.comments = comments } mutating func insertContents(gid: String, pageNum: PageNumber, contents: [MangaContent]) { // items?[gid]?.detail?.currentPageNum = pageNum.current diff --git a/EhPanda/DataFlow/Store.swift b/EhPanda/DataFlow/Store.swift index 8845fd1e..690e276b 100644 --- a/EhPanda/DataFlow/Store.swift +++ b/EhPanda/DataFlow/Store.swift @@ -490,19 +490,22 @@ final class Store: ObservableObject { appState.homeInfo.moreFavoritesLoading[carriedValue] = true } - case .fetchMangaDetail(let gid, let detailURL): + case .fetchMangaDetail(let gid): appState.detailInfo.mangaDetailLoadFailed = false if appState.detailInfo.mangaDetailLoading { break } appState.detailInfo.mangaDetailLoading = true + let detailURL = PersistenceController.fetchManga(gid: gid)?.detailURL ?? "" appCommand = FetchMangaDetailCommand(gid: gid, detailURL: detailURL) case .fetchMangaDetailDone(let result): appState.detailInfo.mangaDetailLoading = false switch result { case .success(let detail): - appState.settings.user?.apikey = detail.1 + if let apikey = detail.1 { + appState.settings.user?.apikey = apikey + } appState.cachedList.cache(detail: detail.0) case .failure: appState.detailInfo.mangaDetailLoadFailed = true @@ -617,32 +620,6 @@ final class Store: ObservableObject { appState.cachedList.insertAlterImages(gid: images.0, images: images.1) } - case .updateMangaDetail(let gid): - if appState.detailInfo.mangaDetailUpdating { break } - appState.detailInfo.mangaDetailUpdating = true - - let detailURL = PersistenceController.fetchManga(gid: gid)?.detailURL ?? "" - appCommand = UpdateMangaDetailCommand(gid: gid, detailURL: detailURL) - case .updateMangaDetailDone(let result): - appState.detailInfo.mangaDetailUpdating = false - - if case .success(let detail) = result { - appState.cachedList.cache(detail: detail) - } - - case .updateMangaComments(let gid): - if appState.detailInfo.mangaCommentsUpdating { break } - appState.detailInfo.mangaCommentsUpdating = true - // debugMark -// let detailURL = PersistenceController.fetchManga(gid: gid)?.detailURL ?? "" -// appCommand = UpdateMangaCommentsCommand(gid: gid, detailURL: detailURL) - case .updateMangaCommentsDone(result: let result): - appState.detailInfo.mangaCommentsUpdating = false - - if case .success(let comments) = result { - appState.cachedList.updateComments(gid: comments.0, comments: comments.1) - } - case .fetchMangaContents(let gid): appState.contentInfo.mangaContentsLoadFailed = false diff --git a/EhPanda/Database/MangaDetailMO+CoreDataClass.swift b/EhPanda/Database/MangaDetailMO+CoreDataClass.swift index 0988ef51..320d1b46 100644 --- a/EhPanda/Database/MangaDetailMO+CoreDataClass.swift +++ b/EhPanda/Database/MangaDetailMO+CoreDataClass.swift @@ -14,8 +14,10 @@ extension MangaDetailMO: ManagedObjectProtocol { MangaDetail( isFavored: isFavored, archiveURL: archiveURL, alterImagesURL: nil, alterImages: [], torrents: [], - comments: [], previews: [], gid: gid, title: title, - rating: rating, ratingCount: ratingCount, detailTags: [], + comments: (comments?.toArray() ?? []) as [MangaComment], + previews: (previews?.toArray() ?? []) as [MangaPreview], + gid: gid, title: title, rating: rating, ratingCount: ratingCount, + detailTags: (tags?.toArray() ?? []) as [MangaTag], category: Category(rawValue: category)!, language: Language(rawValue: language)!, uploader: uploader, publishedDate: publishedDate, @@ -29,27 +31,27 @@ extension MangaDetail: ManagedObjectConvertible { @discardableResult func toManagedObject(in context: NSManagedObjectContext) -> MangaDetailMO { let mangaDetailMO = MangaDetailMO(context: context) -// let mangaDetailMO = MangaDetailMO.getOrCreateSingle( -// with: "gid", from: context -// ) + mangaDetailMO.gid = gid mangaDetailMO.archiveURL = archiveURL mangaDetailMO.category = category.rawValue + mangaDetailMO.comments = comments.toNSData() mangaDetailMO.coverURL = coverURL mangaDetailMO.isFavored = isFavored mangaDetailMO.jpnTitle = jpnTitle mangaDetailMO.language = language.rawValue mangaDetailMO.likeCount = likeCount mangaDetailMO.pageCount = pageCount + mangaDetailMO.previews = previews.toNSData() mangaDetailMO.publishedDate = publishedDate mangaDetailMO.rating = rating mangaDetailMO.ratingCount = ratingCount mangaDetailMO.sizeCount = sizeCount mangaDetailMO.sizeType = sizeType + mangaDetailMO.tags = detailTags.toNSData() mangaDetailMO.title = title mangaDetailMO.torrentCount = Int64(torrentCount) mangaDetailMO.uploader = uploader - mangaDetailMO.userRating = userRating ?? 0.0 return mangaDetailMO } diff --git a/EhPanda/Database/MangaDetailMO+CoreDataProperties.swift b/EhPanda/Database/MangaDetailMO+CoreDataProperties.swift index f3813937..a4cad8dc 100644 --- a/EhPanda/Database/MangaDetailMO+CoreDataProperties.swift +++ b/EhPanda/Database/MangaDetailMO+CoreDataProperties.swift @@ -14,6 +14,7 @@ extension MangaDetailMO: Identifiable { @NSManaged public var archiveURL: String? @NSManaged public var category: String + @NSManaged public var comments: NSData? @NSManaged public var coverURL: String @NSManaged public var gid: String @NSManaged public var isFavored: Bool @@ -21,13 +22,14 @@ extension MangaDetailMO: Identifiable { @NSManaged public var language: String @NSManaged public var likeCount: String @NSManaged public var pageCount: String + @NSManaged public var previews: NSData? @NSManaged public var publishedDate: Date @NSManaged public var rating: Float @NSManaged public var ratingCount: String @NSManaged public var sizeCount: String @NSManaged public var sizeType: String + @NSManaged public var tags: NSData? @NSManaged public var title: String @NSManaged public var torrentCount: Int64 @NSManaged public var uploader: String - @NSManaged public var userRating: Float } diff --git a/EhPanda/Database/MangaMO+CoreDataClass.swift b/EhPanda/Database/MangaMO+CoreDataClass.swift index 55de8995..cf97dd39 100644 --- a/EhPanda/Database/MangaMO+CoreDataClass.swift +++ b/EhPanda/Database/MangaMO+CoreDataClass.swift @@ -26,9 +26,7 @@ extension Manga: ManagedObjectConvertible { @discardableResult func toManagedObject(in context: NSManagedObjectContext) -> MangaMO { let mangaMO = MangaMO(context: context) -// let mangaMO = MangaMO.getOrCreateSingle( -// with: gid, from: context -// ) + mangaMO.gid = gid mangaMO.category = category.rawValue mangaMO.coverURL = coverURL diff --git a/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents b/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents index 862b58f9..e1357d90 100644 --- a/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -3,6 +3,7 @@ + @@ -10,15 +11,16 @@ + + - @@ -32,10 +34,12 @@ - + + + - + - + \ No newline at end of file diff --git a/EhPanda/Database/Persistence.swift b/EhPanda/Database/Persistence.swift index 1429de23..8a5968b0 100644 --- a/EhPanda/Database/Persistence.swift +++ b/EhPanda/Database/Persistence.swift @@ -40,11 +40,44 @@ struct PersistenceController { return (try? shared.container.viewContext.count(for: request)) ?? 0 > 0 } - static func fetch(entityName: String, gid: String) -> E? { - let request = NSFetchRequest(entityName: entityName) + static func materializedObject( + in context: NSManagedObjectContext, + matching predicate: NSPredicate + ) -> NSManagedObject? { + for object in context.registeredObjects + where !object.isFault { + guard predicate.evaluate(with: object) + else { continue } + return object + } + return nil + } + + static func fetch( + entityName: String, gid: String + ) -> E? { + fetch( + entityName: entityName, + predicate: NSPredicate( + format: "gid == %@", gid + ) + ) + } + + static func fetch( + entityName: String, predicate: NSPredicate + ) -> E? { + let context = shared.container.viewContext + if let object = materializedObject( + in: context, matching: predicate + ) as? E { return object } + + let request = NSFetchRequest( + entityName: entityName + ) request.fetchLimit = 1 - request.predicate = NSPredicate(format: "gid == %@", gid) - return try? shared.container.viewContext.fetch(request).first + request.predicate = predicate + return try? context.fetch(request).first } static func add(mangas: [Manga]) { @@ -54,7 +87,6 @@ struct PersistenceController { { storedMangaMO.title = manga.title storedMangaMO.rating = manga.rating -// storedMangaMO.tags = manga.tags storedMangaMO.language = manga.language?.rawValue } else { manga.toManagedObject(in: shared.container.viewContext) @@ -69,8 +101,8 @@ struct PersistenceController { { storedMangaDetailMO.isFavored = detail.isFavored storedMangaDetailMO.archiveURL = detail.archiveURL -// storedMangaDetailMO.detailTags = detail.detailTags -// storedMangaDetailMO.comments = detail.comments + storedMangaDetailMO.tags = detail.detailTags.toNSData() + storedMangaDetailMO.comments = detail.comments.toNSData() storedMangaDetailMO.jpnTitle = detail.jpnTitle storedMangaDetailMO.likeCount = detail.likeCount storedMangaDetailMO.pageCount = detail.pageCount diff --git a/EhPanda/View/Detail/ArchiveView.swift b/EhPanda/View/Detail/ArchiveView.swift index f36fc5f5..699c70b0 100644 --- a/EhPanda/View/Detail/ArchiveView.swift +++ b/EhPanda/View/Detail/ArchiveView.swift @@ -365,10 +365,10 @@ struct ArchiveView_Previews: PreviewProvider { manga.detail?.archive = archive store.appState.settings.user = user store.appState.environment.isPreview = true - // debugMark -// store.appState.cachedList.items?["mangaForTest"] = manga - return ArchiveView(gid: "mangaForTest") + store.appState.cachedList.cache(mangas: [manga]) + + return ArchiveView(gid: "") .environmentObject(store) } } diff --git a/EhPanda/View/Detail/CommentView.swift b/EhPanda/View/Detail/CommentView.swift index f3a37c3e..128540e7 100644 --- a/EhPanda/View/Detail/CommentView.swift +++ b/EhPanda/View/Detail/CommentView.swift @@ -22,10 +22,12 @@ struct CommentView: View, StoreAccessor { private let gid: String private let depth: Int + private let comments: [MangaComment] - init(gid: String, depth: Int) { + init(gid: String, depth: Int, comments: [MangaComment]) { self.gid = gid self.depth = depth + self.comments = comments } // MARK: CommentView @@ -121,12 +123,6 @@ struct CommentView: View, StoreAccessor { // MARK: Private Extension private extension CommentView { - var comments: [MangaComment] { - [] - // debugMark -// store.appState.cachedList.items?[gid]?.detail?.comments ?? [] - } - var environmentBinding: Binding { $store.appState.environment } diff --git a/EhPanda/View/Detail/DetailView.swift b/EhPanda/View/Detail/DetailView.swift index 2764840e..d4f2890f 100644 --- a/EhPanda/View/Detail/DetailView.swift +++ b/EhPanda/View/Detail/DetailView.swift @@ -15,6 +15,9 @@ struct DetailView: View, StoreAccessor, PersistenceAccessor { @State private var associatedKeyword = AssociatedKeyword() @State private var isNavLinkActive = false + @State private var comments = [MangaComment]() + private var loadingFlag = false + let gid: String private let depth: Int @@ -161,11 +164,7 @@ private extension DetailView { store.dispatch(.toggleNavBar(hidden: false)) } - if mangaDetail == nil { - fetchMangaDetail() - } else { - updateMangaDetail() - } + fetchMangaDetail() updateViewControllersCount() } func onDisappear() { @@ -220,10 +219,7 @@ private extension DetailView { store.dispatch(.updateViewControllersCount) } func fetchMangaDetail() { - store.dispatch(.fetchMangaDetail(gid: gid, detailURL: manga.detailURL)) - } - func updateMangaDetail() { - store.dispatch(.updateMangaDetail(gid: gid)) + store.dispatch(.fetchMangaDetail(gid: gid)) } func updateHistoryItems() { DispatchQueue.main.async { @@ -728,7 +724,8 @@ private struct CommentScrollView: View { Spacer() NavigationLink( destination: CommentView( - gid: gid, depth: depth + gid: gid, depth: depth, + comments: comments ) ) { Text("Show All") From 741bb76878ea0737acc9a12a32ecfbd08b84b5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=A4=E3=81=9D=E3=82=99=E3=81=86?= Date: Sat, 10 Jul 2021 11:13:59 +0800 Subject: [PATCH 6/9] feat: Restore reading function --- EhPanda.xcodeproj/project.pbxproj | 8 + EhPanda/App/Extensions.swift | 13 +- EhPanda/App/Tools/Parser.swift | 19 ++- EhPanda/App/ja.lproj/Localizable.strings | 3 - EhPanda/App/zh-Hans.lproj/Localizable.strings | 3 - EhPanda/App/zh-Hant.lproj/Localizable.strings | 3 - EhPanda/DataFlow/AppAction.swift | 3 +- EhPanda/DataFlow/AppCommand.swift | 8 +- EhPanda/DataFlow/AppState.swift | 43 ------ EhPanda/DataFlow/Store.swift | 124 ++++++++------- EhPanda/DataFlow/StoreAccessor.swift | 3 - .../MangaDetailMO+CoreDataClass.swift | 10 +- .../MangaDetailMO+CoreDataProperties.swift | 7 +- .../Database/MangaMO+CoreDataProperties.swift | 2 +- .../Database/MangaStateMO+CoreDataClass.swift | 44 ++++++ .../MangaStateMO+CoreDataProperties.swift | 25 +++ .../Model.xcdatamodel/contents | 20 ++- EhPanda/Database/Persistence.swift | 104 ++++--------- EhPanda/Database/PersistenceAccessor.swift | 142 ++++++++++++++++++ EhPanda/Models/Models.swift | 41 +++-- EhPanda/Network/Request.swift | 11 +- EhPanda/View/Content/ContentView.swift | 42 ++---- EhPanda/View/Detail/ArchiveView.swift | 25 ++- EhPanda/View/Detail/CommentView.swift | 2 +- EhPanda/View/Detail/DetailView.swift | 17 +-- EhPanda/View/Home/HomeView.swift | 2 +- EhPanda/View/Setting/GeneralSettingView.swift | 12 -- EhPanda/View/Setting/SettingView.swift | 16 -- 28 files changed, 419 insertions(+), 333 deletions(-) create mode 100644 EhPanda/Database/MangaStateMO+CoreDataClass.swift create mode 100644 EhPanda/Database/MangaStateMO+CoreDataProperties.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 9368c7ae..4b8d9198 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ AB018DD2268756D000EB0EA9 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = AB018DD1268756D000EB0EA9 /* Kanna */; }; + AB10117E26986B7D00C2C1A9 /* MangaStateMO+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB10117D26986B7D00C2C1A9 /* MangaStateMO+CoreDataProperties.swift */; }; + AB10118026986C1100C2C1A9 /* MangaStateMO+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB10117F26986C1100C2C1A9 /* MangaStateMO+CoreDataClass.swift */; }; AB19D619266E5C6700BA752A /* TTProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = AB19D618266E5C6700BA752A /* TTProgressHUD */; }; AB2CED64268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB2CED63268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift */; }; AB2F72BF2685A8B00088DECA /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = AB2F72BE2685A8B00088DECA /* Kingfisher */; }; @@ -84,6 +86,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + AB10117D26986B7D00C2C1A9 /* MangaStateMO+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaStateMO+CoreDataProperties.swift"; sourceTree = ""; }; + AB10117F26986C1100C2C1A9 /* MangaStateMO+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaStateMO+CoreDataClass.swift"; sourceTree = ""; }; AB2CED63268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaMO+CoreDataProperties.swift"; sourceTree = ""; }; AB38A0CA25CA993D00764D64 /* ColorCodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorCodable.swift; sourceTree = ""; }; AB40CFDB25983EC200D1DC9A /* FileStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileStorage.swift; sourceTree = ""; }; @@ -218,6 +222,8 @@ AB2CED63268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift */, ABCA93C12691929D00A98BC6 /* MangaDetailMO+CoreDataClass.swift */, AB4FD2C0268AB83300A95968 /* MangaDetailMO+CoreDataProperties.swift */, + AB10117F26986C1100C2C1A9 /* MangaStateMO+CoreDataClass.swift */, + AB10117D26986B7D00C2C1A9 /* MangaStateMO+CoreDataProperties.swift */, ); path = Database; sourceTree = ""; @@ -505,6 +511,7 @@ ABCD2F0A259763FC008E5A20 /* Request.swift in Sources */, ABF45AE925F3313D00ECB568 /* AlertView.swift in Sources */, ABF45ABB25F3312F00ECB568 /* AppError.swift in Sources */, + AB10118026986C1100C2C1A9 /* MangaStateMO+CoreDataClass.swift in Sources */, ABF45AE025F3313D00ECB568 /* HomeView.swift in Sources */, ABCA93BE26918DE100A98BC6 /* Persistence.swift in Sources */, ABF45AEF25F3313D00ECB568 /* TorrentsView.swift in Sources */, @@ -549,6 +556,7 @@ ABA732DF25A852D800B3D9AB /* Filter.swift in Sources */, ABC1FAB82642C37D00A9F352 /* NewDawnView.swift in Sources */, ABF45AE825F3313D00ECB568 /* LinkedText.swift in Sources */, + AB10117E26986B7D00C2C1A9 /* MangaStateMO+CoreDataProperties.swift in Sources */, ABF45AE525F3313D00ECB568 /* Comment.swift in Sources */, ABC1FAB6264152C800A9F352 /* StoreAccessor.swift in Sources */, ABF75F3F25A19CD200544D29 /* User.swift in Sources */, diff --git a/EhPanda/App/Extensions.swift b/EhPanda/App/Extensions.swift index a0f49a63..09354cc6 100644 --- a/EhPanda/App/Extensions.swift +++ b/EhPanda/App/Extensions.swift @@ -43,15 +43,18 @@ extension Array where Element: Publisher { } } -extension Array where Element: Codable { - func toNSData() -> NSData? { - try? JSONEncoder().encode(self) as NSData +extension Encodable { + func toData() -> Data? { + try? JSONEncoder().encode(self) } } -extension NSData { +extension Data { func toArray() -> [E]? { - try? JSONDecoder().decode([E].self, from: self as Data) + try? JSONDecoder().decode([E].self, from: self) + } + func toAspectBox() -> [Int: CGFloat]? { + try? JSONDecoder().decode([Int: CGFloat].self, from: self) } } diff --git a/EhPanda/App/Tools/Parser.swift b/EhPanda/App/Tools/Parser.swift index f082d9cd..1f1167a9 100644 --- a/EhPanda/App/Tools/Parser.swift +++ b/EhPanda/App/Tools/Parser.swift @@ -99,7 +99,7 @@ struct Parser { } // MARK: Detail - static func parseMangaDetail(doc: HTMLDocument, gid: String) throws -> MangaDetail { + static func parseMangaDetail(doc: HTMLDocument, gid: String) throws -> (MangaDetail, MangaState) { func parseCoverURL(node: XMLElement?) throws -> String { guard let coverHTML = node?.at_xpath("//div [@id='gd1']")?.innerHTML, let rangeA = coverHTML.range(of: "url("), @@ -257,6 +257,7 @@ struct Parser { } var tmpMangaDetail: MangaDetail? + var tmpMangaState: MangaState? for link in doc.xpath("//div [@class='gm']") { guard let gdtNode = doc.at_xpath("//div [@id='gdt']"), let gd3Node = link.at_xpath("//div [@id='gd3']"), @@ -267,7 +268,7 @@ struct Parser { let gdfNode = gd3Node.at_xpath("//div [@id='gdf']"), let rating = try? parseRating(node: gdrNode), let coverURL = try? parseCoverURL(node: link), - let detailTags = try? parseTags(node: gd4Node), + let tags = try? parseTags(node: gd4Node), let previews = try? parsePreviews(node: gdtNode), let arcAndTor = try? parseArcAndTor(node: gd5Node), let infoPanel = try? parseInfoPanel(node: gddNode), @@ -290,15 +291,11 @@ struct Parser { archiveURL: arcAndTor.0, alterImagesURL: try? parseAlterImagesURL(doc: doc), alterImages: [], - torrents: [], - comments: parseComments(doc: doc), - previews: previews, gid: gid, title: engTitle, jpnTitle: jpnTitle, rating: rating, ratingCount: ratingCount, - detailTags: detailTags, category: category, language: language, uploader: uploader, @@ -310,13 +307,19 @@ struct Parser { sizeType: infoPanel[3], torrentCount: arcAndTor.1 ) + tmpMangaState = MangaState( + gid: gid, tags: tags, + previews: previews, + comments: parseComments(doc: doc) + ) break } - guard let mangaDetail = tmpMangaDetail + guard let mangaDetail = tmpMangaDetail, + let mangaState = tmpMangaState else { throw AppError.parseFailed } - return mangaDetail + return (mangaDetail, mangaState) } // MARK: Comment diff --git a/EhPanda/App/ja.lproj/Localizable.strings b/EhPanda/App/ja.lproj/Localizable.strings index b68a50fa..ae549c93 100644 --- a/EhPanda/App/ja.lproj/Localizable.strings +++ b/EhPanda/App/ja.lproj/Localizable.strings @@ -77,8 +77,6 @@ "Are you sure to logout?" = "本当にログアウトしますか?"; "Are you sure to clear?" = "本当に削除しますか?"; "Clear" = "削除"; -"Warning" = "警告"; -"It's for debug only." = "デバッグ専用機能です"; "General" = "一般"; "Close slide menu after selection" = "選択後スライドメニューを閉じる"; @@ -89,7 +87,6 @@ "App switcher blur" = "アプリスイッチャーぼかし"; "Cache" = "キャッシュ"; "Clear image caches" = "画像キャッシュを削除"; -"Clear web caches" = "ウェブキャッシュを削除"; "Copy cookies" = "クッキーをコピー"; "Appearance" = "外観"; diff --git a/EhPanda/App/zh-Hans.lproj/Localizable.strings b/EhPanda/App/zh-Hans.lproj/Localizable.strings index 2a0d3690..f915a549 100644 --- a/EhPanda/App/zh-Hans.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hans.lproj/Localizable.strings @@ -77,8 +77,6 @@ "Are you sure to logout?" = "确定要退出登录吗?"; "Are you sure to clear?" = "确定要清空吗?"; "Clear" = "清空"; -"Warning" = "警告"; -"It's for debug only." = "这是仅为问题排查设计的功能"; "General" = "一般"; "Close slide menu after selection" = "选择后关闭侧边栏"; @@ -89,7 +87,6 @@ "App switcher blur" = "在应用切换器中模糊处理"; "Cache" = "缓存"; "Clear image caches" = "清空图片缓存"; -"Clear web caches" = "清空网络缓存"; "Copy cookies" = "复制Cookies"; "Appearance" = "外观"; diff --git a/EhPanda/App/zh-Hant.lproj/Localizable.strings b/EhPanda/App/zh-Hant.lproj/Localizable.strings index 5b1412be..cb2f4389 100644 --- a/EhPanda/App/zh-Hant.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hant.lproj/Localizable.strings @@ -77,8 +77,6 @@ "Are you sure to logout?" = "確定要登出嗎?"; "Are you sure to clear?" = "確定要清空嗎?"; "Clear" = "清空"; -"Warning" = "警告"; -"It's for debug only." = "僅用於偵錯的功能"; "General" = "一般"; "Close slide menu after selection" = "選擇後關閉側邊選單"; @@ -89,7 +87,6 @@ "App switcher blur" = "模糊多工處理中的預覽"; "Cache" = "快取"; "Clear image caches" = "清空圖片快取"; -"Clear web caches" = "清空網頁快取"; "Copy cookies" = "複製Cookies"; "Appearance" = "外觀"; diff --git a/EhPanda/DataFlow/AppAction.swift b/EhPanda/DataFlow/AppAction.swift index 1f5f4dd7..21548889 100644 --- a/EhPanda/DataFlow/AppAction.swift +++ b/EhPanda/DataFlow/AppAction.swift @@ -11,7 +11,6 @@ import Foundation enum AppAction { case replaceUser(user: User?) - case clearCachedList case clearHistoryItems case initializeStates case initializeFilter @@ -68,7 +67,7 @@ enum AppAction { case fetchMoreFavoritesItems(index: Int) case fetchMoreFavoritesItemsDone(carriedValue: FavoritesIndex, result: Result<(PageNumber, [Manga]), AppError>) case fetchMangaDetail(gid: String) - case fetchMangaDetailDone(result: Result<(MangaDetail, APIKey?), AppError>) + case fetchMangaDetailDone(result: Result<(MangaDetail, MangaState, APIKey?), AppError>) case fetchMangaArchiveFunds(gid: String) case fetchMangaArchiveFundsDone(result: Result<((CurrentGP, CurrentCredits)), AppError>) case fetchAssociatedItems(depth: Int, keyword: AssociatedKeyword) diff --git a/EhPanda/DataFlow/AppCommand.swift b/EhPanda/DataFlow/AppCommand.swift index 49ab0de2..e144824a 100644 --- a/EhPanda/DataFlow/AppCommand.swift +++ b/EhPanda/DataFlow/AppCommand.swift @@ -301,10 +301,10 @@ struct FetchMangaDetailCommand: AppCommand { } token.unseal() } receiveValue: { detail in - store.dispatch(.fetchMangaDetailDone(result: .success((detail.0, detail.1)))) - if detail.0.previews.isEmpty == true { - store.dispatch(.fetchAlterImages(gid: gid)) - } + store.dispatch(.fetchMangaDetailDone(result: .success((detail.0, detail.1, detail.2)))) +// if detail.0.previews.isEmpty == true { +// store.dispatch(.fetchAlterImages(gid: gid)) +// } } .seal(in: token) } diff --git a/EhPanda/DataFlow/AppState.swift b/EhPanda/DataFlow/AppState.swift index a9ae7b72..0dec7077 100644 --- a/EhPanda/DataFlow/AppState.swift +++ b/EhPanda/DataFlow/AppState.swift @@ -15,7 +15,6 @@ struct AppState { var detailInfo = DetailInfo() var commentInfo = CommentInfo() var contentInfo = ContentInfo() - var cachedList = CachedList() } extension AppState { @@ -297,45 +296,3 @@ extension AppState { var moreMangaContentsLoadFailed = false } } - -extension AppState { - // MARK: CachedList - struct CachedList { - func hasCached(gid: String) -> Bool { - PersistenceController.checkExistence(entityName: "MangaMO", gid: gid) - } - mutating func cache(mangas: [Manga]) { - PersistenceController.add(mangas: mangas) - } - mutating func cache(detail: MangaDetail) { - PersistenceController.add(detail: detail) - } - mutating func insertAlterImages(gid: String, images: [MangaAlterData]) { -// items?[gid]?.detail?.alterImages = images - } - mutating func insertContents(gid: String, pageNum: PageNumber, contents: [MangaContent]) { -// items?[gid]?.detail?.currentPageNum = pageNum.current -// items?[gid]?.detail?.pageNumMaximum = pageNum.maximum -// -// if items?[gid]?.contents == nil { -// items?[gid]?.contents = contents.sorted { $0.tag < $1.tag } -// } else { -// contents.forEach { content in -// if items?[gid]?.contents?.contains(content) == false { -// items?[gid]?.contents?.append(content) -// } -// } -// items?[gid]?.contents?.sort { $0.tag < $1.tag } -// } - } - mutating func insertAspectBox(gid: String, box: [Int: CGFloat]) { -// items?[gid]?.detail?.aspectBox = box - } - mutating func insertReadingProgress(gid: String, progress: Int) { -// items?[gid]?.detail?.readingProgress = progress - } - mutating func updateUserRating(gid: String, rating: Float) { -// items?[gid]?.detail?.userRating = rating - } - } -} diff --git a/EhPanda/DataFlow/Store.swift b/EhPanda/DataFlow/Store.swift index 690e276b..97f69968 100644 --- a/EhPanda/DataFlow/Store.swift +++ b/EhPanda/DataFlow/Store.swift @@ -42,10 +42,6 @@ final class Store: ObservableObject { // MARK: App Ops case .replaceUser(let user): appState.settings.user = user - case .clearCachedList: - break - // debugMark -// appState.cachedList.items = nil case .clearHistoryItems: appState.homeInfo.historyItems = nil case .initializeStates: @@ -64,9 +60,9 @@ final class Store: ObservableObject { case .initializeFilter: appState.settings.filter = Filter() case .saveAspectBox(let gid, let box): - appState.cachedList.insertAspectBox(gid: gid, box: box) + PersistenceController.update(gid: gid, aspectBox: box) case .saveReadingProgress(let gid, let tag): - appState.cachedList.insertReadingProgress(gid: gid, progress: tag) + PersistenceController.update(gid: gid, readingProgress: tag) case .updateDiskImageCacheSize(let size): appState.settings.setting?.diskImageCacheSize = size case .updateAppIconType(let iconType): @@ -182,7 +178,7 @@ final class Store: ObservableObject { switch result { case .success(let manga): - appState.cachedList.cache(mangas: [manga]) + PersistenceController.add(mangas: [manga]) appState.environment.mangaItemReverseID = manga.gid case .failure: appState.environment.mangaItemReverseLoadFailed = true @@ -216,7 +212,7 @@ final class Store: ObservableObject { appState.homeInfo.searchNotFound = true } } else { - appState.cachedList.cache(mangas: mangas.2) + PersistenceController.add(mangas: mangas.2) } case .failure: appState.homeInfo.searchLoadFailed = true @@ -250,7 +246,7 @@ final class Store: ObservableObject { appState.homeInfo.searchPageNumMaximum = mangas.1.maximum appState.homeInfo.insertSearchItems(mangas: mangas.2) - appState.cachedList.cache(mangas: mangas.2) + PersistenceController.add(mangas: mangas.2) if mangas.1.current < mangas.1.maximum && mangas.2.isEmpty { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in @@ -289,7 +285,7 @@ final class Store: ObservableObject { appState.homeInfo.frontpageNotFound = true } } else { - appState.cachedList.cache(mangas: mangas.1) + PersistenceController.add(mangas: mangas.1) } case .failure: appState.homeInfo.frontpageLoadFailed = true @@ -317,7 +313,7 @@ final class Store: ObservableObject { appState.homeInfo.frontpagePageNumMaximum = mangas.0.maximum appState.homeInfo.insertFrontpageItems(mangas: mangas.1) - appState.cachedList.cache(mangas: mangas.1) + PersistenceController.add(mangas: mangas.1) if mangas.0.current < mangas.0.maximum && mangas.1.isEmpty { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in @@ -346,7 +342,7 @@ final class Store: ObservableObject { appState.homeInfo.popularNotFound = true } else { appState.homeInfo.popularItems = mangas.1 - appState.cachedList.cache(mangas: mangas.1) + PersistenceController.add(mangas: mangas.1) } case .failure: appState.homeInfo.popularLoadFailed = true @@ -378,7 +374,7 @@ final class Store: ObservableObject { appState.homeInfo.watchedNotFound = true } } else { - appState.cachedList.cache(mangas: mangas.1) + PersistenceController.add(mangas: mangas.1) } case .failure: appState.homeInfo.watchedLoadFailed = true @@ -406,7 +402,7 @@ final class Store: ObservableObject { appState.homeInfo.watchedPageNumMaximum = mangas.0.maximum appState.homeInfo.insertWatchedItems(mangas: mangas.1) - appState.cachedList.cache(mangas: mangas.1) + PersistenceController.add(mangas: mangas.1) if mangas.0.current < mangas.0.maximum && mangas.1.isEmpty { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in @@ -445,7 +441,7 @@ final class Store: ObservableObject { appState.homeInfo.favoritesNotFound[carriedValue] = true } } else { - appState.cachedList.cache(mangas: mangas.1) + PersistenceController.add(mangas: mangas.1) } case .failure: appState.homeInfo.favoritesLoadFailed[carriedValue] = true @@ -477,7 +473,7 @@ final class Store: ObservableObject { appState.homeInfo.favoritesPageNumMaximum[carriedValue] = mangas.0.maximum appState.homeInfo.insertFavoritesItems(favIndex: carriedValue, mangas: mangas.1) - appState.cachedList.cache(mangas: mangas.1) + PersistenceController.add(mangas: mangas.1) if mangas.0.current < mangas.0.maximum && mangas.1.isEmpty { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in @@ -503,10 +499,11 @@ final class Store: ObservableObject { switch result { case .success(let detail): - if let apikey = detail.1 { + if let apikey = detail.2 { appState.settings.user?.apikey = apikey } - appState.cachedList.cache(detail: detail.0) + PersistenceController.add(detail: detail.0) + PersistenceController.update(fetchedState: detail.1) case .failure: appState.detailInfo.mangaDetailLoadFailed = true } @@ -557,7 +554,7 @@ final class Store: ObservableObject { appState.detailInfo.associatedItemsNotFound = true } } else { - appState.cachedList.cache(mangas: mangas.3) + PersistenceController.add(mangas: mangas.3) } case .failure: appState.detailInfo.associatedItemsLoadFailed = true @@ -593,7 +590,7 @@ final class Store: ObservableObject { pageNum: mangas.2, items: mangas.3 ) - appState.cachedList.cache(mangas: mangas.3) + PersistenceController.add(mangas: mangas.3) if mangas.2.current < mangas.2.maximum && mangas.3.isEmpty { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in @@ -616,26 +613,24 @@ final class Store: ObservableObject { case .fetchAlterImagesDone(let result): appState.detailInfo.alterImagesLoading = false - if case .success(let images) = result { - appState.cachedList.insertAlterImages(gid: images.0, images: images.1) - } +// if case .success(let images) = result { +// appState.cachedList.insertAlterImages(gid: images.0, images: images.1) +// } case .fetchMangaContents(let gid): appState.contentInfo.mangaContentsLoadFailed = false if appState.contentInfo.mangaContentsLoading { break } appState.contentInfo.mangaContentsLoading = true - // debugMark -// PersistenceController.fetchManga(gid: gid)?.detail?.currentPageNum = 0 -// -// let detailURL = PersistenceController.fetchManga(gid: gid)?.detailURL ?? "" -// appCommand = FetchMangaContentsCommand(gid: gid, detailURL: detailURL) + + let detailURL = PersistenceController.fetchManga(gid: gid)?.detailURL ?? "" + appCommand = FetchMangaContentsCommand(gid: gid, detailURL: detailURL) case .fetchMangaContentsDone(let result): appState.contentInfo.mangaContentsLoading = false switch result { case .success(let contents): - appState.cachedList.insertContents( + PersistenceController.update( gid: contents.0, pageNum: contents.1, contents: contents.2 @@ -646,44 +641,45 @@ final class Store: ObservableObject { case .fetchMoreMangaContents(let gid): appState.contentInfo.moreMangaContentsLoadFailed = false - // debugMark -// guard let manga = appState.cachedList.items?[gid], -// let detail = manga.detail -// else { break } -// -// let currentNum = detail.currentPageNum -// let maximumNum = detail.pageNumMaximum -// if currentNum + 1 >= maximumNum { break } -// -// if appState.contentInfo.moreMangaContentsLoading { break } -// appState.contentInfo.moreMangaContentsLoading = true -// -// let detailURL = manga.detailURL -// let pageNum = currentNum + 1 -// let pageCount = manga.contents?.count ?? 0 -// appCommand = FetchMoreMangaContentsCommand( -// gid: gid, -// detailURL: detailURL, -// pageNum: pageNum, -// pageCount: pageCount -// ) -// -// if pageCount >= Int(detail.pageCount) ?? 0 { -// SwiftyBeaver.error( -// "MangaContents overflow", -// context: [ -// "detailURL": manga.detailURL, -// "pageLimit": detail.pageCount, -// "pageCurrentAmount": pageCount -// ] -// ) -// } + + guard let manga = PersistenceController.fetchManga(gid: gid), + let detail = PersistenceController.fetchMangaDetail(gid: gid) + else { break } + let state = PersistenceController.fetchMangaStateNonNil(gid: gid) + + let currentNum = state.currentPageNum + let maximumNum = state.pageNumMaximum + if currentNum + 1 >= maximumNum { break } + + if appState.contentInfo.moreMangaContentsLoading { break } + appState.contentInfo.moreMangaContentsLoading = true + + let detailURL = manga.detailURL + let pageNum = currentNum + 1 + let pageCount = state.contents.count + appCommand = FetchMoreMangaContentsCommand( + gid: gid, + detailURL: detailURL, + pageNum: pageNum, + pageCount: pageCount + ) + + if pageCount >= Int(detail.pageCount) ?? 0 { + SwiftyBeaver.error( + "MangaContents overflow", + context: [ + "detailURL": manga.detailURL, + "pageLimit": detail.pageCount, + "pageCurrentAmount": pageCount + ] + ) + } case .fetchMoreMangaContentsDone(let result): appState.contentInfo.moreMangaContentsLoading = false switch result { case .success(let contents): - appState.cachedList.insertContents( + PersistenceController.update( gid: contents.0, pageNum: contents.1, contents: contents.2 @@ -707,8 +703,8 @@ final class Store: ObservableObject { let gid = Int(gid) else { break } - appState.cachedList.updateUserRating( - gid: String(gid), rating: Float(rating) / 2.0 + PersistenceController.update( + gid: String(gid), userRating: Float(rating) / 2.0 ) appCommand = RateCommand( diff --git a/EhPanda/DataFlow/StoreAccessor.swift b/EhPanda/DataFlow/StoreAccessor.swift index 5eb2998b..17ae0d7a 100644 --- a/EhPanda/DataFlow/StoreAccessor.swift +++ b/EhPanda/DataFlow/StoreAccessor.swift @@ -35,9 +35,6 @@ extension StoreAccessor { var contentInfo: AppState.ContentInfo { appState.contentInfo } - var cachedList: AppState.CachedList { - appState.cachedList - } } // MARK: Environment diff --git a/EhPanda/Database/MangaDetailMO+CoreDataClass.swift b/EhPanda/Database/MangaDetailMO+CoreDataClass.swift index 320d1b46..0a799a72 100644 --- a/EhPanda/Database/MangaDetailMO+CoreDataClass.swift +++ b/EhPanda/Database/MangaDetailMO+CoreDataClass.swift @@ -13,11 +13,8 @@ extension MangaDetailMO: ManagedObjectProtocol { func toEntity() -> MangaDetail { MangaDetail( isFavored: isFavored, archiveURL: archiveURL, - alterImagesURL: nil, alterImages: [], torrents: [], - comments: (comments?.toArray() ?? []) as [MangaComment], - previews: (previews?.toArray() ?? []) as [MangaPreview], + alterImagesURL: nil, alterImages: [], gid: gid, title: title, rating: rating, ratingCount: ratingCount, - detailTags: (tags?.toArray() ?? []) as [MangaTag], category: Category(rawValue: category)!, language: Language(rawValue: language)!, uploader: uploader, publishedDate: publishedDate, @@ -35,22 +32,19 @@ extension MangaDetail: ManagedObjectConvertible { mangaDetailMO.gid = gid mangaDetailMO.archiveURL = archiveURL mangaDetailMO.category = category.rawValue - mangaDetailMO.comments = comments.toNSData() mangaDetailMO.coverURL = coverURL mangaDetailMO.isFavored = isFavored mangaDetailMO.jpnTitle = jpnTitle mangaDetailMO.language = language.rawValue mangaDetailMO.likeCount = likeCount mangaDetailMO.pageCount = pageCount - mangaDetailMO.previews = previews.toNSData() mangaDetailMO.publishedDate = publishedDate mangaDetailMO.rating = rating mangaDetailMO.ratingCount = ratingCount mangaDetailMO.sizeCount = sizeCount mangaDetailMO.sizeType = sizeType - mangaDetailMO.tags = detailTags.toNSData() mangaDetailMO.title = title - mangaDetailMO.torrentCount = Int64(torrentCount) + mangaDetailMO.torrentCount = Int16(torrentCount) mangaDetailMO.uploader = uploader return mangaDetailMO diff --git a/EhPanda/Database/MangaDetailMO+CoreDataProperties.swift b/EhPanda/Database/MangaDetailMO+CoreDataProperties.swift index a4cad8dc..c4a7a1ff 100644 --- a/EhPanda/Database/MangaDetailMO+CoreDataProperties.swift +++ b/EhPanda/Database/MangaDetailMO+CoreDataProperties.swift @@ -9,12 +9,11 @@ import CoreData extension MangaDetailMO: Identifiable { @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "MangaDetailMO") + NSFetchRequest(entityName: "MangaDetailMO") } @NSManaged public var archiveURL: String? @NSManaged public var category: String - @NSManaged public var comments: NSData? @NSManaged public var coverURL: String @NSManaged public var gid: String @NSManaged public var isFavored: Bool @@ -22,14 +21,12 @@ extension MangaDetailMO: Identifiable { @NSManaged public var language: String @NSManaged public var likeCount: String @NSManaged public var pageCount: String - @NSManaged public var previews: NSData? @NSManaged public var publishedDate: Date @NSManaged public var rating: Float @NSManaged public var ratingCount: String @NSManaged public var sizeCount: String @NSManaged public var sizeType: String - @NSManaged public var tags: NSData? @NSManaged public var title: String - @NSManaged public var torrentCount: Int64 + @NSManaged public var torrentCount: Int16 @NSManaged public var uploader: String } diff --git a/EhPanda/Database/MangaMO+CoreDataProperties.swift b/EhPanda/Database/MangaMO+CoreDataProperties.swift index f18d9022..252f3199 100644 --- a/EhPanda/Database/MangaMO+CoreDataProperties.swift +++ b/EhPanda/Database/MangaMO+CoreDataProperties.swift @@ -9,7 +9,7 @@ import CoreData extension MangaMO: Identifiable { @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "MangaMO") + NSFetchRequest(entityName: "MangaMO") } @NSManaged public var category: String diff --git a/EhPanda/Database/MangaStateMO+CoreDataClass.swift b/EhPanda/Database/MangaStateMO+CoreDataClass.swift new file mode 100644 index 00000000..14b63204 --- /dev/null +++ b/EhPanda/Database/MangaStateMO+CoreDataClass.swift @@ -0,0 +1,44 @@ +// +// MangaStateMO+CoreDataClass.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/07/09. +// + +import CoreData + +public class MangaStateMO: NSManagedObject {} + +extension MangaStateMO: ManagedObjectProtocol { + func toEntity() -> MangaState { + MangaState( + gid: gid, tags: tags?.toArray() ?? [MangaTag](), + userRating: userRating, currentPageNum: Int(currentPageNum), + pageNumMaximum: Int(pageNumMaximum), readingProgress: Int(readingProgress), + previews: previews?.toArray() ?? [MangaPreview](), + comments: comments?.toArray() ?? [MangaComment](), + contents: contents?.toArray() ?? [MangaContent](), + aspectBox: aspectBox?.toAspectBox() ?? [:] + ) + } +} + +extension MangaState: ManagedObjectConvertible { + @discardableResult + func toManagedObject(in context: NSManagedObjectContext) -> MangaStateMO { + let mangaMO = MangaStateMO(context: context) + + mangaMO.gid = gid + mangaMO.tags = tags.toData() + mangaMO.userRating = userRating + mangaMO.currentPageNum = Int16(currentPageNum) + mangaMO.pageNumMaximum = Int16(pageNumMaximum) + mangaMO.readingProgress = Int16(readingProgress) + mangaMO.previews = previews.toData() + mangaMO.comments = comments.toData() + mangaMO.contents = contents.toData() + mangaMO.aspectBox = aspectBox.toData() + + return mangaMO + } +} diff --git a/EhPanda/Database/MangaStateMO+CoreDataProperties.swift b/EhPanda/Database/MangaStateMO+CoreDataProperties.swift new file mode 100644 index 00000000..513ec332 --- /dev/null +++ b/EhPanda/Database/MangaStateMO+CoreDataProperties.swift @@ -0,0 +1,25 @@ +// +// MangaStateMO+CoreDataProperties.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/07/09. +// + +import CoreData + +extension MangaStateMO: GalleryIdentifiable { + @nonobjc public class func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: "MangaStateMO") + } + + @NSManaged public var aspectBox: Data? + @NSManaged public var comments: Data? + @NSManaged public var contents: Data? + @NSManaged public var currentPageNum: Int16 + @NSManaged public var gid: String + @NSManaged public var pageNumMaximum: Int16 + @NSManaged public var previews: Data? + @NSManaged public var readingProgress: Int16 + @NSManaged public var tags: Data? + @NSManaged public var userRating: Float +} diff --git a/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents b/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents index e1357d90..0235638a 100644 --- a/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -3,7 +3,6 @@ - @@ -11,15 +10,13 @@ - - - + @@ -34,12 +31,21 @@ - + + + + + + + + + + - + - + \ No newline at end of file diff --git a/EhPanda/Database/Persistence.swift b/EhPanda/Database/Persistence.swift index 8a5968b0..3a35218f 100644 --- a/EhPanda/Database/Persistence.swift +++ b/EhPanda/Database/Persistence.swift @@ -33,11 +33,19 @@ struct PersistenceController { } } - static func checkExistence(entityName: String, gid: String) -> Bool { - let request = NSFetchRequest(entityName: entityName) + static func checkExistence( + entityType: MO.Type, + predicate: NSPredicate + ) -> Bool { + let request = NSFetchRequest( + entityName: String(describing: entityType) + ) request.fetchLimit = 1 - request.predicate = NSPredicate(format: "gid == %@", gid) - return (try? shared.container.viewContext.count(for: request)) ?? 0 > 0 + request.predicate = predicate + + let context = shared.container.viewContext + let resultCount = (try? context.count(for: request)) ?? 0 + return resultCount > 0 } static func materializedObject( @@ -53,86 +61,32 @@ struct PersistenceController { return nil } - static func fetch( - entityName: String, gid: String - ) -> E? { - fetch( - entityName: entityName, - predicate: NSPredicate( - format: "gid == %@", gid - ) - ) - } - - static func fetch( - entityName: String, predicate: NSPredicate - ) -> E? { + static func fetch( + entityType: MO.Type, predicate: NSPredicate + ) -> MO? { let context = shared.container.viewContext if let object = materializedObject( in: context, matching: predicate - ) as? E { return object } + ) as? MO { return object } - let request = NSFetchRequest( - entityName: entityName + let request = NSFetchRequest( + entityName: String(describing: entityType) ) request.fetchLimit = 1 request.predicate = predicate return try? context.fetch(request).first } - static func add(mangas: [Manga]) { - for manga in mangas { - if let storedMangaMO: MangaMO = - fetch(entityName: "MangaMO", gid: manga.gid) - { - storedMangaMO.title = manga.title - storedMangaMO.rating = manga.rating - storedMangaMO.language = manga.language?.rawValue - } else { - manga.toManagedObject(in: shared.container.viewContext) - } - } - saveContext() - } - - static func add(detail: MangaDetail) { - if let storedMangaDetailMO: MangaDetailMO = - fetch(entityName: "MangaDetailMO", gid: detail.gid) - { - storedMangaDetailMO.isFavored = detail.isFavored - storedMangaDetailMO.archiveURL = detail.archiveURL - storedMangaDetailMO.tags = detail.detailTags.toNSData() - storedMangaDetailMO.comments = detail.comments.toNSData() - storedMangaDetailMO.jpnTitle = detail.jpnTitle - storedMangaDetailMO.likeCount = detail.likeCount - storedMangaDetailMO.pageCount = detail.pageCount - storedMangaDetailMO.sizeCount = detail.sizeCount - storedMangaDetailMO.sizeType = detail.sizeType - storedMangaDetailMO.rating = detail.rating - storedMangaDetailMO.ratingCount = detail.ratingCount - storedMangaDetailMO.torrentCount = Int64(detail.torrentCount) - } else { - detail.toManagedObject(in: shared.container.viewContext) - } - saveContext() - } -} - -extension PersistenceController { - static func fetchManga(gid: String) -> Manga? { - let mangaMO: MangaMO? = PersistenceController.fetch( - entityName: "MangaMO", gid: gid + /// Create one if fetch result is empty, and update it. + static func update( + entityType: MO.Type, gid: String, + commitChanges: ((MO) -> Void) + ) { + let storedMO: MO = fetchOrCreate( + entityType: entityType, gid: gid ) - return mangaMO?.toEntity() - } - static func fetchMangaNonNil(gid: String) -> Manga { - fetchManga(gid: gid) ?? Manga.empty - } - static func fetchMangaDetail(gid: String) -> MangaDetail? { - let mangaDetailMO: MangaDetailMO? = PersistenceController.fetch( - entityName: "MangaDetailMO", gid: gid - ) - return mangaDetailMO?.toEntity() + commitChanges(storedMO) + saveContext() } } @@ -148,3 +102,7 @@ protocol ManagedObjectConvertible { @discardableResult func toManagedObject(in context: NSManagedObjectContext) -> ManagedObject } + +protocol GalleryIdentifiable: NSManagedObject { + var gid: String { get set } +} diff --git a/EhPanda/Database/PersistenceAccessor.swift b/EhPanda/Database/PersistenceAccessor.swift index a37fb9fc..3f197ad7 100644 --- a/EhPanda/Database/PersistenceAccessor.swift +++ b/EhPanda/Database/PersistenceAccessor.swift @@ -5,6 +5,9 @@ // Created by 荒木辰造 on R 3/07/05. // +import SwiftUI +import CoreData + protocol PersistenceAccessor { var gid: String { get } } @@ -16,4 +19,143 @@ extension PersistenceAccessor { var mangaDetail: MangaDetail? { PersistenceController.fetchMangaDetail(gid: gid) } + var mangaState: MangaState { + PersistenceController.fetchMangaStateNonNil(gid: gid) + } +} + +// MARK: Accessor Method +extension PersistenceController { + static func fetchManga(gid: String) -> Manga? { + PersistenceController.fetch( + entityType: MangaMO.self, gid: gid + )?.toEntity() + } + static func fetchMangaNonNil(gid: String) -> Manga { + fetchManga(gid: gid) ?? Manga.empty + } + static func fetchMangaDetail(gid: String) -> MangaDetail? { + PersistenceController.fetch( + entityType: MangaDetailMO.self, gid: gid + )?.toEntity() + } + static func fetchMangaStateNonNil(gid: String) -> MangaState { + PersistenceController.fetchOrCreate( + entityType: MangaStateMO.self, gid: gid + ).toEntity() + } + + static func fetch( + entityType: MO.Type, gid: String + ) -> MO? { + fetch( + entityType: entityType, + predicate: NSPredicate( + format: "gid == %@", gid + ) + ) + } + static func fetchOrCreate( + entityType: MO.Type, gid: String + ) -> MO { + if let storedMO = fetch(entityType: entityType, gid: gid) { + return storedMO + } else { + let newMO = MO(context: shared.container.viewContext) + newMO.gid = gid + saveContext() + return newMO + } + } + + static func add(mangas: [Manga]) { + for manga in mangas { + if let storedMangaMO: MangaMO = + fetch(entityType: MangaMO.self, gid: manga.gid) + { + storedMangaMO.title = manga.title + storedMangaMO.rating = manga.rating + storedMangaMO.language = manga.language?.rawValue + } else { + manga.toManagedObject(in: shared.container.viewContext) + } + } + saveContext() + } + + static func add(detail: MangaDetail) { + if let storedMangaDetailMO: MangaDetailMO = + fetch(entityType: MangaDetailMO.self, gid: detail.gid) + { + storedMangaDetailMO.isFavored = detail.isFavored + storedMangaDetailMO.archiveURL = detail.archiveURL + storedMangaDetailMO.jpnTitle = detail.jpnTitle + storedMangaDetailMO.likeCount = detail.likeCount + storedMangaDetailMO.pageCount = detail.pageCount + storedMangaDetailMO.sizeCount = detail.sizeCount + storedMangaDetailMO.sizeType = detail.sizeType + storedMangaDetailMO.rating = detail.rating + storedMangaDetailMO.ratingCount = detail.ratingCount + storedMangaDetailMO.torrentCount = Int16(detail.torrentCount) + } else { + detail.toManagedObject(in: shared.container.viewContext) + } + saveContext() + } + + static func update(gid: String, mangaStateMO: ((MangaStateMO) -> Void)) { + update(entityType: MangaStateMO.self, gid: gid, commitChanges: mangaStateMO) + } + + static func hasCached(gid: String) -> Bool { + PersistenceController.checkExistence( + entityType: MangaMO.self, + predicate: NSPredicate( + format: "gid == %@", gid + ) + ) + } + static func update(fetchedState: MangaState) { + PersistenceController.update(gid: fetchedState.gid) { mangaStateMO in + mangaStateMO.tags = fetchedState.tags.toData() + mangaStateMO.previews = fetchedState.previews.toData() + mangaStateMO.comments = fetchedState.comments.toData() + } + } + static func update(gid: String, aspectBox: [Int: CGFloat]) { + PersistenceController.update(gid: gid) { mangaStateMO in + mangaStateMO.aspectBox = aspectBox.toData() + } + } + static func update(gid: String, readingProgress: Int) { + PersistenceController.update(gid: gid) { mangaStateMO in + mangaStateMO.readingProgress = Int16(readingProgress) + } + } + static func update(gid: String, userRating: Float) { + PersistenceController.update(gid: gid) { mangaStateMO in + mangaStateMO.userRating = userRating + } + } + static func update(gid: String, pageNum: PageNumber, contents: [MangaContent]) { + PersistenceController.update(gid: gid) { mangaStateMO in + mangaStateMO.currentPageNum = Int16(pageNum.current) + mangaStateMO.pageNumMaximum = Int16(pageNum.maximum) + + let newContents = contents.sorted(by: { $0.tag < $1.tag }) + var storedContents = mangaStateMO.contents? + .toArray() ?? [MangaContent]() + + if storedContents.isEmpty { + mangaStateMO.contents = newContents.toData() + } else { + newContents.forEach { content in + if !storedContents.contains(content) { + storedContents.append(content) + } + } + mangaStateMO.contents = storedContents.toData() + } + } + } } diff --git a/EhPanda/Models/Models.swift b/EhPanda/Models/Models.swift index 9bb80c7d..bd51f372 100644 --- a/EhPanda/Models/Models.swift +++ b/EhPanda/Models/Models.swift @@ -65,14 +65,10 @@ struct MangaDetail: Codable { isFavored: false, alterImagesURL: nil, alterImages: [], - torrents: [], - comments: [], - previews: [], gid: "", title: "", rating: 0.0, ratingCount: "", - detailTags: [], category: .nonH, language: .English, uploader: "", @@ -85,27 +81,16 @@ struct MangaDetail: Codable { torrentCount: 0 ) - var readingProgress: Int? - var currentPageNum = 0 - var pageNumMaximum = 1 - var aspectBox = [Int: CGFloat]() - var isFavored: Bool var archiveURL: String? - var archive: MangaArchive? let alterImagesURL: String? var alterImages: [MangaAlterData] - var torrents: [MangaTorrent] - var comments: [MangaComment] - let previews: [MangaPreview] let gid: String var title: String var jpnTitle: String? var rating: Float - var userRating: Float? var ratingCount: String - var detailTags: [MangaTag] let category: Category let language: Language let uploader: String @@ -118,6 +103,32 @@ struct MangaDetail: Codable { var torrentCount: Int } +struct MangaState: Codable { + static let empty = MangaState( + gid: "", + tags: [], + userRating: 0, + currentPageNum: 0, + pageNumMaximum: 1, + readingProgress: 0, + previews: [], + comments: [], + contents: [], + aspectBox: [:] + ) + + let gid: String + var tags = [MangaTag]() + var userRating: Float = 0 + var currentPageNum = 0 + var pageNumMaximum = 1 + var readingProgress = 0 + var previews = [MangaPreview]() + var comments = [MangaComment]() + var contents = [MangaContent]() + var aspectBox = [Int: CGFloat]() +} + struct MangaArchive: Codable { struct HathArchive: Codable, Identifiable { var id: String { resolution.rawValue } diff --git a/EhPanda/Network/Request.swift b/EhPanda/Network/Request.swift index c72633e5..ff1c6f41 100644 --- a/EhPanda/Network/Request.swift +++ b/EhPanda/Network/Request.swift @@ -104,7 +104,7 @@ struct MangaItemReverseRequest { URLSession.shared .dataTaskPublisher(for: detailURL.safeURL()) .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .compactMap { getManga(from: try? Parser.parseMangaDetail(doc: $0, gid: gid)) } + .compactMap { getManga(from: try? Parser.parseMangaDetail(doc: $0, gid: gid).0) } .mapError(mapAppError) .eraseToAnyPublisher() } @@ -277,7 +277,7 @@ struct MangaDetailRequest { let gid: String let detailURL: String - var publisher: AnyPublisher<(MangaDetail, APIKey), AppError> { + var publisher: AnyPublisher<(MangaDetail, MangaState, APIKey), AppError> { URLSession.shared .dataTaskPublisher( for: Defaults.URL @@ -287,7 +287,10 @@ struct MangaDetailRequest { .safeURL() ) .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } - .tryMap { try (Parser.parseMangaDetail(doc: $0, gid: gid), Parser.parseAPIKey(doc: $0))} + .tryMap { + let detail = try Parser.parseMangaDetail(doc: $0, gid: gid) + return (detail.0, detail.1, try Parser.parseAPIKey(doc: $0)) + } .mapError(mapAppError) .eraseToAnyPublisher() } @@ -390,7 +393,7 @@ struct MangaArchiveFundsRequest { .dataTaskPublisher(for: url.safeURL()) .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } .compactMap { try? Parser - .parseMangaDetail(doc: $0, gid: gid) + .parseMangaDetail(doc: $0, gid: gid).0 .archiveURL } .mapError(mapAppError) diff --git a/EhPanda/View/Content/ContentView.swift b/EhPanda/View/Content/ContentView.swift index 8a329152..80e6c5c8 100644 --- a/EhPanda/View/Content/ContentView.swift +++ b/EhPanda/View/Content/ContentView.swift @@ -10,7 +10,7 @@ import Combine import Kingfisher import SDWebImageSwiftUI -struct ContentView: View, StoreAccessor { +struct ContentView: View, StoreAccessor, PersistenceAccessor { @EnvironmentObject var store: Store @State private var position: CGFloat = 0 @@ -21,7 +21,7 @@ struct ContentView: View, StoreAccessor { @State private var offset: CGSize = .zero @State private var newOffset: CGSize = .zero - private let gid: String + let gid: String init(gid: String) { self.gid = gid @@ -73,10 +73,6 @@ struct ContentView: View, StoreAccessor { .onAppear { onWebImageAppear(item: item) } - #if DEBUG - Text("\(item.tag + 1)/\(mangaDetail?.pageCount ?? "")") - .bold().font(.largeTitle).foregroundColor(.gray) - #endif } } LoadMoreFooter( @@ -143,15 +139,8 @@ struct ContentView: View, StoreAccessor { // MARK: Private Extension private extension ContentView { // MARK: Properties - var mangaDetail: MangaDetail? { - nil - // debugMark -// cachedList.items?[gid]?.detail - } - var mangaContents: [MangaContent]? { - [] - // debugMark -// cachedList.items?[gid]?.contents + var mangaContents: [MangaContent] { + mangaState.contents } var moreLoadingFlag: Bool { contentInfo.moreMangaContentsLoading @@ -180,12 +169,13 @@ private extension ContentView { } } func onLazyVStackAppear(proxy: ScrollViewProxy) { - if let tag = mangaDetail?.readingProgress { - proxy.scrollTo(tag) + let progress = mangaState.readingProgress + if progress > 0 { + proxy.scrollTo(progress) } } func onWebImageAppear(item: MangaContent) { - if item == mangaContents?.last { + if item == mangaContents.last { fetchMoreMangaContents() } } @@ -204,11 +194,7 @@ private extension ContentView { } func fetchMangaContentsIfNeeded() { - if let contents = mangaContents, !contents.isEmpty { - if contents.count != Int(mangaDetail?.pageCount ?? "") { - fetchMangaContents() - } - } else { + if mangaContents.isEmpty { fetchMangaContents() } } @@ -227,12 +213,9 @@ private extension ContentView { } } func calReadingProgress() -> Int { - guard let contentsCount = mangaContents?.count - else { return -1 } - var heightArray = Array( repeating: screenH * contentHScale, - count: contentsCount + count: mangaContents.count ) aspectBox.forEach { (key: Int, value: CGFloat) in heightArray[key] = value * screenW @@ -263,8 +246,9 @@ private extension ContentView { } } func restoreAspectBox() { - if let aspectBox = mangaDetail?.aspectBox { - self.aspectBox = aspectBox + let box = mangaState.aspectBox + if !box.isEmpty { + aspectBox = box } } func saveAspectBox() { diff --git a/EhPanda/View/Detail/ArchiveView.swift b/EhPanda/View/Detail/ArchiveView.swift index 699c70b0..6e67685d 100644 --- a/EhPanda/View/Detail/ArchiveView.swift +++ b/EhPanda/View/Detail/ArchiveView.swift @@ -348,25 +348,24 @@ struct ArchiveView_Previews: PreviewProvider { static var previews: some View { let store = Store() var user = User.empty - var manga = Manga.empty - let hathArchives = ArchiveRes.allCases.map { - MangaArchive.HathArchive( - resolution: $0, - fileSize: "114 MB", - gpPrice: "514 GP" - ) - } - let archive = MangaArchive( - hathArchives: hathArchives - ) +// var manga = Manga.empty +// let hathArchives = ArchiveRes.allCases.map { +// MangaArchive.HathArchive( +// resolution: $0, +// fileSize: "114 MB", +// gpPrice: "514 GP" +// ) +// } +// let archive = MangaArchive( +// hathArchives: hathArchives +// ) user.currentGP = "114" user.currentCredits = "514" - manga.detail?.archive = archive store.appState.settings.user = user store.appState.environment.isPreview = true - store.appState.cachedList.cache(mangas: [manga]) +// store.appState.cachedList.cache(mangas: [manga]) return ArchiveView(gid: "") .environmentObject(store) diff --git a/EhPanda/View/Detail/CommentView.swift b/EhPanda/View/Detail/CommentView.swift index 128540e7..e388a4a7 100644 --- a/EhPanda/View/Detail/CommentView.swift +++ b/EhPanda/View/Detail/CommentView.swift @@ -147,7 +147,7 @@ private extension CommentView { func onLinkTap(link: URL) { if isValidDetailURL(url: link) { let gid = link.pathComponents[2] - if cachedList.hasCached(gid: gid) { + if PersistenceController.hasCached(gid: gid) { replaceMangaCommentJumpID(gid: gid) } else { store.dispatch( diff --git a/EhPanda/View/Detail/DetailView.swift b/EhPanda/View/Detail/DetailView.swift index d4f2890f..9e6a03b8 100644 --- a/EhPanda/View/Detail/DetailView.swift +++ b/EhPanda/View/Detail/DetailView.swift @@ -15,9 +15,6 @@ struct DetailView: View, StoreAccessor, PersistenceAccessor { @State private var associatedKeyword = AssociatedKeyword() @State private var isNavLinkActive = false - @State private var comments = [MangaComment]() - private var loadingFlag = false - let gid: String private let depth: Int @@ -55,20 +52,20 @@ struct DetailView: View, StoreAccessor, PersistenceAccessor { ) { onSimilarGalleryTap(title: detail.title) } - if !detail.detailTags.isEmpty { + if !mangaState.tags.isEmpty { TagsView( - tags: detail.detailTags, + tags: mangaState.tags, onTapAction: onTagsViewTap ) } PreviewView( - previews: detail.previews, + previews: mangaState.previews, alterImages: detail.alterImages ) CommentScrollView( gid: gid, depth: depth, - comments: detail.comments, + comments: mangaState.comments, toggleCommentAction: onCommentButtonTap ) } @@ -539,9 +536,9 @@ private struct ActionRow: View { private extension ActionRow { func onStartTasks() { - if let rating = detail.userRating { - userRating = Int(rating.fixedRating() * 2) - } +// if let rating = detail.userRating { +// userRating = Int(rating.fixedRating() * 2) +// } } func onRateButtonTap() { withAnimation { diff --git a/EhPanda/View/Home/HomeView.swift b/EhPanda/View/Home/HomeView.swift index 168e511d..c9d015e8 100644 --- a/EhPanda/View/Home/HomeView.swift +++ b/EhPanda/View/Home/HomeView.swift @@ -354,7 +354,7 @@ private extension HomeView { isValidDetailURL(url: link) { let gid = link.pathComponents[2] - if cachedList.hasCached(gid: gid) { + if PersistenceController.hasCached(gid: gid) { replaceMangaCommentJumpID(gid: gid) } else { store.dispatch( diff --git a/EhPanda/View/Setting/GeneralSettingView.swift b/EhPanda/View/Setting/GeneralSettingView.swift index 774b1b86..2b3fe083 100644 --- a/EhPanda/View/Setting/GeneralSettingView.swift +++ b/EhPanda/View/Setting/GeneralSettingView.swift @@ -69,15 +69,6 @@ struct GeneralSettingView: View, StoreAccessor { } .foregroundColor(.primary) } - Button(action: toggleClearWebCaches) { - HStack { - Text("Clear web caches") - Spacer() - Text(browsingCaches()) - .foregroundStyle(.tint) - } - .foregroundColor(.primary) - } } Section(header: Text("Advanced")) { NavigationLink("Logs", destination: LogsView()) @@ -127,7 +118,4 @@ private extension GeneralSettingView { func toggleClearImgCaches() { store.dispatch(.toggleSettingViewActionSheet(state: .clearImgCaches)) } - func toggleClearWebCaches() { - store.dispatch(.toggleSettingViewActionSheet(state: .clearWebCaches)) - } } diff --git a/EhPanda/View/Setting/SettingView.swift b/EhPanda/View/Setting/SettingView.swift index 775bb36e..8efda561 100644 --- a/EhPanda/View/Setting/SettingView.swift +++ b/EhPanda/View/Setting/SettingView.swift @@ -75,15 +75,6 @@ struct SettingView: View, StoreAccessor { .destructive(Text("Clear"), action: clearImageCaches), .cancel() ]) - case .clearWebCaches: - return ActionSheet( - title: Text("Warning".localized().uppercased()), - message: Text("It's for debug only."), - buttons: [ - .destructive(Text("Clear"), action: clearCachedList), - .cancel() - ] - ) } } } @@ -98,7 +89,6 @@ private extension SettingView { func logout() { clearCookies() clearImageCaches() - store.dispatch(.clearCachedList) store.dispatch(.clearHistoryItems) store.dispatch(.replaceUser(user: nil)) } @@ -123,11 +113,6 @@ private extension SettingView { KingfisherManager.shared.cache.clearDiskCache() calculateDiskCachesSize() } - func clearCachedList() { - store.dispatch(.clearCachedList) - store.dispatch(.clearHistoryItems) - store.dispatch(.fetchFrontpageItems) - } } // MARK: SettingRow @@ -202,7 +187,6 @@ enum SettingViewActionSheetState: Identifiable { case logout case clearImgCaches - case clearWebCaches } enum SettingViewSheetState: Identifiable { From e528ca5278df4b9d71d45ff2f6f60af9d0ab31e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=A4=E3=81=9D=E3=82=99=E3=81=86?= Date: Sat, 10 Jul 2021 17:46:25 +0800 Subject: [PATCH 7/9] feat: Restore history function --- EhPanda/DataFlow/AppAction.swift | 2 - EhPanda/DataFlow/AppState.swift | 37 +++------ EhPanda/DataFlow/Store.swift | 7 -- EhPanda/Database/MangaMO+CoreDataClass.swift | 5 +- .../Database/MangaMO+CoreDataProperties.swift | 3 +- .../Model.xcdatamodel/contents | 3 +- EhPanda/Database/Persistence.swift | 75 ++++++++++++------- EhPanda/Database/PersistenceAccessor.swift | 35 +++++++-- EhPanda/Models/Models.swift | 15 ++-- EhPanda/Network/Request.swift | 1 - EhPanda/View/Detail/CommentView.swift | 2 +- EhPanda/View/Detail/DetailView.swift | 6 +- EhPanda/View/Home/HomeView.swift | 19 ++--- EhPanda/View/Setting/SettingView.swift | 1 - 14 files changed, 112 insertions(+), 99 deletions(-) diff --git a/EhPanda/DataFlow/AppAction.swift b/EhPanda/DataFlow/AppAction.swift index 21548889..aff194f5 100644 --- a/EhPanda/DataFlow/AppAction.swift +++ b/EhPanda/DataFlow/AppAction.swift @@ -11,7 +11,6 @@ import Foundation enum AppAction { case replaceUser(user: User?) - case clearHistoryItems case initializeStates case initializeFilter case clearDetailViewCommentContent @@ -20,7 +19,6 @@ enum AppAction { case saveReadingProgress(gid: String, tag: Int) case updateDiskImageCacheSize(size: String) case updateAppIconType(iconType: IconType) - case updateHistoryItems(gid: String) case updateHistoryKeywords(text: String) case clearHistoryKeywords case updateSearchKeyword(text: String) diff --git a/EhPanda/DataFlow/AppState.swift b/EhPanda/DataFlow/AppState.swift index 0dec7077..2b780108 100644 --- a/EhPanda/DataFlow/AppState.swift +++ b/EhPanda/DataFlow/AppState.swift @@ -122,20 +122,18 @@ extension AppState { var moreWatchedLoadFailed = false var favoritesItems = [Int: [Manga]]() - var favoritesLoading = generateBoolDic() - var favoritesNotFound = generateBoolDic() - var favoritesLoadFailed = generateBoolDic() - var favoritesCurrentPageNum = generateIntDic() - var favoritesPageNumMaximum = generateIntDic(defaultValue: 1) - var moreFavoritesLoading = generateBoolDic() - var moreFavoritesLoadFailed = generateBoolDic() - - @FileStorage(directory: .cachesDirectory, fileName: "historyList.json") - var historyItems: [String: Manga]? + var favoritesLoading = generateBoolDict() + var favoritesNotFound = generateBoolDict() + var favoritesLoadFailed = generateBoolDict() + var favoritesCurrentPageNum = generateIntDict() + var favoritesPageNumMaximum = generateIntDict(defaultValue: 1) + var moreFavoritesLoading = generateBoolDict() + var moreFavoritesLoadFailed = generateBoolDict() + @FileStorage(directory: .cachesDirectory, fileName: "historyKeywords.json") var historyKeywords: [String]? - static func generateBoolDic(defaultValue: Bool = false) -> [Int: Bool] { + static func generateBoolDict(defaultValue: Bool = false) -> [Int: Bool] { var tmp = [Int: Bool]() (-1..<10).forEach { index in tmp[index] = defaultValue @@ -143,7 +141,7 @@ extension AppState { return tmp } - static func generateIntDic(defaultValue: Int = 0) -> [Int: Int] { + static func generateIntDict(defaultValue: Int = 0) -> [Int: Int] { var tmp = [Int: Int]() (-1..<10).forEach { index in tmp[index] = defaultValue @@ -179,21 +177,6 @@ extension AppState { } } } - mutating func insertHistoryItem(manga: Manga?) { - guard var manga = manga else { return } - if historyItems != nil { - if historyItems?.keys.contains(manga.gid) == true { - historyItems?[manga.gid]?.lastOpenTime = Date() - } else { - manga.lastOpenTime = Date() - historyItems?[manga.gid] = manga - } - } else { - historyItems = Dictionary( - uniqueKeysWithValues: [(manga.gid, manga)] - ) - } - } mutating func insertHistoryKeyword(text: String) { guard !text.isEmpty else { return } guard var historyKeywords = historyKeywords else { diff --git a/EhPanda/DataFlow/Store.swift b/EhPanda/DataFlow/Store.swift index 97f69968..2249905f 100644 --- a/EhPanda/DataFlow/Store.swift +++ b/EhPanda/DataFlow/Store.swift @@ -42,8 +42,6 @@ final class Store: ObservableObject { // MARK: App Ops case .replaceUser(let user): appState.settings.user = user - case .clearHistoryItems: - appState.homeInfo.historyItems = nil case .initializeStates: if appState.settings.user == nil { appState.settings.user = User() @@ -67,11 +65,6 @@ final class Store: ObservableObject { appState.settings.setting?.diskImageCacheSize = size case .updateAppIconType(let iconType): appState.settings.setting?.appIconType = iconType - case .updateHistoryItems(let gid): - // debugMark - break -// let item = appState.cachedList.items?[gid] -// appState.homeInfo.insertHistoryItem(manga: item) case .updateHistoryKeywords(let text): appState.homeInfo.insertHistoryKeyword(text: text) case .clearHistoryKeywords: diff --git a/EhPanda/Database/MangaMO+CoreDataClass.swift b/EhPanda/Database/MangaMO+CoreDataClass.swift index cf97dd39..54349541 100644 --- a/EhPanda/Database/MangaMO+CoreDataClass.swift +++ b/EhPanda/Database/MangaMO+CoreDataClass.swift @@ -12,13 +12,13 @@ public class MangaMO: NSManagedObject {} extension MangaMO: ManagedObjectProtocol { func toEntity() -> Manga { Manga( - detail: nil, contents: [], gid: gid, token: token, + gid: gid, token: token, title: title, rating: rating, tags: [], category: Category(rawValue: category)!, language: Language(rawValue: language ?? ""), uploader: uploader, publishedDate: publishedDate, coverURL: coverURL, detailURL: detailURL, - lastOpenTime: nil + lastOpenDate: lastOpenDate ) } } @@ -28,6 +28,7 @@ extension Manga: ManagedObjectConvertible { let mangaMO = MangaMO(context: context) mangaMO.gid = gid + mangaMO.lastOpenDate = lastOpenDate mangaMO.category = category.rawValue mangaMO.coverURL = coverURL mangaMO.detailURL = detailURL diff --git a/EhPanda/Database/MangaMO+CoreDataProperties.swift b/EhPanda/Database/MangaMO+CoreDataProperties.swift index 252f3199..dc051ba6 100644 --- a/EhPanda/Database/MangaMO+CoreDataProperties.swift +++ b/EhPanda/Database/MangaMO+CoreDataProperties.swift @@ -7,11 +7,12 @@ import CoreData -extension MangaMO: Identifiable { +extension MangaMO: Identifiable, GalleryIdentifiable { @nonobjc public class func fetchRequest() -> NSFetchRequest { NSFetchRequest(entityName: "MangaMO") } + @NSManaged public var lastOpenDate: Date? @NSManaged public var category: String @NSManaged public var coverURL: String @NSManaged public var detailURL: String diff --git a/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents b/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents index 0235638a..2edc28d8 100644 --- a/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -25,6 +25,7 @@ + @@ -45,7 +46,7 @@ - + \ No newline at end of file diff --git a/EhPanda/Database/Persistence.swift b/EhPanda/Database/Persistence.swift index 3a35218f..9b1f2dcf 100644 --- a/EhPanda/Database/Persistence.swift +++ b/EhPanda/Database/Persistence.swift @@ -34,41 +34,35 @@ struct PersistenceController { } static func checkExistence( - entityType: MO.Type, - predicate: NSPredicate + entityType: MO.Type, predicate: NSPredicate ) -> Bool { - let request = NSFetchRequest( - entityName: String(describing: entityType) - ) - request.fetchLimit = 1 - request.predicate = predicate - - let context = shared.container.viewContext - let resultCount = (try? context.count(for: request)) ?? 0 - return resultCount > 0 + fetch(entityType: entityType, predicate: predicate) != nil } - static func materializedObject( + static func materializedObjects( in context: NSManagedObjectContext, matching predicate: NSPredicate - ) -> NSManagedObject? { + ) -> [NSManagedObject] { + var objects = [NSManagedObject]() for object in context.registeredObjects where !object.isFault { guard predicate.evaluate(with: object) else { continue } - return object + objects.append(object) } - return nil + return objects } static func fetch( - entityType: MO.Type, predicate: NSPredicate + entityType: MO.Type, predicate: NSPredicate, + findBeforeFetch: Bool = true ) -> MO? { let context = shared.container.viewContext - if let object = materializedObject( - in: context, matching: predicate - ) as? MO { return object } - + if findBeforeFetch { + if let object = materializedObjects( + in: context, matching: predicate + ).first as? MO { return object } + } let request = NSFetchRequest( entityName: String(describing: entityType) ) @@ -77,16 +71,45 @@ struct PersistenceController { return try? context.fetch(request).first } - /// Create one if fetch result is empty, and update it. + static func fetch( + entityType: MO.Type, + predicate: NSPredicate, + sortDescriptors: [NSSortDescriptor]? = nil, + findBeforeFetch: Bool = true + ) -> [MO] { + let context = shared.container.viewContext + if findBeforeFetch { + if let objects = materializedObjects( + in: context, matching: predicate + ) as? [MO] { return objects } + } + let request = NSFetchRequest( + entityName: String(describing: entityType) + ) + request.predicate = predicate + request.sortDescriptors = sortDescriptors + return (try? context.fetch(request)) ?? [] + } + static func update( entityType: MO.Type, gid: String, + createIfNil: Bool = false, commitChanges: ((MO) -> Void) ) { - let storedMO: MO = fetchOrCreate( - entityType: entityType, gid: gid - ) - commitChanges(storedMO) - saveContext() + let storedMO: MO? + if createIfNil { + storedMO = fetchOrCreate( + entityType: entityType, gid: gid + ) + } else { + storedMO = fetch( + entityType: entityType, gid: gid + ) + } + if let storedMO = storedMO { + commitChanges(storedMO) + saveContext() + } } } diff --git a/EhPanda/Database/PersistenceAccessor.swift b/EhPanda/Database/PersistenceAccessor.swift index 3f197ad7..2cb83d51 100644 --- a/EhPanda/Database/PersistenceAccessor.swift +++ b/EhPanda/Database/PersistenceAccessor.swift @@ -44,15 +44,29 @@ extension PersistenceController { entityType: MangaStateMO.self, gid: gid ).toEntity() } + static func fetchMangaHistory() -> [Manga] { + let predicate = NSPredicate(format: "lastOpenDate != nil") + let sortDescriptor = NSSortDescriptor( + keyPath: \MangaMO.lastOpenDate, ascending: false + ) + return PersistenceController.fetch( + entityType: MangaMO.self, + predicate: predicate, + sortDescriptors: [sortDescriptor], + findBeforeFetch: false + ).map({ $0.toEntity() }) + } static func fetch( - entityType: MO.Type, gid: String + entityType: MO.Type, gid: String, + findBeforeFetch: Bool = true ) -> MO? { fetch( entityType: entityType, predicate: NSPredicate( format: "gid == %@", gid - ) + ), + findBeforeFetch: findBeforeFetch ) } static func fetchOrCreate( @@ -103,11 +117,7 @@ extension PersistenceController { saveContext() } - static func update(gid: String, mangaStateMO: ((MangaStateMO) -> Void)) { - update(entityType: MangaStateMO.self, gid: gid, commitChanges: mangaStateMO) - } - - static func hasCached(gid: String) -> Bool { + static func mangaCached(gid: String) -> Bool { PersistenceController.checkExistence( entityType: MangaMO.self, predicate: NSPredicate( @@ -115,6 +125,17 @@ extension PersistenceController { ) ) } + + static func updateLastOpenDate(gid: String) { + update(entityType: MangaMO.self, gid: gid) { mangaMO in + mangaMO.lastOpenDate = Date() + } + } + + // MARK: MangaState + static func update(gid: String, mangaStateMO: ((MangaStateMO) -> Void)) { + update(entityType: MangaStateMO.self, gid: gid, createIfNil: true, commitChanges: mangaStateMO) + } static func update(fetchedState: MangaState) { PersistenceController.update(gid: fetchedState.gid) { mangaStateMO in mangaStateMO.tags = fetchedState.tags.toData() diff --git a/EhPanda/Models/Models.swift b/EhPanda/Models/Models.swift index bd51f372..5930cf9f 100644 --- a/EhPanda/Models/Models.swift +++ b/EhPanda/Models/Models.swift @@ -28,7 +28,6 @@ struct Manga: Identifiable, Codable, Equatable { lhs.gid == rhs.gid } static let empty = Manga( - detail: MangaDetail.empty, gid: "", token: "", title: "", @@ -41,9 +40,6 @@ struct Manga: Identifiable, Codable, Equatable { detailURL: "" ) - var detail: MangaDetail? - var contents: [MangaContent]? - var id: String { gid } let gid: String let token: String @@ -57,7 +53,7 @@ struct Manga: Identifiable, Codable, Equatable { let publishedDate: Date let coverURL: String let detailURL: String - var lastOpenTime: Date? + var lastOpenDate: Date? } struct MangaDetail: Codable { @@ -247,7 +243,7 @@ extension Manga: DateFormattable, CustomStringConvertible { extension MangaDetail: DateFormattable, CustomStringConvertible { var description: String { - "MangaDetail(\(jpnTitle ?? title))" + "MangaDetail(gid: \(gid), \(jpnTitle ?? title))" } var languageAbbr: String { @@ -258,6 +254,13 @@ extension MangaDetail: DateFormattable, CustomStringConvertible { } } +extension MangaState: CustomStringConvertible { + var description: String { + "MangaState(gid: \(gid), tags: \(tags.count)," + + "previews: \(previews.count), comments: \(comments.count))" + } +} + extension MangaContent: CustomStringConvertible { var description: String { "MangaContent(\(tag))" diff --git a/EhPanda/Network/Request.swift b/EhPanda/Network/Request.swift index ff1c6f41..b130780e 100644 --- a/EhPanda/Network/Request.swift +++ b/EhPanda/Network/Request.swift @@ -82,7 +82,6 @@ struct MangaItemReverseRequest { func getManga(from detail: MangaDetail?) -> Manga? { if let detail = detail { return Manga( - detail: detail, gid: gid, token: token, title: detail.title, diff --git a/EhPanda/View/Detail/CommentView.swift b/EhPanda/View/Detail/CommentView.swift index e388a4a7..f38ffaa3 100644 --- a/EhPanda/View/Detail/CommentView.swift +++ b/EhPanda/View/Detail/CommentView.swift @@ -147,7 +147,7 @@ private extension CommentView { func onLinkTap(link: URL) { if isValidDetailURL(url: link) { let gid = link.pathComponents[2] - if PersistenceController.hasCached(gid: gid) { + if PersistenceController.mangaCached(gid: gid) { replaceMangaCommentJumpID(gid: gid) } else { store.dispatch( diff --git a/EhPanda/View/Detail/DetailView.swift b/EhPanda/View/Detail/DetailView.swift index 9e6a03b8..13b2a5af 100644 --- a/EhPanda/View/Detail/DetailView.swift +++ b/EhPanda/View/Detail/DetailView.swift @@ -219,10 +219,8 @@ private extension DetailView { store.dispatch(.fetchMangaDetail(gid: gid)) } func updateHistoryItems() { - DispatchQueue.main.async { - if environment.homeListType != .history { - store.dispatch(.updateHistoryItems(gid: gid)) - } + if environment.homeListType != .history { + PersistenceController.updateLastOpenDate(gid: gid) } } } diff --git a/EhPanda/View/Home/HomeView.swift b/EhPanda/View/Home/HomeView.swift index c9d015e8..7e7dcad2 100644 --- a/EhPanda/View/Home/HomeView.swift +++ b/EhPanda/View/Home/HomeView.swift @@ -131,6 +131,9 @@ struct HomeView: View, StoreAccessor { // MARK: Private Properties private extension HomeView { + var mangaHistory: [Manga] { + PersistenceController.fetchMangaHistory() + } var environmentBinding: Binding { $store.appState.environment } @@ -148,16 +151,6 @@ private extension HomeView { : word.contains(homeInfo.searchKeyword) }) ?? [] } - var historyItems: [Manga] { - var items = homeInfo.historyItems? - .compactMap({ $0.value }) - .filter({ $0.lastOpenTime != nil }) - items?.sort { - $0.lastOpenTime ?? Date() - > $1.lastOpenTime ?? Date() - } - return items ?? [] - } var navigationBarTitle: String { if let user = settings.user, environment.favoritesIndex != -1, @@ -247,10 +240,10 @@ private extension HomeView { NotFoundView(retryAction: nil) case .history: GenericList( - items: historyItems, + items: mangaHistory, setting: setting ?? Setting(), loadingFlag: false, - notFoundFlag: historyItems.isEmpty, + notFoundFlag: mangaHistory.isEmpty, loadFailedFlag: false, moreLoadingFlag: false, moreLoadFailedFlag: false @@ -354,7 +347,7 @@ private extension HomeView { isValidDetailURL(url: link) { let gid = link.pathComponents[2] - if PersistenceController.hasCached(gid: gid) { + if PersistenceController.mangaCached(gid: gid) { replaceMangaCommentJumpID(gid: gid) } else { store.dispatch( diff --git a/EhPanda/View/Setting/SettingView.swift b/EhPanda/View/Setting/SettingView.swift index 8efda561..47306bd5 100644 --- a/EhPanda/View/Setting/SettingView.swift +++ b/EhPanda/View/Setting/SettingView.swift @@ -89,7 +89,6 @@ private extension SettingView { func logout() { clearCookies() clearImageCaches() - store.dispatch(.clearHistoryItems) store.dispatch(.replaceUser(user: nil)) } From 2529dcf02165f8d5e5a5517533029f9ff594bb3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=A4=E3=81=9D=E3=82=99=E3=81=86?= Date: Sat, 10 Jul 2021 23:27:18 +0800 Subject: [PATCH 8/9] feat: Replace JSON-based properties with CoreData --- EhPanda.xcodeproj/project.pbxproj | 20 +-- EhPanda/App/Extensions.swift | 7 +- EhPanda/App/Tools/AppEnvStorage.swift | 48 +++++++ EhPanda/App/Tools/FileHelper.swift | 60 --------- EhPanda/App/Tools/FileStorage.swift | 42 ------ EhPanda/DataFlow/AppCommand.swift | 3 - EhPanda/DataFlow/AppState.swift | 42 +++--- EhPanda/DataFlow/Store.swift | 41 +++--- EhPanda/DataFlow/StoreAccessor.swift | 20 +-- EhPanda/Database/AppEnvMO+CoreDataClass.swift | 42 ++++++ .../AppEnvMO+CoreDataProperties.swift | 19 +++ .../Database/MangaStateMO+CoreDataClass.swift | 11 +- .../Model.xcdatamodel/contents | 7 + EhPanda/Database/Persistence.swift | 77 ++++++++--- EhPanda/Database/PersistenceAccessor.swift | 29 ++-- EhPanda/Models/Filter.swift | 4 +- EhPanda/Models/Models.swift | 2 +- EhPanda/View/Content/ContentView.swift | 4 +- EhPanda/View/Detail/AssociatedView.swift | 2 +- EhPanda/View/Detail/DetailView.swift | 6 +- EhPanda/View/Home/FilterView.swift | 93 +++++++------ EhPanda/View/Home/HomeView.swift | 31 +++-- EhPanda/View/Home/SlideMenu.swift | 12 +- EhPanda/View/Setting/AccountSettingView.swift | 4 +- .../View/Setting/AppearanceSettingView.swift | 126 +++++++++--------- EhPanda/View/Setting/GeneralSettingView.swift | 112 ++++++++-------- EhPanda/View/Setting/ReadingSettingView.swift | 100 +++++++------- 27 files changed, 501 insertions(+), 463 deletions(-) create mode 100644 EhPanda/App/Tools/AppEnvStorage.swift delete mode 100644 EhPanda/App/Tools/FileHelper.swift delete mode 100644 EhPanda/App/Tools/FileStorage.swift create mode 100644 EhPanda/Database/AppEnvMO+CoreDataClass.swift create mode 100644 EhPanda/Database/AppEnvMO+CoreDataProperties.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 4b8d9198..22d1398d 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -14,8 +14,6 @@ AB2CED64268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB2CED63268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift */; }; AB2F72BF2685A8B00088DECA /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = AB2F72BE2685A8B00088DECA /* Kingfisher */; }; AB38A0CB25CA993D00764D64 /* ColorCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB38A0CA25CA993D00764D64 /* ColorCodable.swift */; }; - AB40CFDC25983EC200D1DC9A /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB40CFDB25983EC200D1DC9A /* FileStorage.swift */; }; - AB40CFDF25983EDF00D1DC9A /* FileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB40CFDE25983EDF00D1DC9A /* FileHelper.swift */; }; AB47FD9F25BC81A40007765D /* Normal@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB47FD9925BC81A40007765D /* Normal@3x.png */; }; AB47FDA025BC81A40007765D /* Normal@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB47FD9A25BC81A40007765D /* Normal@2x.png */; }; AB47FDA125BC81A40007765D /* Weird@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB47FD9B25BC81A40007765D /* Weird@3x.png */; }; @@ -27,6 +25,8 @@ AB47FDB525BC859E0007765D /* Weird-ipad-pro@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB47FDB225BC859E0007765D /* Weird-ipad-pro@2x.png */; }; AB47FDB625BC859E0007765D /* Weird-ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = AB47FDB325BC859E0007765D /* Weird-ipad.png */; }; AB4FD2C1268AB83300A95968 /* MangaDetailMO+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4FD2C0268AB83300A95968 /* MangaDetailMO+CoreDataProperties.swift */; }; + AB63EADB2699AC8200090535 /* AppEnvMO+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB63EADA2699AC8200090535 /* AppEnvMO+CoreDataProperties.swift */; }; + AB63EADD2699AC9100090535 /* AppEnvMO+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB63EADC2699AC9100090535 /* AppEnvMO+CoreDataClass.swift */; }; AB6DE897268822390087C579 /* LogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6DE896268822390087C579 /* LogsView.swift */; }; AB6DE89A268861790087C579 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = AB6DE899268861790087C579 /* SwiftyBeaver */; }; AB7E6B3025D24FE00035CC68 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = AB7E6B3225D24FE00035CC68 /* InfoPlist.strings */; }; @@ -49,6 +49,7 @@ ABCD2F0E25976B95008E5A20 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD2F0D25976B95008E5A20 /* Parser.swift */; }; ABD5FDD4263D05110021A4C6 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = ABD5FDD3263D05110021A4C6 /* .swiftlint.yml */; }; ABE0FA2F25D4CAEC00E52F18 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = ABE0FA2E25D4CAEC00E52F18 /* SDWebImageSwiftUI */; }; + ABE2AE752699F238001D47AA /* AppEnvStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABE2AE742699F238001D47AA /* AppEnvStorage.swift */; }; ABEA1FE625A9B40B002966B9 /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABEA1FE525A9B40B002966B9 /* Setting.swift */; }; ABEE0AFA2595C6F800C997AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = ABEE0AFC2595C6F800C997AE /* Localizable.strings */; }; ABF313A525B1AB6600D47A2F /* Misc.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF313A425B1AB6600D47A2F /* Misc.swift */; }; @@ -90,8 +91,6 @@ AB10117F26986C1100C2C1A9 /* MangaStateMO+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaStateMO+CoreDataClass.swift"; sourceTree = ""; }; AB2CED63268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaMO+CoreDataProperties.swift"; sourceTree = ""; }; AB38A0CA25CA993D00764D64 /* ColorCodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorCodable.swift; sourceTree = ""; }; - AB40CFDB25983EC200D1DC9A /* FileStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileStorage.swift; sourceTree = ""; }; - AB40CFDE25983EDF00D1DC9A /* FileHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHelper.swift; sourceTree = ""; }; AB47FD9925BC81A40007765D /* Normal@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Normal@3x.png"; sourceTree = ""; }; AB47FD9A25BC81A40007765D /* Normal@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Normal@2x.png"; sourceTree = ""; }; AB47FD9B25BC81A40007765D /* Weird@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Weird@3x.png"; sourceTree = ""; }; @@ -103,6 +102,8 @@ AB47FDB225BC859E0007765D /* Weird-ipad-pro@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Weird-ipad-pro@2x.png"; sourceTree = ""; }; AB47FDB325BC859E0007765D /* Weird-ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Weird-ipad.png"; sourceTree = ""; }; AB4FD2C0268AB83300A95968 /* MangaDetailMO+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MangaDetailMO+CoreDataProperties.swift"; sourceTree = ""; }; + AB63EADA2699AC8200090535 /* AppEnvMO+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppEnvMO+CoreDataProperties.swift"; sourceTree = ""; }; + AB63EADC2699AC9100090535 /* AppEnvMO+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppEnvMO+CoreDataClass.swift"; sourceTree = ""; }; AB6DE896268822390087C579 /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = ""; }; AB7E6B3125D24FE00035CC68 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; AB7E6B3425D24FE40035CC68 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; @@ -128,6 +129,7 @@ ABCD2F09259763FC008E5A20 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; ABCD2F0D25976B95008E5A20 /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; ABD5FDD3263D05110021A4C6 /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = SOURCE_ROOT; }; + ABE2AE742699F238001D47AA /* AppEnvStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnvStorage.swift; sourceTree = ""; }; ABE9376C265DCD9400EA8B30 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; ABE9376D265DCD9400EA8B30 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; ABEA1FE525A9B40B002966B9 /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; }; @@ -188,8 +190,7 @@ isa = PBXGroup; children = ( ABCD2F0D25976B95008E5A20 /* Parser.swift */, - AB40CFDB25983EC200D1DC9A /* FileStorage.swift */, - AB40CFDE25983EDF00D1DC9A /* FileHelper.swift */, + ABE2AE742699F238001D47AA /* AppEnvStorage.swift */, AB38A0CA25CA993D00764D64 /* ColorCodable.swift */, ); path = Tools; @@ -218,6 +219,8 @@ ABC681F126898D46007BBD69 /* Model.xcdatamodeld */, ABCA93BD26918DE100A98BC6 /* Persistence.swift */, ABCA93C32692A0BF00A98BC6 /* PersistenceAccessor.swift */, + AB63EADC2699AC9100090535 /* AppEnvMO+CoreDataClass.swift */, + AB63EADA2699AC8200090535 /* AppEnvMO+CoreDataProperties.swift */, ABCA93BF2691925900A98BC6 /* MangaMO+CoreDataClass.swift */, AB2CED63268AB6AE003130F7 /* MangaMO+CoreDataProperties.swift */, ABCA93C12691929D00A98BC6 /* MangaDetailMO+CoreDataClass.swift */, @@ -505,14 +508,15 @@ files = ( ABA732D925A8018A00B3D9AB /* Extensions.swift in Sources */, ABF45AF325F3313D00ECB568 /* AccountSettingView.swift in Sources */, - AB40CFDC25983EC200D1DC9A /* FileStorage.swift in Sources */, ABF45ABD25F3312F00ECB568 /* Store.swift in Sources */, ABF45AED25F3313D00ECB568 /* DetailView.swift in Sources */, ABCD2F0A259763FC008E5A20 /* Request.swift in Sources */, ABF45AE925F3313D00ECB568 /* AlertView.swift in Sources */, ABF45ABB25F3312F00ECB568 /* AppError.swift in Sources */, AB10118026986C1100C2C1A9 /* MangaStateMO+CoreDataClass.swift in Sources */, + AB63EADB2699AC8200090535 /* AppEnvMO+CoreDataProperties.swift in Sources */, ABF45AE025F3313D00ECB568 /* HomeView.swift in Sources */, + AB63EADD2699AC9100090535 /* AppEnvMO+CoreDataClass.swift in Sources */, ABCA93BE26918DE100A98BC6 /* Persistence.swift in Sources */, ABF45AEF25F3313D00ECB568 /* TorrentsView.swift in Sources */, ABF45AB925F3312F00ECB568 /* AppState.swift in Sources */, @@ -521,7 +525,7 @@ ABC3C7892593699B00E0C11B /* Defaults.swift in Sources */, ABF45AF125F3313D00ECB568 /* AssociatedView.swift in Sources */, ABF45ADF25F3313D00ECB568 /* FilterView.swift in Sources */, - AB40CFDF25983EDF00D1DC9A /* FileHelper.swift in Sources */, + ABE2AE752699F238001D47AA /* AppEnvStorage.swift in Sources */, ABC3C78F2593699B00E0C11B /* ViewModifiers.swift in Sources */, ABF45AF825F3313D00ECB568 /* EhPandaView.swift in Sources */, ABCA93C42692A0BF00A98BC6 /* PersistenceAccessor.swift in Sources */, diff --git a/EhPanda/App/Extensions.swift b/EhPanda/App/Extensions.swift index 09354cc6..4532b4d0 100644 --- a/EhPanda/App/Extensions.swift +++ b/EhPanda/App/Extensions.swift @@ -50,11 +50,8 @@ extension Encodable { } extension Data { - func toArray() -> [E]? { - try? JSONDecoder().decode([E].self, from: self) - } - func toAspectBox() -> [Int: CGFloat]? { - try? JSONDecoder().decode([Int: CGFloat].self, from: self) + func toObject() -> O? { + try? JSONDecoder().decode(O.self, from: self) } } diff --git a/EhPanda/App/Tools/AppEnvStorage.swift b/EhPanda/App/Tools/AppEnvStorage.swift new file mode 100644 index 00000000..bec87f8c --- /dev/null +++ b/EhPanda/App/Tools/AppEnvStorage.swift @@ -0,0 +1,48 @@ +// +// AppEnvStorage.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/07/10. +// + +import SwiftyBeaver + +@propertyWrapper +struct AppEnvStorage { + private var key: String + + private var appEnv: AppEnv { + PersistenceController.fetchAppEnvNonNil() + } + + var wrappedValue: T { + get { + let mirror = Mirror(reflecting: appEnv) + for child in mirror.children where child.label == key { + if let value = child.value as? T { + return value + } + } + SwiftyBeaver.error( + "Failed in force downcasting to generic type..." + + "Shutting down now..." + ) + fatalError() + } + set { + PersistenceController.update { appEnvMO in + appEnvMO.setValue(newValue.toData(), forKeyPath: key) + } + } + } + + init(type: T.Type, key: String? = nil) { + if let key = key { + self.key = key + } else { + self.key = String( + describing: type + ).lowercased() + } + } +} diff --git a/EhPanda/App/Tools/FileHelper.swift b/EhPanda/App/Tools/FileHelper.swift deleted file mode 100644 index bb0e6d35..00000000 --- a/EhPanda/App/Tools/FileHelper.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// FileHelper.swift -// PokeMaster -// -// Created by 王 巍 on 2019/08/22. -// Copyright © 2019 OneV's Den. All rights reserved. -// - -import Foundation - -enum FileHelper { - static func loadBundledJSON(file: String) -> T { - guard let url = Bundle.main.url(forResource: file, withExtension: "json") else { - fatalError("Resource not found: \(file)") - } - // swiftlint:disable force_try - return try! loadJSON(from: url) - // swiftlint:enable force_try - } - - static func loadJSON(from url: URL) throws -> T { - let data = try Data(contentsOf: url) - return try JSONDecoder().decode(T.self, from: data) - } - - static func loadJSON( - from directory: FileManager.SearchPathDirectory, - fileName: String - ) throws -> T - { - guard let url = FileManager.default.urls(for: directory, in: .userDomainMask).first else { - throw AppError.fileError - } - return try loadJSON(from: url.appendingPathComponent(fileName)) - } - - static func writeJSON(_ value: T, to url: URL) throws { - let data = try JSONEncoder().encode(value) - try data.write(to: url) - } - - static func writeJSON( - _ value: T, - to directory: FileManager.SearchPathDirectory, - fileName: String - ) throws - { - guard let url = FileManager.default.urls(for: directory, in: .userDomainMask).first else { - return - } - try writeJSON(value, to: url.appendingPathComponent(fileName)) - } - - static func delete(from directory: FileManager.SearchPathDirectory, fileName: String) throws { - guard let url = FileManager.default.urls(for: directory, in: .userDomainMask).first else { - return - } - try FileManager.default.removeItem(at: url.appendingPathComponent(fileName)) - } -} diff --git a/EhPanda/App/Tools/FileStorage.swift b/EhPanda/App/Tools/FileStorage.swift deleted file mode 100644 index ffdabb4c..00000000 --- a/EhPanda/App/Tools/FileStorage.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// FileStorage.swift -// PokeMaster -// -// Created by 王 巍 on 2019/08/22. -// Copyright © 2019 OneV's Den. All rights reserved. -// - -import Foundation - -@propertyWrapper -struct FileStorage { - private var value: T? - - private let directory: FileManager.SearchPathDirectory - private let fileName: String - - private let queue = DispatchQueue(label: (UUID().uuidString)) - - init(directory: FileManager.SearchPathDirectory, fileName: String) { - value = try? FileHelper.loadJSON(from: directory, fileName: fileName) - self.directory = directory - self.fileName = fileName - } - - var wrappedValue: T? { - get { value } - - set { - value = newValue - let directory = self.directory - let fileName = self.fileName - queue.async { - if let value = newValue { - try? FileHelper.writeJSON(value, to: directory, fileName: fileName) - } else { - try? FileHelper.delete(from: directory, fileName: fileName) - } - } - } - } -} diff --git a/EhPanda/DataFlow/AppCommand.swift b/EhPanda/DataFlow/AppCommand.swift index e144824a..3fb0c152 100644 --- a/EhPanda/DataFlow/AppCommand.swift +++ b/EhPanda/DataFlow/AppCommand.swift @@ -302,9 +302,6 @@ struct FetchMangaDetailCommand: AppCommand { token.unseal() } receiveValue: { detail in store.dispatch(.fetchMangaDetailDone(result: .success((detail.0, detail.1, detail.2)))) -// if detail.0.previews.isEmpty == true { -// store.dispatch(.fetchAlterImages(gid: gid)) -// } } .seal(in: token) } diff --git a/EhPanda/DataFlow/AppState.swift b/EhPanda/DataFlow/AppState.swift index 2b780108..b2d4a8f8 100644 --- a/EhPanda/DataFlow/AppState.swift +++ b/EhPanda/DataFlow/AppState.swift @@ -46,25 +46,31 @@ extension AppState { var favoriteNamesLoading = false var greetingLoading = false - @FileStorage(directory: .cachesDirectory, fileName: "user.json") - var user: User? - @FileStorage(directory: .cachesDirectory, fileName: "filter.json") - var filter: Filter? - @FileStorage(directory: .cachesDirectory, fileName: "setting.json") - var setting: Setting? + var appEnv: AppEnv { + PersistenceController.fetchAppEnvNonNil() + } + + @AppEnvStorage(type: User.self) + var user: User + + @AppEnvStorage(type: Filter.self) + var filter: Filter + + @AppEnvStorage(type: Setting.self) + var setting: Setting mutating func update(user: User) { if let displayName = user.displayName { - self.user?.displayName = displayName + self.user.displayName = displayName } if let avatarURL = user.avatarURL { - self.user?.avatarURL = avatarURL + self.user.avatarURL = avatarURL } if let currentGP = user.currentGP, let currentCredits = user.currentCredits { - self.user?.currentGP = currentGP - self.user?.currentCredits = currentCredits + self.user.currentGP = currentGP + self.user.currentCredits = currentCredits } } @@ -72,13 +78,13 @@ extension AppState { guard let currDate = greeting.updateTime else { return } - if let prevGreeting = user?.greeting, + if let prevGreeting = user.greeting, let prevDate = prevGreeting.updateTime, prevDate < currDate { - user?.greeting = greeting - } else if user?.greeting == nil { - user?.greeting = greeting + user.greeting = greeting + } else if user.greeting == nil { + user.greeting = greeting } } } @@ -130,8 +136,8 @@ extension AppState { var moreFavoritesLoading = generateBoolDict() var moreFavoritesLoadFailed = generateBoolDict() - @FileStorage(directory: .cachesDirectory, fileName: "historyKeywords.json") - var historyKeywords: [String]? + @AppEnvStorage(type: [String].self, key: "historyKeywords") + var historyKeywords: [String] static func generateBoolDict(defaultValue: Bool = false) -> [Int: Bool] { var tmp = [Int: Bool]() @@ -179,10 +185,6 @@ extension AppState { } mutating func insertHistoryKeyword(text: String) { guard !text.isEmpty else { return } - guard var historyKeywords = historyKeywords else { - historyKeywords = [text] - return - } if let index = historyKeywords.firstIndex(of: text) { if historyKeywords.last != text { diff --git a/EhPanda/DataFlow/Store.swift b/EhPanda/DataFlow/Store.swift index 2249905f..aa18504d 100644 --- a/EhPanda/DataFlow/Store.swift +++ b/EhPanda/DataFlow/Store.swift @@ -41,17 +41,10 @@ final class Store: ObservableObject { switch action { // MARK: App Ops case .replaceUser(let user): - appState.settings.user = user - case .initializeStates: - if appState.settings.user == nil { - appState.settings.user = User() - } - if appState.settings.filter == nil { - appState.settings.filter = Filter() - } - if appState.settings.setting == nil { - appState.settings.setting = Setting() + if let user = user { + appState.settings.user = user } + case .initializeStates: // swiftlint:disable unneeded_break_in_switch break // swiftlint:enable unneeded_break_in_switch @@ -62,13 +55,13 @@ final class Store: ObservableObject { case .saveReadingProgress(let gid, let tag): PersistenceController.update(gid: gid, readingProgress: tag) case .updateDiskImageCacheSize(let size): - appState.settings.setting?.diskImageCacheSize = size + appState.settings.setting.diskImageCacheSize = size case .updateAppIconType(let iconType): - appState.settings.setting?.appIconType = iconType + appState.settings.setting.appIconType = iconType case .updateHistoryKeywords(let text): appState.homeInfo.insertHistoryKeyword(text: text) case .clearHistoryKeywords: - appState.homeInfo.historyKeywords = nil + appState.homeInfo.historyKeywords = [] case .updateSearchKeyword(let text): appState.homeInfo.searchKeyword = text case .updateViewControllersCount: @@ -134,8 +127,8 @@ final class Store: ObservableObject { } case .fetchUserInfo: - guard let uid = appState.settings.user?.apiuid, !uid.isEmpty, - !appState.settings.userInfoLoading + let uid = appState.settings.user.apiuid + guard !uid.isEmpty, !appState.settings.userInfoLoading else { break } appState.settings.userInfoLoading = true @@ -156,7 +149,7 @@ final class Store: ObservableObject { appState.settings.favoriteNamesLoading = false if case .success(let names) = result { - appState.settings.user?.favoriteNames = names + appState.settings.user.favoriteNames = names } case .fetchMangaItemReverse(let detailURL): @@ -185,7 +178,7 @@ final class Store: ObservableObject { appState.homeInfo.searchCurrentPageNum = 0 appState.homeInfo.searchLoading = true - let filter = appState.settings.filter ?? Filter() + let filter = appState.settings.filter appCommand = FetchSearchItemsCommand(keyword: keyword, filter: filter) case .fetchSearchItemsDone(let result): appState.homeInfo.searchLoading = false @@ -221,7 +214,7 @@ final class Store: ObservableObject { if appState.homeInfo.moreSearchLoading { break } appState.homeInfo.moreSearchLoading = true - let filter = appState.settings.filter ?? Filter() + let filter = appState.settings.filter let lastID = appState.homeInfo.searchItems?.last?.id ?? "" let pageNum = appState.homeInfo.searchCurrentPageNum + 1 appCommand = FetchMoreSearchItemsCommand( @@ -493,7 +486,7 @@ final class Store: ObservableObject { switch result { case .success(let detail): if let apikey = detail.2 { - appState.settings.user?.apikey = apikey + appState.settings.user.apikey = apikey } PersistenceController.add(detail: detail.0) PersistenceController.update(fetchedState: detail.1) @@ -689,8 +682,9 @@ final class Store: ObservableObject { appCommand = DeleteFavoriteCommand(gid: gid) case .rate(let gid, let rating): - guard let apiuidString = appState.settings.user?.apiuid, - let apikey = appState.settings.user?.apikey, + let apiuidString = appState.settings.user.apiuid + guard !apiuidString.isEmpty, + let apikey = appState.settings.user.apikey, let token = PersistenceController.fetchManga(gid: gid)?.token, let apiuid = Int(apiuidString), let gid = Int(gid) @@ -721,8 +715,9 @@ final class Store: ObservableObject { detailURL: detailURL ) case .voteComment(let gid, let commentID, let vote): - guard let apiuidString = appState.settings.user?.apiuid, - let apikey = appState.settings.user?.apikey, + let apiuidString = appState.settings.user.apiuid + guard !apiuidString.isEmpty, + let apikey = appState.settings.user.apikey, let token = PersistenceController.fetchManga(gid: gid)?.token, let commentID = Int(commentID), let apiuid = Int(apiuidString), diff --git a/EhPanda/DataFlow/StoreAccessor.swift b/EhPanda/DataFlow/StoreAccessor.swift index 17ae0d7a..0921e2a0 100644 --- a/EhPanda/DataFlow/StoreAccessor.swift +++ b/EhPanda/DataFlow/StoreAccessor.swift @@ -55,35 +55,35 @@ extension StoreAccessor { // MARK: Settings extension StoreAccessor { - var user: User? { + var user: User { settings.user } var currentGP: String? { - user?.currentGP + user.currentGP } var currentCredits: String? { - user?.currentCredits + user.currentCredits } var favoriteNames: [Int: String]? { - user?.favoriteNames + user.favoriteNames } - var setting: Setting? { + var setting: Setting { settings.setting } - var filter: Filter? { + var filter: Filter { settings.filter } var accentColor: Color { - setting?.accentColor ?? .blue + setting.accentColor } var allowsResignActiveBlur: Bool { - setting?.allowsResignActiveBlur ?? true + setting.allowsResignActiveBlur } var autoLockPolicy: AutoLockPolicy { - setting?.autoLockPolicy ?? .never + setting.autoLockPolicy } var detectGalleryFromPasteboard: Bool { - setting?.detectGalleryFromPasteboard ?? false + setting.detectGalleryFromPasteboard } } diff --git a/EhPanda/Database/AppEnvMO+CoreDataClass.swift b/EhPanda/Database/AppEnvMO+CoreDataClass.swift new file mode 100644 index 00000000..ade0d0c9 --- /dev/null +++ b/EhPanda/Database/AppEnvMO+CoreDataClass.swift @@ -0,0 +1,42 @@ +// +// AppEnvMO+CoreDataClass.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/07/10. +// + +import CoreData + +public class AppEnvMO: NSManagedObject {} + +extension AppEnvMO: ManagedObjectProtocol { + func toEntity() -> AppEnv { + AppEnv( + user: user?.toObject() ?? User(), + filter: filter?.toObject() ?? Filter(), + setting: setting?.toObject() ?? Setting(), + historyKeywords: historyKeywords?.toObject() ?? [String]() + ) + } +} + +extension AppEnv: ManagedObjectConvertible { + @discardableResult + func toManagedObject(in context: NSManagedObjectContext) -> AppEnvMO { + let appEnvMO = AppEnvMO(context: context) + + appEnvMO.user = user.toData() + appEnvMO.filter = filter.toData() + appEnvMO.setting = setting.toData() + appEnvMO.historyKeywords = historyKeywords.toData() + + return appEnvMO + } +} + +struct AppEnv: Codable { + let user: User + let filter: Filter + let setting: Setting + let historyKeywords: [String] +} diff --git a/EhPanda/Database/AppEnvMO+CoreDataProperties.swift b/EhPanda/Database/AppEnvMO+CoreDataProperties.swift new file mode 100644 index 00000000..c64a4066 --- /dev/null +++ b/EhPanda/Database/AppEnvMO+CoreDataProperties.swift @@ -0,0 +1,19 @@ +// +// AppEnvMO+CoreDataProperties.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/07/10. +// + +import CoreData + +extension AppEnvMO { + @nonobjc public class func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: "AppEnvMO") + } + + @NSManaged public var user: Data? + @NSManaged public var filter: Data? + @NSManaged public var setting: Data? + @NSManaged public var historyKeywords: Data? +} diff --git a/EhPanda/Database/MangaStateMO+CoreDataClass.swift b/EhPanda/Database/MangaStateMO+CoreDataClass.swift index 14b63204..f926d26e 100644 --- a/EhPanda/Database/MangaStateMO+CoreDataClass.swift +++ b/EhPanda/Database/MangaStateMO+CoreDataClass.swift @@ -5,6 +5,7 @@ // Created by 荒木辰造 on R 3/07/09. // +import SwiftUI import CoreData public class MangaStateMO: NSManagedObject {} @@ -12,13 +13,13 @@ public class MangaStateMO: NSManagedObject {} extension MangaStateMO: ManagedObjectProtocol { func toEntity() -> MangaState { MangaState( - gid: gid, tags: tags?.toArray() ?? [MangaTag](), + gid: gid, tags: tags?.toObject() ?? [MangaTag](), userRating: userRating, currentPageNum: Int(currentPageNum), pageNumMaximum: Int(pageNumMaximum), readingProgress: Int(readingProgress), - previews: previews?.toArray() ?? [MangaPreview](), - comments: comments?.toArray() ?? [MangaComment](), - contents: contents?.toArray() ?? [MangaContent](), - aspectBox: aspectBox?.toAspectBox() ?? [:] + previews: previews?.toObject() ?? [MangaPreview](), + comments: comments?.toObject() ?? [MangaComment](), + contents: contents?.toObject() ?? [MangaContent](), + aspectBox: aspectBox?.toObject() ?? [Int: CGFloat]() ) } } diff --git a/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents b/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents index 2edc28d8..bc797c37 100644 --- a/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/EhPanda/Database/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,5 +1,11 @@ + + + + + + @@ -45,6 +51,7 @@ + diff --git a/EhPanda/Database/Persistence.swift b/EhPanda/Database/Persistence.swift index 9b1f2dcf..54e3ae10 100644 --- a/EhPanda/Database/Persistence.swift +++ b/EhPanda/Database/Persistence.swift @@ -46,7 +46,9 @@ struct PersistenceController { var objects = [NSManagedObject]() for object in context.registeredObjects where !object.isFault { - guard predicate.evaluate(with: object) + guard object.entity.attributesByName + .keys.contains("gid"), + predicate.evaluate(with: object) else { continue } objects.append(object) } @@ -54,43 +56,80 @@ struct PersistenceController { } static func fetch( - entityType: MO.Type, predicate: NSPredicate, + entityType: MO.Type, predicate: NSPredicate? = nil, findBeforeFetch: Bool = true ) -> MO? { - let context = shared.container.viewContext - if findBeforeFetch { - if let object = materializedObjects( - in: context, matching: predicate - ).first as? MO { return object } - } - let request = NSFetchRequest( - entityName: String(describing: entityType) - ) - request.fetchLimit = 1 - request.predicate = predicate - return try? context.fetch(request).first + fetch( + entityType: entityType, fetchLimit: 1, + predicate: predicate, findBeforeFetch: findBeforeFetch + ).first } static func fetch( entityType: MO.Type, - predicate: NSPredicate, - sortDescriptors: [NSSortDescriptor]? = nil, - findBeforeFetch: Bool = true + fetchLimit: Int = 0, + predicate: NSPredicate? = nil, + findBeforeFetch: Bool = true, + sortDescriptors: [NSSortDescriptor]? = nil ) -> [MO] { let context = shared.container.viewContext - if findBeforeFetch { + if findBeforeFetch, let predicate = predicate { if let objects = materializedObjects( in: context, matching: predicate - ) as? [MO] { return objects } + ) as? [MO], !objects.isEmpty { return objects } } let request = NSFetchRequest( entityName: String(describing: entityType) ) request.predicate = predicate + request.fetchLimit = fetchLimit request.sortDescriptors = sortDescriptors return (try? context.fetch(request)) ?? [] } + static func fetchOrCreate( + entityType: MO.Type, predicate: NSPredicate? = nil + ) -> MO { + if let storedMO = fetch( + entityType: entityType, + predicate: predicate + ) { + return storedMO + } else { + let newMO = MO( + context: shared + .container + .viewContext + ) + saveContext() + return newMO + } + } + + static func update( + entityType: MO.Type, + predicate: NSPredicate? = nil, + createIfNil: Bool = false, + commitChanges: ((MO) -> Void) + ) { + let storedMO: MO? + if createIfNil { + storedMO = fetchOrCreate( + entityType: entityType, + predicate: predicate + ) + } else { + storedMO = fetch( + entityType: entityType, + predicate: predicate + ) + } + if let storedMO = storedMO { + commitChanges(storedMO) + saveContext() + } + } + static func update( entityType: MO.Type, gid: String, createIfNil: Bool = false, diff --git a/EhPanda/Database/PersistenceAccessor.swift b/EhPanda/Database/PersistenceAccessor.swift index 2cb83d51..bcd39215 100644 --- a/EhPanda/Database/PersistenceAccessor.swift +++ b/EhPanda/Database/PersistenceAccessor.swift @@ -44,6 +44,9 @@ extension PersistenceController { entityType: MangaStateMO.self, gid: gid ).toEntity() } + static func fetchAppEnvNonNil() -> AppEnv { + PersistenceController.fetchOrCreate(entityType: AppEnvMO.self).toEntity() + } static func fetchMangaHistory() -> [Manga] { let predicate = NSPredicate(format: "lastOpenDate != nil") let sortDescriptor = NSSortDescriptor( @@ -52,8 +55,8 @@ extension PersistenceController { return PersistenceController.fetch( entityType: MangaMO.self, predicate: predicate, - sortDescriptors: [sortDescriptor], - findBeforeFetch: false + findBeforeFetch: false, + sortDescriptors: [sortDescriptor] ).map({ $0.toEntity() }) } @@ -72,14 +75,15 @@ extension PersistenceController { static func fetchOrCreate( entityType: MO.Type, gid: String ) -> MO { - if let storedMO = fetch(entityType: entityType, gid: gid) { - return storedMO - } else { - let newMO = MO(context: shared.container.viewContext) - newMO.gid = gid - saveContext() - return newMO - } + let newMO = fetchOrCreate( + entityType: entityType, + predicate: NSPredicate( + format: "gid == %@", gid + ) + ) + newMO.gid = gid + saveContext() + return newMO } static func add(mangas: [Manga]) { @@ -131,6 +135,9 @@ extension PersistenceController { mangaMO.lastOpenDate = Date() } } + static func update(appEnvMO: ((AppEnvMO) -> Void)) { + update(entityType: AppEnvMO.self, createIfNil: true, commitChanges: appEnvMO) + } // MARK: MangaState static func update(gid: String, mangaStateMO: ((MangaStateMO) -> Void)) { @@ -165,7 +172,7 @@ extension PersistenceController { let newContents = contents.sorted(by: { $0.tag < $1.tag }) var storedContents = mangaStateMO.contents? - .toArray() ?? [MangaContent]() + .toObject() ?? [MangaContent]() if storedContents.isEmpty { mangaStateMO.contents = newContents.toData() diff --git a/EhPanda/Models/Filter.swift b/EhPanda/Models/Filter.swift index 1eeaef5e..e1ed7f9c 100644 --- a/EhPanda/Models/Filter.swift +++ b/EhPanda/Models/Filter.swift @@ -27,14 +27,14 @@ struct Filter: Codable { var onlyWithTorrents = false var lowPowerTags = false { didSet { - if lowPowerTags == true { + if lowPowerTags { downvotedTags = false } } } var downvotedTags = false { didSet { - if downvotedTags == true { + if downvotedTags { lowPowerTags = false } } diff --git a/EhPanda/Models/Models.swift b/EhPanda/Models/Models.swift index 5930cf9f..fbd53eba 100644 --- a/EhPanda/Models/Models.swift +++ b/EhPanda/Models/Models.swift @@ -256,7 +256,7 @@ extension MangaDetail: DateFormattable, CustomStringConvertible { extension MangaState: CustomStringConvertible { var description: String { - "MangaState(gid: \(gid), tags: \(tags.count)," + "MangaState(gid: \(gid), tags: \(tags.count), " + "previews: \(previews.count), comments: \(comments.count))" } } diff --git a/EhPanda/View/Content/ContentView.swift b/EhPanda/View/Content/ContentView.swift index 80e6c5c8..d5d0aa50 100644 --- a/EhPanda/View/Content/ContentView.swift +++ b/EhPanda/View/Content/ContentView.swift @@ -265,7 +265,7 @@ private extension ContentView { // MARK: Gestures func onDoubleTap(value: TapGesture.Value) { set(newOffset: .zero) - set(newScale: scale == 1 ? setting?.doubleTapScaleFactor ?? 2 : 1) + set(newScale: scale == 1 ? setting.doubleTapScaleFactor : 1) } func onDragGestureChanged(value: DragGesture.Value) { if scale > 1 { @@ -321,7 +321,7 @@ private extension ContentView { } } func set(newScale: CGFloat) { - let max = setting?.maximumScaleFactor ?? 3 + let max = setting.maximumScaleFactor guard scale != newScale && newScale >= 1 && newScale <= max else { return } diff --git a/EhPanda/View/Detail/AssociatedView.swift b/EhPanda/View/Detail/AssociatedView.swift index 559252ec..cb573ffb 100644 --- a/EhPanda/View/Detail/AssociatedView.swift +++ b/EhPanda/View/Detail/AssociatedView.swift @@ -33,7 +33,7 @@ struct AssociatedView: View, StoreAccessor { .opacity(0) MangaSummaryRow( manga: manga, - setting: setting ?? Setting() + setting: setting ) } .onAppear { diff --git a/EhPanda/View/Detail/DetailView.swift b/EhPanda/View/Detail/DetailView.swift index 13b2a5af..9271a337 100644 --- a/EhPanda/View/Detail/DetailView.swift +++ b/EhPanda/View/Detail/DetailView.swift @@ -39,9 +39,9 @@ struct DetailView: View, StoreAccessor, PersistenceAccessor { VStack(spacing: 30) { HeaderView( manga: manga, detail: detail, - translateCategory: setting? - .translateCategory != false, - favoriteNames: user?.favoriteNames, + translateCategory: setting + .translateCategory, + favoriteNames: user.favoriteNames, addFavAction: addFavorite, deleteFavAction: deleteFavorite ) diff --git a/EhPanda/View/Home/FilterView.swift b/EhPanda/View/Home/FilterView.swift index 9d30c2ff..bb6ca19b 100644 --- a/EhPanda/View/Home/FilterView.swift +++ b/EhPanda/View/Home/FilterView.swift @@ -12,58 +12,57 @@ struct FilterView: View, StoreAccessor { // MARK: FilterView var body: some View { - if let filter = settings.filter, - let filterBinding = Binding(settingsBinding.filter) { - Form { - Section(header: Text("Basic")) { - CategoryView(filter: filter, filterBinding: filterBinding) - Button(action: onResetButtonTap) { - Text("Reset filters") - .foregroundStyle(.red) - } - Toggle("Advanced settings", isOn: filterBinding.advanced) + let filter = settings.filter + let filterBinding = settingsBinding.filter + Form { + Section(header: Text("Basic")) { + CategoryView(filter: filter, filterBinding: filterBinding) + Button(action: onResetButtonTap) { + Text("Reset filters") + .foregroundStyle(.red) } - Group { - Section(header: Text("Advanced")) { - Toggle("Search gallery name", isOn: filterBinding.galleryName) - Toggle("Search gallery tags", isOn: filterBinding.galleryTags) - Toggle("Search gallery description", isOn: filterBinding.galleryDesc) - Toggle("Search torrent filenames", isOn: filterBinding.torrentFilenames) - Toggle("Only show galleries with torrents", isOn: filterBinding.onlyWithTorrents) - Toggle("Search Low-Power tags", isOn: filterBinding.lowPowerTags) - Toggle("Search downvoted tags", isOn: filterBinding.downvotedTags) - Toggle("Show expunged galleries", isOn: filterBinding.expungedGalleries) - } - Section { - Toggle("Set minimum rating", isOn: filterBinding.minRatingActivated) - MinimumRatingSetter(minimum: filterBinding.minRating) - .disabled(!filter.minRatingActivated) - Toggle("Set pages range", isOn: filterBinding.pageRangeActivated) - PagesRangeSetter( - lowerBound: filterBinding.pageLowerBound, - upperBound: filterBinding.pageUpperBound - ) - .disabled(!filter.pageRangeActivated) - } - Section(header: Text("Default Filter")) { - Toggle("Disable language filter", isOn: filterBinding.disableLanguage) - Toggle("Disable uploader filter", isOn: filterBinding.disableUploader) - Toggle("Disable tags filter", isOn: filterBinding.disableTags) - } - } - .disabled(!filter.advanced) + Toggle("Advanced settings", isOn: filterBinding.advanced) } - .actionSheet(item: environmentBinding.filterViewActionSheetState) { item in - switch item { - case .resetFilters: - return ActionSheet(title: Text("Are you sure to reset?"), buttons: [ - .destructive(Text("Reset"), action: resetFilters), - .cancel() - ]) + Group { + Section(header: Text("Advanced")) { + Toggle("Search gallery name", isOn: filterBinding.galleryName) + Toggle("Search gallery tags", isOn: filterBinding.galleryTags) + Toggle("Search gallery description", isOn: filterBinding.galleryDesc) + Toggle("Search torrent filenames", isOn: filterBinding.torrentFilenames) + Toggle("Only show galleries with torrents", isOn: filterBinding.onlyWithTorrents) + Toggle("Search Low-Power tags", isOn: filterBinding.lowPowerTags) + Toggle("Search downvoted tags", isOn: filterBinding.downvotedTags) + Toggle("Show expunged galleries", isOn: filterBinding.expungedGalleries) + } + Section { + Toggle("Set minimum rating", isOn: filterBinding.minRatingActivated) + MinimumRatingSetter(minimum: filterBinding.minRating) + .disabled(!filter.minRatingActivated) + Toggle("Set pages range", isOn: filterBinding.pageRangeActivated) + PagesRangeSetter( + lowerBound: filterBinding.pageLowerBound, + upperBound: filterBinding.pageUpperBound + ) + .disabled(!filter.pageRangeActivated) + } + Section(header: Text("Default Filter")) { + Toggle("Disable language filter", isOn: filterBinding.disableLanguage) + Toggle("Disable uploader filter", isOn: filterBinding.disableUploader) + Toggle("Disable tags filter", isOn: filterBinding.disableTags) } } - .navigationBarTitle("Filters") + .disabled(!filter.advanced) + } + .actionSheet(item: environmentBinding.filterViewActionSheetState) { item in + switch item { + case .resetFilters: + return ActionSheet(title: Text("Are you sure to reset?"), buttons: [ + .destructive(Text("Reset"), action: resetFilters), + .cancel() + ]) + } } + .navigationBarTitle("Filters") } } diff --git a/EhPanda/View/Home/HomeView.swift b/EhPanda/View/Home/HomeView.swift index 7e7dcad2..b2532d2d 100644 --- a/EhPanda/View/Home/HomeView.swift +++ b/EhPanda/View/Home/HomeView.swift @@ -119,7 +119,7 @@ struct HomeView: View, StoreAccessor { perform: onFavIndexChange ) .onChange( - of: user?.greeting, + of: user.greeting, perform: onReceive ) .onChange( @@ -146,17 +146,16 @@ private extension HomeView { && viewControllersCount == 1 } var suggestions: [String] { - homeInfo.historyKeywords?.reversed().filter({ word in + homeInfo.historyKeywords.reversed().filter({ word in homeInfo.searchKeyword.isEmpty ? true : word.contains(homeInfo.searchKeyword) - }) ?? [] + }) } var navigationBarTitle: String { - if let user = settings.user, - environment.favoritesIndex != -1, + if environment.favoritesIndex != -1, environment.homeListType == .favorites { - return user.getFavNameFrom(index: environment.favoritesIndex) + return settings.user.getFavNameFrom(index: environment.favoritesIndex) } else { return environment.homeListType.rawValue.localized() } @@ -168,7 +167,7 @@ private extension HomeView { case .search: GenericList( items: homeInfo.searchItems, - setting: setting ?? Setting(), + setting: setting, loadingFlag: homeInfo.searchLoading, notFoundFlag: homeInfo.searchNotFound, loadFailedFlag: homeInfo.searchLoadFailed, @@ -180,7 +179,7 @@ private extension HomeView { case .frontpage: GenericList( items: homeInfo.frontpageItems, - setting: setting ?? Setting(), + setting: setting, loadingFlag: homeInfo.frontpageLoading, notFoundFlag: homeInfo.frontpageNotFound, loadFailedFlag: homeInfo.frontpageLoadFailed, @@ -192,7 +191,7 @@ private extension HomeView { case .popular: GenericList( items: homeInfo.popularItems, - setting: setting ?? Setting(), + setting: setting, loadingFlag: homeInfo.popularLoading, notFoundFlag: homeInfo.popularNotFound, loadFailedFlag: homeInfo.popularLoadFailed, @@ -203,7 +202,7 @@ private extension HomeView { case .watched: GenericList( items: homeInfo.watchedItems, - setting: setting ?? Setting(), + setting: setting, loadingFlag: homeInfo.watchedLoading, notFoundFlag: homeInfo.watchedNotFound, loadFailedFlag: homeInfo.watchedLoadFailed, @@ -217,7 +216,7 @@ private extension HomeView { items: homeInfo.favoritesItems[ environment.favoritesIndex ], - setting: setting ?? Setting(), + setting: setting, loadingFlag: homeInfo.favoritesLoading[ environment.favoritesIndex ] ?? false, @@ -241,7 +240,7 @@ private extension HomeView { case .history: GenericList( items: mangaHistory, - setting: setting ?? Setting(), + setting: setting, loadingFlag: false, notFoundFlag: mangaHistory.isEmpty, loadFailedFlag: false, @@ -366,7 +365,7 @@ private extension HomeView { store.dispatch(.replaceMangaCommentJumpID(gid: gid)) } func getPasteboardLinkIfAllowed() -> URL? { - if setting?.allowsDetectionWhenNoChange == true { + if setting.allowsDetectionWhenNoChange { return getPasteboardLink() } else { let currentChangeCount = UIPasteboard.general.changeCount @@ -382,7 +381,7 @@ private extension HomeView { if environment.homeViewSheetState != nil { store.dispatch(.toggleHomeViewSheet(state: nil)) } - if environment.isSlideMenuClosed != true { + if environment.isSlideMenuClosed { postSlideMenuShouldCloseNotification() } } @@ -435,8 +434,8 @@ private extension HomeView { } dispatchMainAsync { - if setting?.showNewDawnGreeting == true { - if let greeting = user?.greeting { + if setting.showNewDawnGreeting { + if let greeting = user.greeting { if verifyDate(with: greeting.updateTime) { store.dispatch(.fetchGreeting) } diff --git a/EhPanda/View/Home/SlideMenu.swift b/EhPanda/View/Home/SlideMenu.swift index 0d779dc9..a3bf9bfe 100644 --- a/EhPanda/View/Home/SlideMenu.swift +++ b/EhPanda/View/Home/SlideMenu.swift @@ -26,8 +26,8 @@ struct SlideMenu: View, StoreAccessor { VStack(spacing: 0) { AvatarView( iconName: iconType.iconName, - avatarURL: user?.avatarURL, - displayName: user?.displayName, + avatarURL: user.avatarURL, + displayName: user.displayName, width: Defaults.ImageSize.avatarW, height: Defaults.ImageSize.avatarH ) @@ -73,9 +73,7 @@ private extension SlideMenu { Defaults.FrameSize.slideMenuWidth } var iconType: IconType { - store.appState - .settings.setting? - .appIconType ?? appIconType + store.appState.settings.setting.appIconType } func onMenuRowTap(item: HomeListType) { @@ -83,7 +81,7 @@ private extension SlideMenu { store.dispatch(.toggleHomeList(type: item)) impactFeedback(style: .soft) - if setting?.closeSlideMenuAfterSelection == true { + if setting.closeSlideMenuAfterSelection { performTransition(offset: -width) } } @@ -92,7 +90,7 @@ private extension SlideMenu { store.dispatch(.toggleHomeViewSheet(state: .setting)) } func onFavoritesIndexChange(_ : Int) { - if setting?.closeSlideMenuAfterSelection == true { + if setting.closeSlideMenuAfterSelection { performTransition(offset: -width) } } diff --git a/EhPanda/View/Setting/AccountSettingView.swift b/EhPanda/View/Setting/AccountSettingView.swift index e5e0b9c2..223fa2f9 100644 --- a/EhPanda/View/Setting/AccountSettingView.swift +++ b/EhPanda/View/Setting/AccountSettingView.swift @@ -93,8 +93,8 @@ struct AccountSettingView: View { } private extension AccountSettingView { - var settingBinding: Binding? { - Binding($store.appState.settings.setting) + var settingBinding: Binding { + $store.appState.settings.setting } // MARK: Cookies Methods diff --git a/EhPanda/View/Setting/AppearanceSettingView.swift b/EhPanda/View/Setting/AppearanceSettingView.swift index e83abde0..b50e72c1 100644 --- a/EhPanda/View/Setting/AppearanceSettingView.swift +++ b/EhPanda/View/Setting/AppearanceSettingView.swift @@ -12,85 +12,79 @@ struct AppearanceSettingView: View, StoreAccessor { @EnvironmentObject var store: Store @State private var isNavLinkActive = false - private var settingBinding: Binding? { - Binding($store.appState.settings.setting) + private var settingBinding: Binding { + $store.appState.settings.setting } private var selectedIcon: IconType { - store.appState - .settings.setting? - .appIconType ?? appIconType + store.appState.settings.setting.appIconType } var body: some View { - if let setting = setting, - let settingBinding = settingBinding - { - NavigationLink( - destination: SelectAppIconView( - selectedIcon: selectedIcon, - selectAction: onIconSelect - ), - isActive: $isNavLinkActive, - label: {} - ) - Form { - Section(header: Text("Global")) { - HStack { - Text("Theme") - Spacer() - Picker( - selection: settingBinding.preferredColorScheme, - label: Text(setting.preferredColorScheme.rawValue.localized()), - content: { - ForEach(PreferredColorScheme.allCases) { colorScheme in - Text(colorScheme.rawValue.localized()).tag(colorScheme) - } + NavigationLink( + destination: SelectAppIconView( + selectedIcon: selectedIcon, + selectAction: onIconSelect + ), + isActive: $isNavLinkActive, + label: {} + ) + Form { + Section(header: Text("Global")) { + HStack { + Text("Theme") + Spacer() + Picker( + selection: settingBinding.preferredColorScheme, + label: Text(setting.preferredColorScheme.rawValue.localized()), + content: { + ForEach(PreferredColorScheme.allCases) { colorScheme in + Text(colorScheme.rawValue.localized()).tag(colorScheme) } - ) - } - .pickerStyle(.menu) - ColorPicker("Tint Color", selection: settingBinding.accentColor) - Button("App Icon", action: onAppIconButtonTap) - .foregroundStyle(.primary).withArrow() - Toggle("Translate category", isOn: settingBinding.translateCategory) - .disabled(Locale.current.languageCode == "en") - } - Section(header: Text("List")) { - Toggle(isOn: settingBinding.showSummaryRowTags) { - HStack { - Text("Show tags in list") - Image(systemName: "exclamationmark.triangle.fill") - .opacity(setting.showSummaryRowTags ? 1 : 0) - .foregroundStyle(.yellow) } - } - Toggle(isOn: settingBinding.summaryRowTagsMaximumActivated) { - Text("Set maximum number of tags") - } - .disabled(!setting.showSummaryRowTags) + ) + } + .pickerStyle(.menu) + ColorPicker("Tint Color", selection: settingBinding.accentColor) + Button("App Icon", action: onAppIconButtonTap) + .foregroundStyle(.primary).withArrow() + Toggle("Translate category", isOn: settingBinding.translateCategory) + .disabled(Locale.current.languageCode == "en") + } + Section(header: Text("List")) { + Toggle(isOn: settingBinding.showSummaryRowTags) { HStack { - Text("Maximum number of tags") - Spacer() - Picker(selection: settingBinding.summaryRowTagsMaximum, - label: Text("\(setting.summaryRowTagsMaximum)") - ) { - let nums = Array(stride( - from: 5, through: 20, by: 5 - )) - ForEach(nums, id: \.self) { num in - Text("\(num)").tag(num) - } + Text("Show tags in list") + Image(systemName: "exclamationmark.triangle.fill") + .opacity(setting.showSummaryRowTags ? 1 : 0) + .foregroundStyle(.yellow) + } + } + Toggle(isOn: settingBinding.summaryRowTagsMaximumActivated) { + Text("Set maximum number of tags") + } + .disabled(!setting.showSummaryRowTags) + HStack { + Text("Maximum number of tags") + Spacer() + Picker(selection: settingBinding.summaryRowTagsMaximum, + label: Text("\(setting.summaryRowTagsMaximum)") + ) { + let nums = Array(stride( + from: 5, through: 20, by: 5 + )) + ForEach(nums, id: \.self) { num in + Text("\(num)").tag(num) } - .pickerStyle(.menu) } - .disabled( - !setting.summaryRowTagsMaximumActivated - || !setting.showSummaryRowTags - ) + .pickerStyle(.menu) } + .disabled( + !setting.summaryRowTagsMaximumActivated + || !setting.showSummaryRowTags + ) } - .navigationBarTitle("Appearance") } + .navigationBarTitle("Appearance") } private func onAppIconButtonTap() { diff --git a/EhPanda/View/Setting/GeneralSettingView.swift b/EhPanda/View/Setting/GeneralSettingView.swift index 2b3fe083..7abcb92e 100644 --- a/EhPanda/View/Setting/GeneralSettingView.swift +++ b/EhPanda/View/Setting/GeneralSettingView.swift @@ -14,76 +14,72 @@ struct GeneralSettingView: View, StoreAccessor { @State private var passcodeNotSet = false var body: some View { - if let setting = setting, - let settingBinding = settingBinding - { - Form { - Section { - HStack { - Text("Language") - Spacer() - Button(language, action: toSettingLanguage) - } - Toggle( - "Close slide menu after selection", - isOn: settingBinding.closeSlideMenuAfterSelection - ) - Toggle( - "Detect link from the clipboard", - isOn: settingBinding.detectGalleryFromPasteboard - ) - Toggle( - "Allows detection even when no change", - isOn: settingBinding.allowsDetectionWhenNoChange - ) - .disabled(!setting.detectGalleryFromPasteboard) + Form { + Section { + HStack { + Text("Language") + Spacer() + Button(language, action: toSettingLanguage) } - Section(header: Text("Security")) { - HStack { - Text("Auto-Lock") - Spacer() - Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.yellow) - .opacity((passcodeNotSet && setting.autoLockPolicy != .never) ? 1 : 0) - Picker( - selection: settingBinding.autoLockPolicy, - label: Text(setting.autoLockPolicy.rawValue.localized()) - ) { - ForEach(AutoLockPolicy.allCases) { policy in - Text(policy.rawValue.localized()).tag(policy) - } + Toggle( + "Close slide menu after selection", + isOn: settingBinding.closeSlideMenuAfterSelection + ) + Toggle( + "Detect link from the clipboard", + isOn: settingBinding.detectGalleryFromPasteboard + ) + Toggle( + "Allows detection even when no change", + isOn: settingBinding.allowsDetectionWhenNoChange + ) + .disabled(!setting.detectGalleryFromPasteboard) + } + Section(header: Text("Security")) { + HStack { + Text("Auto-Lock") + Spacer() + Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.yellow) + .opacity((passcodeNotSet && setting.autoLockPolicy != .never) ? 1 : 0) + Picker( + selection: settingBinding.autoLockPolicy, + label: Text(setting.autoLockPolicy.rawValue.localized()) + ) { + ForEach(AutoLockPolicy.allCases) { policy in + Text(policy.rawValue.localized()).tag(policy) } - .pickerStyle(.menu) } - Toggle( - "App switcher blur", - isOn: settingBinding.allowsResignActiveBlur - ) + .pickerStyle(.menu) } - Section(header: Text("Cache")) { - Button(action: toggleClearImgCaches) { - HStack { - Text("Clear image caches") - Spacer() - Text(setting.diskImageCacheSize) - .foregroundStyle(.tint) - } - .foregroundColor(.primary) + Toggle( + "App switcher blur", + isOn: settingBinding.allowsResignActiveBlur + ) + } + Section(header: Text("Cache")) { + Button(action: toggleClearImgCaches) { + HStack { + Text("Clear image caches") + Spacer() + Text(setting.diskImageCacheSize) + .foregroundStyle(.tint) } + .foregroundColor(.primary) } - Section(header: Text("Advanced")) { - NavigationLink("Logs", destination: LogsView()) - NavigationLink("Filters", destination: FilterView()) - } } - .navigationBarTitle("General") - .task(checkPasscodeExistence) + Section(header: Text("Advanced")) { + NavigationLink("Logs", destination: LogsView()) + NavigationLink("Filters", destination: FilterView()) + } } + .navigationBarTitle("General") + .task(checkPasscodeExistence) } } private extension GeneralSettingView { - var settingBinding: Binding? { - Binding($store.appState.settings.setting) + var settingBinding: Binding { + $store.appState.settings.setting } var language: String { if let code = Locale.current.languageCode, diff --git a/EhPanda/View/Setting/ReadingSettingView.swift b/EhPanda/View/Setting/ReadingSettingView.swift index c7c07b92..92b0ebeb 100644 --- a/EhPanda/View/Setting/ReadingSettingView.swift +++ b/EhPanda/View/Setting/ReadingSettingView.swift @@ -10,66 +10,62 @@ import SwiftUI struct ReadingSettingView: View, StoreAccessor { @EnvironmentObject var store: Store - private var settingBinding: Binding? { - Binding($store.appState.settings.setting) + private var settingBinding: Binding { + $store.appState.settings.setting } var body: some View { - if let setting = setting, - let settingBinding = settingBinding - { - Form { - Section { - HStack { - let time = " times".localized() - Text("Retry limit") - Spacer() - Picker( - selection: settingBinding.contentRetryLimit, - label: Text("\(setting.contentRetryLimit)" + time), - content: { - Text("5" + time).tag(5) - Text("10" + time).tag(10) - Text("15" + time).tag(15) - Text("20" + time).tag(20) - } - ) - .pickerStyle(.menu) - } - } - Section(header: Text("Appearance")) { - HStack { - Text("Separator height") - Spacer() - Picker( - selection: settingBinding.contentDividerHeight, - label: Text("\(Int(setting.contentDividerHeight))pt"), - content: { - Text("0pt").tag(CGFloat(0)) - Text("5pt").tag(CGFloat(5)) - Text("10pt").tag(CGFloat(10)) - Text("15pt").tag(CGFloat(15)) - Text("20pt").tag(CGFloat(20)) - } - ) - .pickerStyle(.menu) - } - ScaleFactorRow( - scaleFactor: settingBinding.maximumScaleFactor, - labelContent: "Maximum scale factor", - minFactor: 1.5, - maxFactor: 10 + Form { + Section { + HStack { + let time = " times".localized() + Text("Retry limit") + Spacer() + Picker( + selection: settingBinding.contentRetryLimit, + label: Text("\(setting.contentRetryLimit)" + time), + content: { + Text("5" + time).tag(5) + Text("10" + time).tag(10) + Text("15" + time).tag(15) + Text("20" + time).tag(20) + } ) - ScaleFactorRow( - scaleFactor: settingBinding.doubleTapScaleFactor, - labelContent: "Double tap scale factor", - minFactor: 1.5, - maxFactor: 5 + .pickerStyle(.menu) + } + } + Section(header: Text("Appearance")) { + HStack { + Text("Separator height") + Spacer() + Picker( + selection: settingBinding.contentDividerHeight, + label: Text("\(Int(setting.contentDividerHeight))pt"), + content: { + Text("0pt").tag(CGFloat(0)) + Text("5pt").tag(CGFloat(5)) + Text("10pt").tag(CGFloat(10)) + Text("15pt").tag(CGFloat(15)) + Text("20pt").tag(CGFloat(20)) + } ) + .pickerStyle(.menu) } + ScaleFactorRow( + scaleFactor: settingBinding.maximumScaleFactor, + labelContent: "Maximum scale factor", + minFactor: 1.5, + maxFactor: 10 + ) + ScaleFactorRow( + scaleFactor: settingBinding.doubleTapScaleFactor, + labelContent: "Double tap scale factor", + minFactor: 1.5, + maxFactor: 5 + ) } - .navigationBarTitle("Reading") } + .navigationBarTitle("Reading") } } From 8e8425651ceaf51747e84d7c82c7c2e7f9565982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=A4=E3=81=9D=E3=82=99=E3=81=86?= Date: Sat, 10 Jul 2021 23:55:49 +0800 Subject: [PATCH 9/9] fix: Typos & bugs --- EhPanda/App/EhPandaApp.swift | 9 ++++----- EhPanda/App/Tools/AppEnvStorage.swift | 2 +- EhPanda/DataFlow/AppAction.swift | 3 +-- EhPanda/DataFlow/Store.swift | 8 +------- EhPanda/Database/PersistenceAccessor.swift | 1 - EhPanda/View/Setting/SettingView.swift | 2 +- 6 files changed, 8 insertions(+), 17 deletions(-) diff --git a/EhPanda/App/EhPandaApp.swift b/EhPanda/App/EhPandaApp.swift index 2e007400..a7f12004 100644 --- a/EhPanda/App/EhPandaApp.swift +++ b/EhPanda/App/EhPandaApp.swift @@ -33,19 +33,18 @@ struct EhPandaApp: App { } private extension EhPandaApp { - var setting: Setting? { + var setting: Setting { store.appState.settings.setting } - var accentColor: Color? { - setting?.accentColor + var accentColor: Color { + setting.accentColor } var preferredColorScheme: ColorScheme? { - setting?.colorScheme ?? .none + setting.colorScheme } func onStartTasks() { DispatchQueue.main.async { - store.dispatch(.initializeStates) store.dispatch(.fetchFavoriteNames) store.dispatch(.fetchUserInfo) } diff --git a/EhPanda/App/Tools/AppEnvStorage.swift b/EhPanda/App/Tools/AppEnvStorage.swift index bec87f8c..d3d6db42 100644 --- a/EhPanda/App/Tools/AppEnvStorage.swift +++ b/EhPanda/App/Tools/AppEnvStorage.swift @@ -8,7 +8,7 @@ import SwiftyBeaver @propertyWrapper -struct AppEnvStorage { +struct AppEnvStorage { private var key: String private var appEnv: AppEnv { diff --git a/EhPanda/DataFlow/AppAction.swift b/EhPanda/DataFlow/AppAction.swift index aff194f5..1c01c3f8 100644 --- a/EhPanda/DataFlow/AppAction.swift +++ b/EhPanda/DataFlow/AppAction.swift @@ -10,8 +10,7 @@ import Kanna import Foundation enum AppAction { - case replaceUser(user: User?) - case initializeStates + case replaceUser(user: User) case initializeFilter case clearDetailViewCommentContent case clearCommentViewCommentContent diff --git a/EhPanda/DataFlow/Store.swift b/EhPanda/DataFlow/Store.swift index aa18504d..7f56dc50 100644 --- a/EhPanda/DataFlow/Store.swift +++ b/EhPanda/DataFlow/Store.swift @@ -41,13 +41,7 @@ final class Store: ObservableObject { switch action { // MARK: App Ops case .replaceUser(let user): - if let user = user { - appState.settings.user = user - } - case .initializeStates: - // swiftlint:disable unneeded_break_in_switch - break - // swiftlint:enable unneeded_break_in_switch + appState.settings.user = user case .initializeFilter: appState.settings.filter = Filter() case .saveAspectBox(let gid, let box): diff --git a/EhPanda/Database/PersistenceAccessor.swift b/EhPanda/Database/PersistenceAccessor.swift index bcd39215..6f4c88dd 100644 --- a/EhPanda/Database/PersistenceAccessor.swift +++ b/EhPanda/Database/PersistenceAccessor.swift @@ -82,7 +82,6 @@ extension PersistenceController { ) ) newMO.gid = gid - saveContext() return newMO } diff --git a/EhPanda/View/Setting/SettingView.swift b/EhPanda/View/Setting/SettingView.swift index 47306bd5..e79017a7 100644 --- a/EhPanda/View/Setting/SettingView.swift +++ b/EhPanda/View/Setting/SettingView.swift @@ -89,7 +89,7 @@ private extension SettingView { func logout() { clearCookies() clearImageCaches() - store.dispatch(.replaceUser(user: nil)) + store.dispatch(.replaceUser(user: User())) } func calculateDiskCachesSize() {