From 0bb29cb9eddf82558096ea7bb65c1b4da0ce697d Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sun, 10 Nov 2024 11:02:17 +0800 Subject: [PATCH] Update dependencies --- EhPanda.xcodeproj/project.pbxproj | 48 +- .../xcshareddata/swiftpm/Package.resolved | 70 +-- .../App/Tools/Clients/AppDelegateClient.swift | 6 +- .../Tools/Clients/AuthorizationClient.swift | 6 +- .../App/Tools/Clients/ClipboardClient.swift | 10 +- EhPanda/App/Tools/Clients/CookieClient.swift | 12 +- EhPanda/App/Tools/Clients/DFClient.swift | 4 +- .../App/Tools/Clients/DatabaseClient.swift | 10 +- EhPanda/App/Tools/Clients/DeviceClient.swift | 10 +- EhPanda/App/Tools/Clients/FileClient.swift | 10 +- EhPanda/App/Tools/Clients/HapticsClient.swift | 6 +- EhPanda/App/Tools/Clients/ImageClient.swift | 10 +- EhPanda/App/Tools/Clients/LibraryClient.swift | 12 +- EhPanda/App/Tools/Clients/LoggerClient.swift | 6 +- .../Tools/Clients/UIApplicationClient.swift | 12 +- EhPanda/App/Tools/Clients/URLClient.swift | 8 +- .../Tools/Clients/UserDefaultsClient.swift | 4 +- EhPanda/App/Tools/EquatableVoid.swift | 22 + .../Tools/Extensions/Reducer_Extension.swift | 10 +- .../SwiftUINavigation_Extension.swift | 36 +- EhPanda/App/Tools/IdentifiableBox.swift | 21 + .../App/Tools/Utilities/MarkdownUtil.swift | 146 ++++- EhPanda/DataFlow/AppDelegateReducer.swift | 2 +- EhPanda/DataFlow/AppReducer.swift | 18 +- EhPanda/DataFlow/AppRouteReducer.swift | 10 +- EhPanda/Models/Persistent/Greeting.swift | 4 +- EhPanda/Models/Support/Misc.swift | 2 + .../Detail/Archives/ArchivesReducer.swift | 1 + .../View/Detail/Archives/ArchivesView.swift | 23 +- .../Detail/Comments/CommentsReducer.swift | 4 +- .../View/Detail/Comments/CommentsView.swift | 6 +- EhPanda/View/Detail/DetailReducer.swift | 588 +++++++++--------- .../DetailSearch/DetailSearchReducer.swift | 14 +- .../DetailSearch/DetailSearchView.swift | 12 +- EhPanda/View/Detail/DetailView.swift | 159 ++--- .../GalleryInfos/GalleryInfosReducer.swift | 1 + .../GalleryInfos/GalleryInfosView.swift | 2 +- .../Detail/Previews/PreviewsReducer.swift | 9 +- .../View/Detail/Previews/PreviewsView.swift | 15 +- .../Detail/Torrents/TorrentsReducer.swift | 4 +- .../View/Detail/Torrents/TorrentsView.swift | 11 +- EhPanda/View/Favorites/FavoritesReducer.swift | 8 +- EhPanda/View/Favorites/FavoritesView.swift | 8 +- .../Home/Frontpage/FrontpageReducer.swift | 8 +- .../View/Home/Frontpage/FrontpageView.swift | 8 +- .../View/Home/History/HistoryReducer.swift | 2 +- EhPanda/View/Home/History/HistoryView.swift | 6 +- EhPanda/View/Home/HomeReducer.swift | 12 +- EhPanda/View/Home/HomeView.swift | 12 +- .../View/Home/Popular/PopularReducer.swift | 10 +- EhPanda/View/Home/Popular/PopularView.swift | 8 +- .../View/Home/Toplists/ToplistsReducer.swift | 4 +- EhPanda/View/Home/Toplists/ToplistsView.swift | 4 +- .../View/Home/Watched/WatchedReducer.swift | 14 +- EhPanda/View/Home/Watched/WatchedView.swift | 12 +- EhPanda/View/Migration/MigrationView.swift | 4 +- EhPanda/View/Reading/ReadingReducer.swift | 12 +- EhPanda/View/Reading/ReadingView.swift | 238 +++---- EhPanda/View/Search/SearchReducer.swift | 14 +- EhPanda/View/Search/SearchRootReducer.swift | 16 +- EhPanda/View/Search/SearchRootView.swift | 23 +- EhPanda/View/Search/SearchView.swift | 12 +- .../View/Search/Support/QuickSearchView.swift | 6 +- .../AccountSettingReducer.swift | 8 +- .../AccountSetting/AccountSettingView.swift | 13 +- .../AppearanceSettingReducer.swift | 1 + .../AppearanceSettingView.swift | 2 +- .../Setting/EhSetting/EhSettingReducer.swift | 2 +- .../Setting/EhSetting/EhSettingView.swift | 6 +- .../GeneralSettingReducer.swift | 3 +- .../GeneralSetting/GeneralSettingView.swift | 6 +- EhPanda/View/Setting/Login/LoginReducer.swift | 3 +- EhPanda/View/Setting/Login/LoginView.swift | 13 +- EhPanda/View/Setting/Logs/LogsView.swift | 6 +- EhPanda/View/Setting/SettingReducer.swift | 6 +- EhPanda/View/Setting/SettingView.swift | 12 +- .../View/Support/Components/GenericList.swift | 11 +- .../Support/Components/TagCloudView.swift | 4 +- EhPanda/View/Support/FiltersReducer.swift | 1 + EhPanda/View/Support/FiltersView.swift | 3 +- EhPanda/View/TabBar/TabBarView.swift | 10 +- 81 files changed, 1126 insertions(+), 789 deletions(-) create mode 100644 EhPanda/App/Tools/EquatableVoid.swift create mode 100644 EhPanda/App/Tools/IdentifiableBox.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index aa595113..6d4b9ec9 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -279,6 +279,8 @@ EA0C925E2C3EB49500D211F6 /* README.jpn.md in Resources */ = {isa = PBXBuildFile; fileRef = EA0C92582C3EB49500D211F6 /* README.jpn.md */; }; EA2E2E7F2A1F7E500038A261 /* SettingReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2E2E7E2A1F7E500038A261 /* SettingReducer.swift */; }; EA2E2E822A1FA1060038A261 /* SearchReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2E2E812A1FA1050038A261 /* SearchReducer.swift */; }; + EA698C032CCDD2FB0058BC19 /* EquatableVoid.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA698C022CCDD2FB0058BC19 /* EquatableVoid.swift */; }; + EA698C092CCDE7090058BC19 /* IdentifiableBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA698C082CCDE7050058BC19 /* IdentifiableBox.swift */; }; EAE63E2129E2A6330048C601 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = EAE63E2029E2A6330048C601 /* SwiftyBeaver */; }; /* End PBXBuildFile section */ @@ -602,6 +604,8 @@ EA0C92582C3EB49500D211F6 /* README.jpn.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.jpn.md; path = READMEs/README.jpn.md; sourceTree = ""; }; EA2E2E7E2A1F7E500038A261 /* SettingReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingReducer.swift; sourceTree = ""; }; EA2E2E812A1FA1050038A261 /* SearchReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchReducer.swift; sourceTree = ""; }; + EA698C022CCDD2FB0058BC19 /* EquatableVoid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EquatableVoid.swift; sourceTree = ""; }; + EA698C082CCDE7050058BC19 /* IdentifiableBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableBox.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -870,6 +874,8 @@ ABCD2F0D25976B95008E5A20 /* Parser.swift */, ABC3C76D2593699A00E0C11B /* Defaults.swift */, AB38A0CA25CA993D00764D64 /* ColorCodable.swift */, + EA698C022CCDD2FB0058BC19 /* EquatableVoid.swift */, + EA698C082CCDE7050058BC19 /* IdentifiableBox.swift */, ABBB2670279AFA61007B6149 /* EnvironmentKeys.swift */, ); path = Tools; @@ -1652,7 +1658,7 @@ AB17573E27678B3400FD64E2 /* XCRemoteSwiftPackageReference "UIImageColors" */, ABD49D5B277C6C9D003D1A07 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, AB86AC0E27831AD100E61E6A /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, - ABBB2634278FB888007B6149 /* XCRemoteSwiftPackageReference "swiftui-navigation" */, + ABBB2634278FB888007B6149 /* XCRemoteSwiftPackageReference "swift-navigation" */, AB26F59727ACDB4200AB3468 /* XCRemoteSwiftPackageReference "FilePicker" */, AB1FA94727C62BC80063EF55 /* XCRemoteSwiftPackageReference "SwiftCommonMark" */, AB2EB99D280251D600011A8A /* XCRemoteSwiftPackageReference "TTProgressHUD" */, @@ -1860,6 +1866,7 @@ ABF45AF625F3313D00ECB568 /* AppearanceSettingView.swift in Sources */, AB7BF2CE27AA3E58001865A3 /* AppUtil.swift in Sources */, AB86AC1A2785C2B300E61E6A /* HomeReducer.swift in Sources */, + EA698C032CCDD2FB0058BC19 /* EquatableVoid.swift in Sources */, AB7BF2D427AA3F12001865A3 /* CookieUtil.swift in Sources */, AB7BF30A27ABDFF1001865A3 /* CoreDataMigrationStep.swift in Sources */, AB69CB8226B3DAF400699359 /* ControlPanel.swift in Sources */, @@ -1943,6 +1950,7 @@ AB86AC0A2782FAFA00E61E6A /* AppearanceSettingReducer.swift in Sources */, AB7BF2C427A9683F001865A3 /* GalleryArchive.swift in Sources */, ABF45AF525F3313D00ECB568 /* ReadingSettingView.swift in Sources */, + EA698C092CCDE7090058BC19 /* IdentifiableBox.swift in Sources */, AB0CFBD727C3B2D0004BD372 /* TagDetailView.swift in Sources */, AB38A0CB25CA993D00764D64 /* ColorCodable.swift in Sources */, ABC732C127B8962000D47DA9 /* LiveTextHandler.swift in Sources */, @@ -2404,7 +2412,7 @@ /* Begin XCRemoteSwiftPackageReference section */ AB17573B27675B1E00FD64E2 /* XCRemoteSwiftPackageReference "Colorful" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Co2333/Colorful.git"; + repositoryURL = "https://github.com/Co2333/Colorful"; requirement = { kind = exactVersion; version = 1.0.1; @@ -2412,7 +2420,7 @@ }; AB17573E27678B3400FD64E2 /* XCRemoteSwiftPackageReference "UIImageColors" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/jathu/UIImageColors.git"; + repositoryURL = "https://github.com/jathu/UIImageColors"; requirement = { kind = upToNextMajorVersion; minimumVersion = 2.0.0; @@ -2420,7 +2428,7 @@ }; AB1FA94727C62BC80063EF55 /* XCRemoteSwiftPackageReference "SwiftCommonMark" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/gonzalezreal/SwiftCommonMark.git"; + repositoryURL = "https://github.com/gonzalezreal/SwiftCommonMark"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.0.0; @@ -2428,7 +2436,7 @@ }; AB26F59727ACDB4200AB3468 /* XCRemoteSwiftPackageReference "FilePicker" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/markrenaud/FilePicker.git"; + repositoryURL = "https://github.com/markrenaud/FilePicker"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.0.0; @@ -2436,7 +2444,7 @@ }; AB2EB99D280251D600011A8A /* XCRemoteSwiftPackageReference "TTProgressHUD" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/EhPanda-Team/TTProgressHUD.git"; + repositoryURL = "https://github.com/EhPanda-Team/TTProgressHUD"; requirement = { branch = custom; kind = branch; @@ -2444,7 +2452,7 @@ }; AB2EB9A0280251F600011A8A /* XCRemoteSwiftPackageReference "AlertKit" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/EhPanda-Team/AlertKit.git"; + repositoryURL = "https://github.com/EhPanda-Team/AlertKit"; requirement = { branch = custom; kind = branch; @@ -2452,7 +2460,7 @@ }; AB2EB9A32802521700011A8A /* XCRemoteSwiftPackageReference "DeprecatedAPI" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/EhPanda-Team/DeprecatedAPI.git"; + repositoryURL = "https://github.com/EhPanda-Team/DeprecatedAPI"; requirement = { branch = main; kind = branch; @@ -2460,7 +2468,7 @@ }; AB60D0E7274C7ECE00F899AB /* XCRemoteSwiftPackageReference "WaterfallGrid" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/paololeonardi/WaterfallGrid.git"; + repositoryURL = "https://github.com/paololeonardi/WaterfallGrid"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.0.0; @@ -2468,7 +2476,7 @@ }; AB65059E26B0027800F91E9D /* XCRemoteSwiftPackageReference "SwiftUIPager" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/fermoya/SwiftUIPager.git"; + repositoryURL = "https://github.com/fermoya/SwiftUIPager"; requirement = { kind = upToNextMajorVersion; minimumVersion = 2.0.0; @@ -2476,7 +2484,7 @@ }; AB86AC0E27831AD100E61E6A /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture.git"; + repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.11.2; @@ -2484,23 +2492,23 @@ }; ABAC82FC26BC4866009F5026 /* XCRemoteSwiftPackageReference "SwiftyOpenCC" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/ddddxxx/SwiftyOpenCC.git"; + repositoryURL = "https://github.com/ddddxxx/SwiftyOpenCC"; requirement = { kind = upToNextMajorVersion; minimumVersion = "2.0.0-beta"; }; }; - ABBB2634278FB888007B6149 /* XCRemoteSwiftPackageReference "swiftui-navigation" */ = { + ABBB2634278FB888007B6149 /* XCRemoteSwiftPackageReference "swift-navigation" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pointfreeco/swiftui-navigation.git"; + repositoryURL = "https://github.com/pointfreeco/swift-navigation"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + minimumVersion = 2.0.0; }; }; ABC4A0772751B40E00968A4F /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/onevcat/Kingfisher.git"; + repositoryURL = "https://github.com/onevcat/Kingfisher"; requirement = { kind = upToNextMajorVersion; minimumVersion = 7.0.0; @@ -2508,7 +2516,7 @@ }; ABD49D5B277C6C9D003D1A07 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols.git"; + repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols"; requirement = { kind = upToNextMajorVersion; minimumVersion = 4.0.0; @@ -2516,7 +2524,7 @@ }; ABD7005726B1C31500DC59C9 /* XCRemoteSwiftPackageReference "Kanna" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/tid-kijyun/Kanna.git"; + repositoryURL = "https://github.com/tid-kijyun/Kanna"; requirement = { kind = upToNextMajorVersion; minimumVersion = 5.0.0; @@ -2524,7 +2532,7 @@ }; EAE63E1F29E2A6330048C601 /* XCRemoteSwiftPackageReference "SwiftyBeaver" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SwiftyBeaver/SwiftyBeaver.git"; + repositoryURL = "https://github.com/SwiftyBeaver/SwiftyBeaver"; requirement = { kind = upToNextMajorVersion; minimumVersion = 2.0.0; @@ -2590,7 +2598,7 @@ }; ABBB2635278FB888007B6149 /* SwiftUINavigation */ = { isa = XCSwiftPackageProductDependency; - package = ABBB2634278FB888007B6149 /* XCRemoteSwiftPackageReference "swiftui-navigation" */; + package = ABBB2634278FB888007B6149 /* XCRemoteSwiftPackageReference "swift-navigation" */; productName = SwiftUINavigation; }; ABC4A0782751B40E00968A4F /* Kingfisher */ = { diff --git a/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cf894c66..3c41b161 100644 --- a/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "93e2ed5e29c203f999b061bca8bfe01e97d8d679589199b88cec774e80de86b8", + "originHash" : "d8869a11d1f8a8b3fb0742cb58c2f9108c13dc9a2b1b7c7665e58ff59cab2596", "pins" : [ { "identity" : "alertkit", @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version" : "1.0.0" + "revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13", + "version" : "1.0.2" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "b871e5ed11a23e52c2896a92ce2c829982ff8619", - "version" : "1.4.2" + "revision" : "bc92c4b27f9a84bfb498cdbfdf35d5a357e9161f", + "version" : "1.5.6" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", - "version" : "1.0.2" + "revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53", + "version" : "1.0.5" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "ee97538f5b81ae89698fd95938896dec5217b148", - "version" : "1.1.1" + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" } }, { @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", "state" : { - "revision" : "1f952d8c69ace5e53bb69a218e6ed00e03a4695c", - "version" : "1.11.2" + "revision" : "56149436a3c3dbf605a89a204aaa904de8ba4580", + "version" : "1.15.2" } }, { @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", - "version" : "1.1.0" + "revision" : "6054df64b55186f08b6d0fd87152081b8ad8d613", + "version" : "1.2.0" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", - "version" : "1.3.0" + "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", + "version" : "1.3.3" } }, { @@ -132,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "00bc30ca03f98881329fab7f1bebef8eba472596", - "version" : "1.3.1" + "revision" : "0fc0255e780bf742abeef29dec80924f5f0ae7b9", + "version" : "1.4.1" } }, { @@ -145,22 +145,31 @@ "version" : "1.1.0" } }, + { + "identity" : "swift-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-navigation", + "state" : { + "revision" : "16a27ab7ae0abfefbbcba73581b3e2380b47a579", + "version" : "2.2.2" + } + }, { "identity" : "swift-perception", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "d3ab98dc2887d1cc3bed676f6fa354da4cb22b3c", - "version" : "1.2.4" + "revision" : "bc67aa8e461351c97282c2419153757a446ae1c9", + "version" : "1.3.5" } }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax", + "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { @@ -172,15 +181,6 @@ "version" : "1.0.0" } }, - { - "identity" : "swiftui-navigation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swiftui-navigation.git", - "state" : { - "revision" : "b7c9a79f6f6b1fefb87d3e5a83a9c2fe7cdc9720", - "version" : "1.5.0" - } - }, { "identity" : "swiftuipager", "kind" : "remoteSourceControl", @@ -231,8 +231,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/paololeonardi/WaterfallGrid.git", "state" : { - "revision" : "944aa82832ed5a9eaaf50862cdd53e3c10ab55eb", - "version" : "1.0.1" + "revision" : "c7c08652c3540adf8e48409c351879b4caea7e89", + "version" : "1.1.0" } }, { @@ -240,8 +240,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", - "version" : "1.1.2" + "revision" : "770f990d3e4eececb57ac04a6076e22f8c97daeb", + "version" : "1.4.2" } } ], diff --git a/EhPanda/App/Tools/Clients/AppDelegateClient.swift b/EhPanda/App/Tools/Clients/AppDelegateClient.swift index 41626c76..4c7c7521 100644 --- a/EhPanda/App/Tools/Clients/AppDelegateClient.swift +++ b/EhPanda/App/Tools/Clients/AppDelegateClient.swift @@ -56,8 +56,10 @@ extension AppDelegateClient { setOrientationMask: { _ in } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - setOrientation: XCTestDynamicOverlay.unimplemented("\(Self.self).setOrientation"), - setOrientationMask: XCTestDynamicOverlay.unimplemented("\(Self.self).setOrientationMask") + setOrientation: IssueReporting.unimplemented(placeholder: placeholder()), + setOrientationMask: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/AuthorizationClient.swift b/EhPanda/App/Tools/Clients/AuthorizationClient.swift index 62599839..b5ad3097 100644 --- a/EhPanda/App/Tools/Clients/AuthorizationClient.swift +++ b/EhPanda/App/Tools/Clients/AuthorizationClient.swift @@ -58,8 +58,10 @@ extension AuthorizationClient { localAuthroize: { _ in false } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - passcodeNotSet: XCTestDynamicOverlay.unimplemented("\(Self.self).passcodeNotSet"), - localAuthroize: XCTestDynamicOverlay.unimplemented("\(Self.self).localAuthroize") + passcodeNotSet: IssueReporting.unimplemented(placeholder: placeholder()), + localAuthroize: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/ClipboardClient.swift b/EhPanda/App/Tools/Clients/ClipboardClient.swift index 0b4a312d..5cbf527b 100644 --- a/EhPanda/App/Tools/Clients/ClipboardClient.swift +++ b/EhPanda/App/Tools/Clients/ClipboardClient.swift @@ -68,10 +68,12 @@ extension ClipboardClient { saveImage: { _, _ in } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - url: XCTestDynamicOverlay.unimplemented("\(Self.self).url"), - changeCount: XCTestDynamicOverlay.unimplemented("\(Self.self).changeCount"), - saveText: XCTestDynamicOverlay.unimplemented("\(Self.self).saveText"), - saveImage: XCTestDynamicOverlay.unimplemented("\(Self.self).saveImage") + url: IssueReporting.unimplemented(placeholder: placeholder()), + changeCount: IssueReporting.unimplemented(placeholder: placeholder()), + saveText: IssueReporting.unimplemented(placeholder: placeholder()), + saveImage: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/CookieClient.swift b/EhPanda/App/Tools/Clients/CookieClient.swift index 5f091819..82209a27 100644 --- a/EhPanda/App/Tools/Clients/CookieClient.swift +++ b/EhPanda/App/Tools/Clients/CookieClient.swift @@ -272,11 +272,13 @@ extension CookieClient { initializeCookie: { _, _ in .init() } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - clearAll: XCTestDynamicOverlay.unimplemented("\(Self.self).clearAll"), - getCookie: XCTestDynamicOverlay.unimplemented("\(Self.self).getCookie"), - removeCookie: XCTestDynamicOverlay.unimplemented("\(Self.self).removeCookie"), - checkExistence: XCTestDynamicOverlay.unimplemented("\(Self.self).checkExistence"), - initializeCookie: XCTestDynamicOverlay.unimplemented("\(Self.self).initializeCookie") + clearAll: IssueReporting.unimplemented(placeholder: placeholder()), + getCookie: IssueReporting.unimplemented(placeholder: placeholder()), + removeCookie: IssueReporting.unimplemented(placeholder: placeholder()), + checkExistence: IssueReporting.unimplemented(placeholder: placeholder()), + initializeCookie: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/DFClient.swift b/EhPanda/App/Tools/Clients/DFClient.swift index 449b9aae..b1be5960 100644 --- a/EhPanda/App/Tools/Clients/DFClient.swift +++ b/EhPanda/App/Tools/Clients/DFClient.swift @@ -49,7 +49,9 @@ extension DFClient { setActive: { _ in } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - setActive: XCTestDynamicOverlay.unimplemented("\(Self.self).setActive") + setActive: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/DatabaseClient.swift b/EhPanda/App/Tools/Clients/DatabaseClient.swift index 80f9b6a8..dacabc28 100644 --- a/EhPanda/App/Tools/Clients/DatabaseClient.swift +++ b/EhPanda/App/Tools/Clients/DatabaseClient.swift @@ -496,10 +496,12 @@ extension DatabaseClient { materializedObjects: { _, _ in .init() } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - prepareDatabase: XCTestDynamicOverlay.unimplemented("\(Self.self).prepareDatabase"), - dropDatabase: XCTestDynamicOverlay.unimplemented("\(Self.self).dropDatabase"), - saveContext: XCTestDynamicOverlay.unimplemented("\(Self.self).saveContext"), - materializedObjects: XCTestDynamicOverlay.unimplemented("\(Self.self).materializedObjects") + prepareDatabase: IssueReporting.unimplemented(placeholder: placeholder()), + dropDatabase: IssueReporting.unimplemented(placeholder: placeholder()), + saveContext: IssueReporting.unimplemented(placeholder: placeholder()), + materializedObjects: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/DeviceClient.swift b/EhPanda/App/Tools/Clients/DeviceClient.swift index 6e660117..f35ab2e5 100644 --- a/EhPanda/App/Tools/Clients/DeviceClient.swift +++ b/EhPanda/App/Tools/Clients/DeviceClient.swift @@ -55,10 +55,12 @@ extension DeviceClient { touchPoint: { .zero } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - isPad: XCTestDynamicOverlay.unimplemented("\(Self.self).isPad"), - absWindowW: XCTestDynamicOverlay.unimplemented("\(Self.self).absWindowW"), - absWindowH: XCTestDynamicOverlay.unimplemented("\(Self.self).absWindowH"), - touchPoint: XCTestDynamicOverlay.unimplemented("\(Self.self).touchPoint") + isPad: IssueReporting.unimplemented(placeholder: placeholder()), + absWindowW: IssueReporting.unimplemented(placeholder: placeholder()), + absWindowH: IssueReporting.unimplemented(placeholder: placeholder()), + touchPoint: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/FileClient.swift b/EhPanda/App/Tools/Clients/FileClient.swift index 5e15dad1..b4160ea0 100644 --- a/EhPanda/App/Tools/Clients/FileClient.swift +++ b/EhPanda/App/Tools/Clients/FileClient.swift @@ -115,10 +115,12 @@ extension FileClient { importTagTranslator: { _ in .success(.init()) } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - createFile: XCTestDynamicOverlay.unimplemented("\(Self.self).createFile"), - fetchLogs: XCTestDynamicOverlay.unimplemented("\(Self.self).fetchLogs"), - deleteLog: XCTestDynamicOverlay.unimplemented("\(Self.self).deleteLog"), - importTagTranslator: XCTestDynamicOverlay.unimplemented("\(Self.self).importTagTranslator") + createFile: IssueReporting.unimplemented(placeholder: placeholder()), + fetchLogs: IssueReporting.unimplemented(placeholder: placeholder()), + deleteLog: IssueReporting.unimplemented(placeholder: placeholder()), + importTagTranslator: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/HapticsClient.swift b/EhPanda/App/Tools/Clients/HapticsClient.swift index 4fe2a7d5..f71bfe75 100644 --- a/EhPanda/App/Tools/Clients/HapticsClient.swift +++ b/EhPanda/App/Tools/Clients/HapticsClient.swift @@ -45,8 +45,10 @@ extension HapticsClient { generateNotificationFeedback: { _ in } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - generateFeedback: XCTestDynamicOverlay.unimplemented("\(Self.self).generateFeedback"), - generateNotificationFeedback: XCTestDynamicOverlay.unimplemented("\(Self.self).generateNotificationFeedback") + generateFeedback: IssueReporting.unimplemented(placeholder: placeholder()), + generateNotificationFeedback: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/ImageClient.swift b/EhPanda/App/Tools/Clients/ImageClient.swift index 4ebd0766..4037d1aa 100644 --- a/EhPanda/App/Tools/Clients/ImageClient.swift +++ b/EhPanda/App/Tools/Clients/ImageClient.swift @@ -116,10 +116,12 @@ extension ImageClient { retrieveImage: { _ in .success(UIImage()) } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - prefetchImages: XCTestDynamicOverlay.unimplemented("\(Self.self).prefetchImages"), - saveImageToPhotoLibrary: XCTestDynamicOverlay.unimplemented("\(Self.self).saveImageToPhotoLibrary"), - downloadImage: XCTestDynamicOverlay.unimplemented("\(Self.self).downloadImage"), - retrieveImage: XCTestDynamicOverlay.unimplemented("\(Self.self).retrieveImage") + prefetchImages: IssueReporting.unimplemented(placeholder: placeholder()), + saveImageToPhotoLibrary: IssueReporting.unimplemented(placeholder: placeholder()), + downloadImage: IssueReporting.unimplemented(placeholder: placeholder()), + retrieveImage: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/LibraryClient.swift b/EhPanda/App/Tools/Clients/LibraryClient.swift index ca18d4cb..327ff0c6 100644 --- a/EhPanda/App/Tools/Clients/LibraryClient.swift +++ b/EhPanda/App/Tools/Clients/LibraryClient.swift @@ -101,12 +101,14 @@ extension LibraryClient { calculateWebImageDiskCacheSize: { .none } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - initializeLogger: XCTestDynamicOverlay.unimplemented("\(Self.self).initializeLogger"), - initializeWebImage: XCTestDynamicOverlay.unimplemented("\(Self.self).initializeWebImage"), - clearWebImageDiskCache: XCTestDynamicOverlay.unimplemented("\(Self.self).clearWebImageDiskCache"), - analyzeImageColors: XCTestDynamicOverlay.unimplemented("\(Self.self).analyzeImageColors"), + initializeLogger: IssueReporting.unimplemented(placeholder: placeholder()), + initializeWebImage: IssueReporting.unimplemented(placeholder: placeholder()), + clearWebImageDiskCache: IssueReporting.unimplemented(placeholder: placeholder()), + analyzeImageColors: IssueReporting.unimplemented(placeholder: placeholder()), calculateWebImageDiskCacheSize: - XCTestDynamicOverlay.unimplemented("\(Self.self).calculateWebImageDiskCacheSize") + IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/LoggerClient.swift b/EhPanda/App/Tools/Clients/LoggerClient.swift index b1bfc6ad..3712d615 100644 --- a/EhPanda/App/Tools/Clients/LoggerClient.swift +++ b/EhPanda/App/Tools/Clients/LoggerClient.swift @@ -44,8 +44,10 @@ extension LoggerClient { error: { _, _ in } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - info: XCTestDynamicOverlay.unimplemented("\(Self.self).info"), - error: XCTestDynamicOverlay.unimplemented("\(Self.self).error") + info: IssueReporting.unimplemented(placeholder: placeholder()), + error: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/UIApplicationClient.swift b/EhPanda/App/Tools/Clients/UIApplicationClient.swift index 13846577..3f75ad67 100644 --- a/EhPanda/App/Tools/Clients/UIApplicationClient.swift +++ b/EhPanda/App/Tools/Clients/UIApplicationClient.swift @@ -85,11 +85,13 @@ extension UIApplicationClient { setUserInterfaceStyle: { _ in } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - openURL: XCTestDynamicOverlay.unimplemented("\(Self.self).openURL"), - hideKeyboard: XCTestDynamicOverlay.unimplemented("\(Self.self).hideKeyboard"), - alternateIconName: XCTestDynamicOverlay.unimplemented("\(Self.self).alternateIconName"), - setAlternateIconName: XCTestDynamicOverlay.unimplemented("\(Self.self).importTagTranslator"), - setUserInterfaceStyle: XCTestDynamicOverlay.unimplemented("\(Self.self).setUserInterfaceStyle") + openURL: IssueReporting.unimplemented(placeholder: placeholder()), + hideKeyboard: IssueReporting.unimplemented(placeholder: placeholder()), + alternateIconName: IssueReporting.unimplemented(placeholder: placeholder()), + setAlternateIconName: IssueReporting.unimplemented(placeholder: placeholder()), + setUserInterfaceStyle: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/URLClient.swift b/EhPanda/App/Tools/Clients/URLClient.swift index 30ea498c..62b01c36 100644 --- a/EhPanda/App/Tools/Clients/URLClient.swift +++ b/EhPanda/App/Tools/Clients/URLClient.swift @@ -90,9 +90,11 @@ extension URLClient { parseGalleryID: { _ in .init() } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - checkIfHandleable: XCTestDynamicOverlay.unimplemented("\(Self.self).checkIfHandleable"), - checkIfMPVURL: XCTestDynamicOverlay.unimplemented("\(Self.self).checkIfMPVURL"), - parseGalleryID: XCTestDynamicOverlay.unimplemented("\(Self.self).parseGalleryID") + checkIfHandleable: IssueReporting.unimplemented(placeholder: placeholder()), + checkIfMPVURL: IssueReporting.unimplemented(placeholder: placeholder()), + parseGalleryID: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/Clients/UserDefaultsClient.swift b/EhPanda/App/Tools/Clients/UserDefaultsClient.swift index 4ae104be..72ba48d3 100644 --- a/EhPanda/App/Tools/Clients/UserDefaultsClient.swift +++ b/EhPanda/App/Tools/Clients/UserDefaultsClient.swift @@ -44,7 +44,9 @@ extension UserDefaultsClient { setValue: { _, _ in } ) + static func placeholder() -> Result { fatalError() } + static let unimplemented: Self = .init( - setValue: XCTestDynamicOverlay.unimplemented("\(Self.self).setValue") + setValue: IssueReporting.unimplemented(placeholder: placeholder()) ) } diff --git a/EhPanda/App/Tools/EquatableVoid.swift b/EhPanda/App/Tools/EquatableVoid.swift new file mode 100644 index 00000000..c2a7942b --- /dev/null +++ b/EhPanda/App/Tools/EquatableVoid.swift @@ -0,0 +1,22 @@ +// +// EquatableVoid.swift +// EhPanda +// +// Created by Chihchy on 2024/10/27. +// + +import Foundation + +public struct EquatableVoid: Hashable, Sendable, Identifiable { + public let id: UUID + + public init(id: UUID = .init()) { + self.id = id + } +} + +private let uniqueID = UUID() + +public extension EquatableVoid { + static let unique = Self(id: uniqueID) +} diff --git a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift index e09111c3..8ce8ea12 100644 --- a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift +++ b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift @@ -11,24 +11,24 @@ import ComposableArchitecture extension Reducer { func haptics( unwrapping enum: @escaping (State) -> Enum?, - case casePath: AnyCasePath, + case caseKeyPath: CaseKeyPath, hapticsClient: HapticsClient, style: UIImpactFeedbackGenerator.FeedbackStyle = .light ) -> some Reducer { - onBecomeNonNil(unwrapping: `enum`, case: casePath) { _, _ in + onBecomeNonNil(unwrapping: `enum`, case: caseKeyPath) { _, _ in .run(operation: { _ in hapticsClient.generateFeedback(style) }) } } private func onBecomeNonNil( unwrapping enum: @escaping (State) -> Enum?, - case casePath: AnyCasePath, + case caseKeyPath: CaseKeyPath, perform additionalEffects: @escaping (inout State, Action) -> Effect ) -> some Reducer { Reduce { state, action in - let previousCase = Binding.constant(`enum`(state)).case(casePath).wrappedValue + let previousCase = Binding.constant(`enum`(state)).case(caseKeyPath).wrappedValue let effects = reduce(into: &state, action: action) - let currentCase = Binding.constant(`enum`(state)).case(casePath).wrappedValue + let currentCase = Binding.constant(`enum`(state)).case(caseKeyPath).wrappedValue return previousCase == nil && currentCase != nil ? .merge(effects, additionalEffects(&state, action)) diff --git a/EhPanda/App/Tools/Extensions/SwiftUINavigation_Extension.swift b/EhPanda/App/Tools/Extensions/SwiftUINavigation_Extension.swift index 8f7bf195..79100afe 100644 --- a/EhPanda/App/Tools/Extensions/SwiftUINavigation_Extension.swift +++ b/EhPanda/App/Tools/Extensions/SwiftUINavigation_Extension.swift @@ -23,11 +23,11 @@ extension NavigationLink { } init( unwrapping enum: Binding, - case casePath: AnyCasePath, + case caseKeyPath: CaseKeyPath, @ViewBuilder destination: @escaping (Binding) -> WrappedDestination ) where Destination == WrappedDestination?, Label == Text { self.init( - "", unwrapping: `enum`.case(casePath), + "", unwrapping: `enum`.case(caseKeyPath), destination: destination ) } @@ -37,11 +37,11 @@ extension View { func confirmationDialog( message: String, unwrapping enum: Binding, - case casePath: AnyCasePath, + case caseKeyPath: CaseKeyPath, @ViewBuilder actions: @escaping (Case) -> A ) -> some View { self.confirmationDialog( - item: `enum`.case(casePath), + item: `enum`.case(caseKeyPath), titleVisibility: .hidden, title: { _ in Text("") }, actions: actions, @@ -51,13 +51,13 @@ extension View { func confirmationDialog( message: String, unwrapping enum: Binding, - case casePath: AnyCasePath, + case caseKeyPath: CaseKeyPath, matching case: Case, @ViewBuilder actions: @escaping (Case) -> A ) -> some View { self.confirmationDialog( item: { - let unwrapping = `enum`.case(casePath) + let unwrapping = `enum`.case(caseKeyPath) let isMatched = `case` == unwrapping.wrappedValue return isMatched ? unwrapping : .constant(nil) }(), @@ -68,15 +68,26 @@ extension View { ) } + func sheet( + unwrapping enum: Binding, + case caseKeyPath: CaseKeyPath, + @ViewBuilder content: @escaping (Case) -> Content + ) -> some View { + self.sheet( + isPresented: .constant(`enum`.case(caseKeyPath).wrappedValue != nil), + content: { `enum`.case(caseKeyPath).wrappedValue.map(content) } + ) + } + func progressHUD( config: TTProgressHUDConfig, unwrapping enum: Binding, - case casePath: AnyCasePath + case caseKeyPath: CaseKeyPath ) -> some View { ZStack { self TTProgressHUD( - `enum`.case(casePath).isRemovedDuplicatesPresent(), + `enum`.case(caseKeyPath).isRemovedDuplicatesPresent(), config: config ) } @@ -84,6 +95,15 @@ extension View { } extension Binding { + func `case`(_ caseKeyPath: CaseKeyPath) -> Binding where Value == Enum? { + .init( + get: { self.wrappedValue.flatMap(AnyCasePath(caseKeyPath).extract(from:)) }, + set: { newValue, transaction in + self.transaction(transaction).wrappedValue = newValue.map(AnyCasePath(caseKeyPath).embed) + } + ) + } + func isRemovedDuplicatesPresent() -> Binding where Value == Wrapped? { .init( get: { wrappedValue != nil }, diff --git a/EhPanda/App/Tools/IdentifiableBox.swift b/EhPanda/App/Tools/IdentifiableBox.swift new file mode 100644 index 00000000..2a758f8b --- /dev/null +++ b/EhPanda/App/Tools/IdentifiableBox.swift @@ -0,0 +1,21 @@ +// +// IdentifiableBox.swift +// EhPanda +// +// Created by Chihchy on 2024/10/27. +// + +import Foundation + +public struct IdentifiableBox: Identifiable { + public let id = UUID() + public let wrappedValue: Value + + public init(value: Value) { + self.wrappedValue = value + } +} + +extension IdentifiableBox: Hashable where Value: Hashable {} +extension IdentifiableBox: Sendable where Value: Sendable {} +extension IdentifiableBox: Equatable where Value: Equatable {} diff --git a/EhPanda/App/Tools/Utilities/MarkdownUtil.swift b/EhPanda/App/Tools/Utilities/MarkdownUtil.swift index 11e501b4..4283f596 100644 --- a/EhPanda/App/Tools/Utilities/MarkdownUtil.swift +++ b/EhPanda/App/Tools/Utilities/MarkdownUtil.swift @@ -12,24 +12,24 @@ import Foundation struct MarkdownUtil { static func parseTexts(markdown: String) -> [String] { (try? Document(markdown: markdown))?.blocks - .compactMap((/Block.paragraph).extract) + .compactMap({ $0[case: \.paragraph] }) .flatMap(\.text) - .compactMap((/Inline.text)) + .compactMap({ $0[case: \.text] }) ?? [] } static func parseLinks(markdown: String) -> [URL] { (try? Document(markdown: markdown))?.blocks - .compactMap((/Block.paragraph).extract) + .compactMap({ $0[case: \.paragraph] }) .flatMap(\.text) - .compactMap((/Inline.link)) + .compactMap({ $0[case: \.link] }) .compactMap(\.url) ?? [] } static func parseImages(markdown: String) -> [URL] { (try? Document(markdown: markdown))?.blocks - .compactMap((/Block.paragraph).extract) + .compactMap({ $0[case: \.paragraph] }) .flatMap(\.text) - .compactMap((/Inline.image)) + .compactMap({ $0[case: \.image] }) .compactMap { image in if image.url?.absoluteString.isValidURL == true { return image.url @@ -41,3 +41,137 @@ struct MarkdownUtil { ?? [] } } + +// MARK: CasePathable +extension Block: @retroactive CasePathable, @retroactive CasePathIterable { + public struct AllCasePaths: CasePathReflectable, Sendable { + public subscript(root: Block) -> PartialCaseKeyPath { + switch root { + case .blockQuote: \.blockQuote + case .bulletList: \.bulletList + case .orderedList: \.orderedList + case .code: \.code + case .html: \.html + case .paragraph: \.paragraph + case .heading: \.heading + case .thematicBreak: \.thematicBreak + } + } + + // swiftlint:disable line_length + public var blockQuote: AnyCasePath { + AnyCasePath(embed: { .blockQuote($0) }, extract: { if case .blockQuote(let value) = $0 { return value } else { return nil }}) + } + public var bulletList: AnyCasePath { + AnyCasePath(embed: { .bulletList($0) }, extract: { if case .bulletList(let value) = $0 { return value } else { return nil }}) + } + public var orderedList: AnyCasePath { + AnyCasePath(embed: { .orderedList($0) }, extract: { if case .orderedList(let value) = $0 { return value } else { return nil }}) + } + public var code: AnyCasePath { + AnyCasePath(embed: { .code($0) }, extract: { if case .code(let value) = $0 { return value } else { return nil }}) + } + public var html: AnyCasePath { + AnyCasePath(embed: { .html($0) }, extract: { if case .html(let value) = $0 { return value } else { return nil }}) + } + public var paragraph: AnyCasePath { + AnyCasePath(embed: { .paragraph($0) }, extract: { if case .paragraph(let value) = $0 { return value } else { return nil }}) + } + public var heading: AnyCasePath { + AnyCasePath(embed: { .heading($0) }, extract: { if case .heading(let value) = $0 { return value } else { return nil }}) + } + public var thematicBreak: AnyCasePath { + AnyCasePath(embed: { .thematicBreak }, extract: { if case .thematicBreak = $0 { return () } else { return nil }}) + } + // swiftlint:enable line_length + } + + public static var allCasePaths: AllCasePaths { + AllCasePaths() + } +} + +extension Block.AllCasePaths: Sequence { + public func makeIterator() -> some IteratorProtocol> { + [ + \.blockQuote, + \.bulletList, + \.orderedList, + \.code, + \.html, + \.paragraph, + \.heading, + \.thematicBreak + ] + .makeIterator() + } +} + +extension Inline: @retroactive CasePathable, @retroactive CasePathIterable { + public struct AllCasePaths: CasePathReflectable, Sendable { + public subscript(root: Inline) -> PartialCaseKeyPath { + switch root { + case .text: \.text + case .softBreak: \.softBreak + case .lineBreak: \.lineBreak + case .code: \.code + case .html: \.html + case .emphasis: \.emphasis + case .strong: \.strong + case .link: \.link + case .image: \.image + } + } + + // swiftlint:disable line_length + public var text: AnyCasePath { + AnyCasePath(embed: { .text($0) }, extract: { if case .text(let value) = $0 { return value } else { return nil }}) + } + public var softBreak: AnyCasePath { + AnyCasePath(embed: { .softBreak }, extract: { if case .softBreak = $0 { return () } else { return nil }}) + } + public var lineBreak: AnyCasePath { + AnyCasePath(embed: { .lineBreak }, extract: { if case .lineBreak = $0 { return () } else { return nil }}) + } + public var code: AnyCasePath { + AnyCasePath(embed: { .code($0) }, extract: { if case .code(let value) = $0 { return value } else { return nil }}) + } + public var html: AnyCasePath { + AnyCasePath(embed: { .html($0) }, extract: { if case .html(let value) = $0 { return value } else { return nil }}) + } + public var emphasis: AnyCasePath { + AnyCasePath(embed: { .emphasis($0) }, extract: { if case .emphasis(let value) = $0 { return value } else { return nil }}) + } + public var strong: AnyCasePath { + AnyCasePath(embed: { .strong($0) }, extract: { if case .strong(let value) = $0 { return value } else { return nil }}) + } + public var link: AnyCasePath { + AnyCasePath(embed: { .link($0) }, extract: { if case .link(let value) = $0 { return value } else { return nil }}) + } + public var image: AnyCasePath { + AnyCasePath(embed: { .image($0) }, extract: { if case .image(let value) = $0 { return value } else { return nil }}) + } + // swiftlint:enable line_length + } + + public static var allCasePaths: AllCasePaths { + AllCasePaths() + } +} + +extension Inline.AllCasePaths: Sequence { + public func makeIterator() -> some IteratorProtocol> { + [ + \.text, + \.softBreak, + \.lineBreak, + \.code, + \.html, + \.emphasis, + \.strong, + \.link, + \.image + ] + .makeIterator() + } +} diff --git a/EhPanda/DataFlow/AppDelegateReducer.swift b/EhPanda/DataFlow/AppDelegateReducer.swift index 7142bde8..6e06aaf6 100644 --- a/EhPanda/DataFlow/AppDelegateReducer.swift +++ b/EhPanda/DataFlow/AppDelegateReducer.swift @@ -49,7 +49,7 @@ struct AppDelegateReducer { } } - Scope(state: \.migrationState, action: /Action.migration, child: MigrationReducer.init) + Scope(state: \.migrationState, action: \.migration, child: MigrationReducer.init) } } diff --git a/EhPanda/DataFlow/AppReducer.swift b/EhPanda/DataFlow/AppReducer.swift index 5dcc39dc..ec163e3e 100644 --- a/EhPanda/DataFlow/AppReducer.swift +++ b/EhPanda/DataFlow/AppReducer.swift @@ -142,7 +142,7 @@ struct AppReducer { } } if type == .setting && deviceClient.isPad() { - effects.append(.send(.appRoute(.setNavigation(.setting)))) + effects.append(.send(.appRoute(.setNavigation(.setting())))) } return effects.isEmpty ? .none : .merge(effects) @@ -196,14 +196,14 @@ struct AppReducer { } } - Scope(state: \.appRouteState, action: /Action.appRoute, child: AppRouteReducer.init) - Scope(state: \.appLockState, action: /Action.appLock, child: AppLockReducer.init) - Scope(state: \.appDelegateState, action: /Action.appDelegate, child: AppDelegateReducer.init) - Scope(state: \.tabBarState, action: /Action.tabBar, child: TabBarReducer.init) - Scope(state: \.homeState, action: /Action.home, child: HomeReducer.init) - Scope(state: \.favoritesState, action: /Action.favorites, child: FavoritesReducer.init) - Scope(state: \.searchRootState, action: /Action.searchRoot, child: SearchRootReducer.init) - Scope(state: \.settingState, action: /Action.setting, child: SettingReducer.init) + Scope(state: \.appRouteState, action: \.appRoute, child: AppRouteReducer.init) + Scope(state: \.appLockState, action: \.appLock, child: AppLockReducer.init) + Scope(state: \.appDelegateState, action: \.appDelegate, child: AppDelegateReducer.init) + Scope(state: \.tabBarState, action: \.tabBar, child: TabBarReducer.init) + Scope(state: \.homeState, action: \.home, child: HomeReducer.init) + Scope(state: \.favoritesState, action: \.favorites, child: FavoritesReducer.init) + Scope(state: \.searchRootState, action: \.searchRoot, child: SearchRootReducer.init) + Scope(state: \.settingState, action: \.setting, child: SettingReducer.init) } } } diff --git a/EhPanda/DataFlow/AppRouteReducer.swift b/EhPanda/DataFlow/AppRouteReducer.swift index 20ed7f8d..34cd7b34 100644 --- a/EhPanda/DataFlow/AppRouteReducer.swift +++ b/EhPanda/DataFlow/AppRouteReducer.swift @@ -14,7 +14,7 @@ struct AppRouteReducer { @CasePathable enum Route: Equatable, Hashable { case hud - case setting + case setting(EquatableVoid = .init()) case detail(String) case newDawn(Greeting) } @@ -124,7 +124,7 @@ struct AppRouteReducer { effects.append( .run { send in try await Task.sleep(for: .milliseconds(500)) - await send(.detail(.setNavigation(.reading))) + await send(.detail(.setNavigation(.reading()))) } ) } else if let commentID = commentID { @@ -182,15 +182,15 @@ struct AppRouteReducer { } .haptics( unwrapping: \.route, - case: /Route.newDawn, + case: \.newDawn, hapticsClient: hapticsClient ) .haptics( unwrapping: \.route, - case: /Route.detail, + case: \.detail, hapticsClient: hapticsClient ) - Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: \.detail, child: DetailReducer.init) } } diff --git a/EhPanda/Models/Persistent/Greeting.swift b/EhPanda/Models/Persistent/Greeting.swift index c9070daf..882de5cb 100644 --- a/EhPanda/Models/Persistent/Greeting.swift +++ b/EhPanda/Models/Persistent/Greeting.swift @@ -7,7 +7,7 @@ import Foundation -struct Greeting: Codable, Equatable, Hashable { +struct Greeting: Codable, Equatable, Hashable, Identifiable { static let mock: Self = { var greeting = Greeting() greeting.gainedEXP = 10 @@ -17,6 +17,8 @@ struct Greeting: Codable, Equatable, Hashable { return greeting }() + var id = UUID() + var gainedEXP: Int? var gainedCredits: Int? var gainedGP: Int? diff --git a/EhPanda/Models/Support/Misc.swift b/EhPanda/Models/Support/Misc.swift index d84560c0..80336b86 100644 --- a/EhPanda/Models/Support/Misc.swift +++ b/EhPanda/Models/Support/Misc.swift @@ -5,6 +5,7 @@ // Created by 荒木辰造 on R 3/01/15. // +import CasePaths import Foundation import SwiftyBeaver @@ -50,6 +51,7 @@ struct QuickSearchWord: Codable, Equatable, Identifiable { var content: String } +@dynamicMemberLookup @CasePathable enum LoadingState: Equatable, Hashable { case idle case loading diff --git a/EhPanda/View/Detail/Archives/ArchivesReducer.swift b/EhPanda/View/Detail/Archives/ArchivesReducer.swift index e9acdd37..024221d6 100644 --- a/EhPanda/View/Detail/Archives/ArchivesReducer.swift +++ b/EhPanda/View/Detail/Archives/ArchivesReducer.swift @@ -11,6 +11,7 @@ import ComposableArchitecture @Reducer struct ArchivesReducer { + @CasePathable enum Route { case messageHUD case communicatingHUD diff --git a/EhPanda/View/Detail/Archives/ArchivesView.swift b/EhPanda/View/Detail/Archives/ArchivesView.swift index f8b5388b..0d6efb9d 100644 --- a/EhPanda/View/Detail/Archives/ArchivesView.swift +++ b/EhPanda/View/Detail/Archives/ArchivesView.swift @@ -32,20 +32,27 @@ struct ArchivesView: View { ZStack { VStack { HathArchivesView(archives: store.hathArchives, selection: $store.selectedArchive) + Spacer() + if let credits = Int(user.credits ?? ""), let galleryPoints = Int(user.galleryPoints ?? "") { ArchiveFundsView(credits: credits, galleryPoints: galleryPoints) } + DownloadButton(isDisabled: store.selectedArchive == nil) { store.send(.fetchDownloadResponse(archiveURL)) } } - .padding(.horizontal).opacity(store.hathArchives.isEmpty ? 0 : 1) - LoadingView().opacity( - store.loadingState == .loading - && store.hathArchives.isEmpty ? 1 : 0 - ) - let error = (/LoadingState.failed).extract(from: store.loadingState) + .padding(.horizontal) + .opacity(store.hathArchives.isEmpty ? 0 : 1) + + LoadingView() + .opacity( + store.loadingState == .loading + && store.hathArchives.isEmpty ? 1 : 0 + ) + + let error = store.loadingState.failed ErrorView(error: error ?? .unknown) { store.send(.fetchArchive(gid, galleryURL, archiveURL)) } @@ -54,12 +61,12 @@ struct ArchivesView: View { .progressHUD( config: store.communicatingHUDConfig, unwrapping: $store.route, - case: /ArchivesReducer.Route.communicatingHUD + case: \.communicatingHUD ) .progressHUD( config: store.messageHUDConfig, unwrapping: $store.route, - case: /ArchivesReducer.Route.messageHUD + case: \.messageHUD ) .animation(.default, value: store.hathArchives) .animation(.default, value: user.galleryPoints) diff --git a/EhPanda/View/Detail/Comments/CommentsReducer.swift b/EhPanda/View/Detail/Comments/CommentsReducer.swift index 7b3b51dc..ff6330a2 100644 --- a/EhPanda/View/Detail/Comments/CommentsReducer.swift +++ b/EhPanda/View/Detail/Comments/CommentsReducer.swift @@ -150,7 +150,7 @@ struct CommentsReducer { effects.append( .run { send in try await Task.sleep(for: .milliseconds(750)) - await send(.detail(.setNavigation(.reading))) + await send(.detail(.setNavigation(.reading()))) } ) } else if let commentID = commentID { @@ -263,7 +263,7 @@ struct CommentsReducer { } .haptics( unwrapping: \.route, - case: /Route.postComment, + case: \.postComment, hapticsClient: hapticsClient ) } diff --git a/EhPanda/View/Detail/Comments/CommentsView.swift b/EhPanda/View/Detail/Comments/CommentsView.swift index fde5b52b..34be8ce0 100644 --- a/EhPanda/View/Detail/Comments/CommentsView.swift +++ b/EhPanda/View/Detail/Comments/CommentsView.swift @@ -90,7 +90,7 @@ struct CommentsView: View { } } } - .sheet(unwrapping: $store.route, case: /CommentsReducer.Route.postComment) { route in + .sheet(item: $store.route.sending(\.setNavigation).postComment, id: \.self) { route in let hasCommentID = !route.wrappedValue.isEmpty PostCommentView( title: hasCommentID @@ -115,7 +115,7 @@ struct CommentsView: View { .progressHUD( config: store.hudConfig, unwrapping: $store.route, - case: /CommentsReducer.Route.hud + case: \.hud ) .animation(.default, value: store.scrollRowOpacity) .onAppear { @@ -141,7 +141,7 @@ struct CommentsView: View { // MARK: NavigationLinks private extension CommentsView { @ViewBuilder var navigationLink: some View { - NavigationLink(unwrapping: $store.route, case: /CommentsReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: \.detail) { route in DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, diff --git a/EhPanda/View/Detail/DetailReducer.swift b/EhPanda/View/Detail/DetailReducer.swift index edac3a5e..d8c256bd 100644 --- a/EhPanda/View/Detail/DetailReducer.swift +++ b/EhPanda/View/Detail/DetailReducer.swift @@ -13,13 +13,13 @@ import ComposableArchitecture struct DetailReducer { @CasePathable enum Route: Equatable { - case reading + case reading(EquatableVoid = .init()) case archives(URL, URL) - case torrents + case torrents(EquatableVoid = .init()) case previews case comments(URL) case share(URL) - case postComment + case postComment(EquatableVoid = .init()) case newDawn(Greeting) case detailSearch(String) case tagDetail(TagDetail) @@ -118,361 +118,371 @@ struct DetailReducer { @Dependency(\.hapticsClient) private var hapticsClient @Dependency(\.cookieClient) private var cookieClient - var body: some Reducer { - RecurseReducer { (self) in - BindingReducer() - .onChange(of: \.route) { _, newValue in - Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + func coreReducer(self: Reduce) -> some Reducer { + Reduce { state, action in + switch action { + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .send(.clearSubStates) : .none + + case .clearSubStates: + state.readingState = .init() + state.archivesState = .init() + state.torrentsState = .init() + state.previewsState = .init() + state.commentsState.wrappedValue = .init() + state.commentContent = .init() + state.postCommentFocused = false + state.galleryInfosState = .init() + state.detailSearchState.wrappedValue = .init() + return .merge( + .send(.reading(.teardown)), + .send(.archives(.teardown)), + .send(.torrents(.teardown)), + .send(.previews(.teardown)), + .send(.comments(.teardown)), + .send(.detailSearch(.teardown)) + ) + + case .onPostCommentAppear: + return .run { send in + try await Task.sleep(for: .milliseconds(750)) + await send(.setPostCommentFocused(true)) } - Reduce { state, action in - switch action { - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .send(.clearSubStates) : .none - - case .clearSubStates: - state.readingState = .init() - state.archivesState = .init() - state.torrentsState = .init() - state.previewsState = .init() - state.commentsState.wrappedValue = .init() - state.commentContent = .init() - state.postCommentFocused = false - state.galleryInfosState = .init() + case .onAppear(let gid, let showsNewDawnGreeting): + state.showsNewDawnGreeting = showsNewDawnGreeting + if state.detailSearchState.wrappedValue == nil { state.detailSearchState.wrappedValue = .init() - return .merge( - .send(.reading(.teardown)), - .send(.archives(.teardown)), - .send(.torrents(.teardown)), - .send(.previews(.teardown)), - .send(.comments(.teardown)), - .send(.detailSearch(.teardown)) - ) - - case .onPostCommentAppear: - return .run { send in - try await Task.sleep(for: .milliseconds(750)) - await send(.setPostCommentFocused(true)) - } - - case .onAppear(let gid, let showsNewDawnGreeting): - state.showsNewDawnGreeting = showsNewDawnGreeting - if state.detailSearchState.wrappedValue == nil { - state.detailSearchState.wrappedValue = .init() - } - if state.commentsState.wrappedValue == nil { - state.commentsState.wrappedValue = .init() + } + if state.commentsState.wrappedValue == nil { + state.commentsState.wrappedValue = .init() + } + return .send(.fetchDatabaseInfos(gid)) + + case .toggleShowFullTitle: + state.showsFullTitle.toggle() + return .run(operation: { _ in hapticsClient.generateFeedback(.soft) }) + + case .toggleShowUserRating: + state.showsUserRating.toggle() + return .run(operation: { _ in hapticsClient.generateFeedback(.soft) }) + + case .setCommentContent(let content): + state.commentContent = content + return .none + + case .setPostCommentFocused(let isFocused): + state.postCommentFocused = isFocused + return .none + + case .updateRating(let value): + state.updateRating(value: value) + return .none + + case .confirmRating(let value): + state.updateRating(value: value) + return .merge( + .send(.rateGallery), + .run(operation: { _ in hapticsClient.generateFeedback(.soft) }), + .run { send in + try await Task.sleep(for: .seconds(1)) + await send(.confirmRatingDone) } - return .send(.fetchDatabaseInfos(gid)) - - case .toggleShowFullTitle: - state.showsFullTitle.toggle() - return .run(operation: { _ in hapticsClient.generateFeedback(.soft) }) - - case .toggleShowUserRating: - state.showsUserRating.toggle() - return .run(operation: { _ in hapticsClient.generateFeedback(.soft) }) - - case .setCommentContent(let content): - state.commentContent = content - return .none - - case .setPostCommentFocused(let isFocused): - state.postCommentFocused = isFocused - return .none + ) - case .updateRating(let value): - state.updateRating(value: value) - return .none + case .confirmRatingDone: + state.showsUserRating = false + return .none - case .confirmRating(let value): - state.updateRating(value: value) - return .merge( - .send(.rateGallery), - .run(operation: { _ in hapticsClient.generateFeedback(.soft) }), - .run { send in - try await Task.sleep(for: .seconds(1)) - await send(.confirmRatingDone) - } - ) - - case .confirmRatingDone: - state.showsUserRating = false - return .none - - case .syncGalleryTags: - return .run { [state] _ in - await databaseClient.updateGalleryTags(gid: state.gallery.id, tags: state.galleryTags) - } + case .syncGalleryTags: + return .run { [state] _ in + await databaseClient.updateGalleryTags(gid: state.gallery.id, tags: state.galleryTags) + } - case .syncGalleryDetail: - guard let detail = state.galleryDetail else { return .none } - return .run(operation: { _ in await databaseClient.cacheGalleryDetail(detail) }) + case .syncGalleryDetail: + guard let detail = state.galleryDetail else { return .none } + return .run(operation: { _ in await databaseClient.cacheGalleryDetail(detail) }) - case .syncGalleryPreviewURLs: - return .run { [state] _ in - await databaseClient - .updatePreviewURLs(gid: state.gallery.id, previewURLs: state.galleryPreviewURLs) - } + case .syncGalleryPreviewURLs: + return .run { [state] _ in + await databaseClient + .updatePreviewURLs(gid: state.gallery.id, previewURLs: state.galleryPreviewURLs) + } - case .syncGalleryComments: - return .run { [state] _ in - await databaseClient.updateComments(gid: state.gallery.id, comments: state.galleryComments) - } + case .syncGalleryComments: + return .run { [state] _ in + await databaseClient.updateComments(gid: state.gallery.id, comments: state.galleryComments) + } - case .syncGreeting(let greeting): - return .run(operation: { _ in await databaseClient.updateGreeting(greeting) }) + case .syncGreeting(let greeting): + return .run(operation: { _ in await databaseClient.updateGreeting(greeting) }) - case .syncPreviewConfig(let config): - return .run { [state] _ in - await databaseClient.updatePreviewConfig(gid: state.gallery.id, config: config) - } + case .syncPreviewConfig(let config): + return .run { [state] _ in + await databaseClient.updatePreviewConfig(gid: state.gallery.id, config: config) + } - case .saveGalleryHistory: - return .run { [state] _ in - await databaseClient.updateLastOpenDate(gid: state.gallery.id) - } + case .saveGalleryHistory: + return .run { [state] _ in + await databaseClient.updateLastOpenDate(gid: state.gallery.id) + } - case .updateReadingProgress(let progress): - return .run { [state] _ in - await databaseClient.updateReadingProgress(gid: state.gallery.id, progress: progress) - } + case .updateReadingProgress(let progress): + return .run { [state] _ in + await databaseClient.updateReadingProgress(gid: state.gallery.id, progress: progress) + } - case .teardown: - return .merge(CancelID.allCases.map(Effect.cancel(id:))) + case .teardown: + return .merge(CancelID.allCases.map(Effect.cancel(id:))) - case .fetchDatabaseInfos(let gid): - guard let gallery = databaseClient.fetchGallery(gid: gid) else { return .none } - state.gallery = gallery - if let detail = databaseClient.fetchGalleryDetail(gid: gid) { - state.galleryDetail = detail + case .fetchDatabaseInfos(let gid): + guard let gallery = databaseClient.fetchGallery(gid: gid) else { return .none } + state.gallery = gallery + if let detail = databaseClient.fetchGalleryDetail(gid: gid) { + state.galleryDetail = detail + } + return .merge( + .send(.saveGalleryHistory), + .run { [galleryID = state.gallery.id] send in + guard let dbState = await databaseClient.fetchGalleryState(gid: galleryID) else { return } + await send(.fetchDatabaseInfosDone(dbState)) } - return .merge( - .send(.saveGalleryHistory), - .run { [galleryID = state.gallery.id] send in - guard let dbState = await databaseClient.fetchGalleryState(gid: galleryID) else { return } - await send(.fetchDatabaseInfosDone(dbState)) - } .cancellable(id: CancelID.fetchDatabaseInfos) - ) - - case .fetchDatabaseInfosDone(let galleryState): + ) + + case .fetchDatabaseInfosDone(let galleryState): + state.galleryTags = galleryState.tags + state.galleryPreviewURLs = galleryState.previewURLs + state.galleryComments = galleryState.comments + return .send(.fetchGalleryDetail) + + case .fetchGalleryDetail: + guard state.loadingState != .loading, + let galleryURL = state.gallery.galleryURL + else { return .none } + state.loadingState = .loading + return .run { [galleryID = state.gallery.id] send in + let response = await GalleryDetailRequest(gid: galleryID, galleryURL: galleryURL).response() + await send(.fetchGalleryDetailDone(response)) + } + .cancellable(id: CancelID.fetchGalleryDetail) + + case .fetchGalleryDetailDone(let result): + state.loadingState = .idle + switch result { + case .success(let (galleryDetail, galleryState, apiKey, greeting)): + var effects: [Effect] = [ + .send(.syncGalleryTags), + .send(.syncGalleryDetail), + .send(.syncGalleryPreviewURLs), + .send(.syncGalleryComments) + ] + state.apiKey = apiKey + state.galleryDetail = galleryDetail state.galleryTags = galleryState.tags state.galleryPreviewURLs = galleryState.previewURLs state.galleryComments = galleryState.comments - return .send(.fetchGalleryDetail) - - case .fetchGalleryDetail: - guard state.loadingState != .loading, - let galleryURL = state.gallery.galleryURL - else { return .none } - state.loadingState = .loading - return .run { [galleryID = state.gallery.id] send in - let response = await GalleryDetailRequest(gid: galleryID, galleryURL: galleryURL).response() - await send(.fetchGalleryDetailDone(response)) - } - .cancellable(id: CancelID.fetchGalleryDetail) - - case .fetchGalleryDetailDone(let result): - state.loadingState = .idle - switch result { - case .success(let (galleryDetail, galleryState, apiKey, greeting)): - var effects: [Effect] = [ - .send(.syncGalleryTags), - .send(.syncGalleryDetail), - .send(.syncGalleryPreviewURLs), - .send(.syncGalleryComments) - ] - state.apiKey = apiKey - state.galleryDetail = galleryDetail - state.galleryTags = galleryState.tags - state.galleryPreviewURLs = galleryState.previewURLs - state.galleryComments = galleryState.comments - state.userRating = Int(galleryDetail.userRating) * 2 - if let greeting = greeting { - effects.append(.send(.syncGreeting(greeting))) - if !greeting.gainedNothing && state.showsNewDawnGreeting { - effects.append(.send(.setNavigation(.newDawn(greeting)))) - } + state.userRating = Int(galleryDetail.userRating) * 2 + if let greeting = greeting { + effects.append(.send(.syncGreeting(greeting))) + if !greeting.gainedNothing && state.showsNewDawnGreeting { + effects.append(.send(.setNavigation(.newDawn(greeting)))) } - if let config = galleryState.previewConfig { - effects.append(.send(.syncPreviewConfig(config))) - } - return .merge(effects) - case .failure(let error): - state.loadingState = .failed(error) } - return .none - - case .rateGallery: - guard let apiuid = Int(cookieClient.apiuid), let gid = Int(state.gallery.id) - else { return .none } - return .run { [state] send in - let response = await RateGalleryRequest( - apiuid: apiuid, - apikey: state.apiKey, - gid: gid, - token: state.gallery.token, - rating: state.userRating - ) + if let config = galleryState.previewConfig { + effects.append(.send(.syncPreviewConfig(config))) + } + return .merge(effects) + case .failure(let error): + state.loadingState = .failed(error) + } + return .none + + case .rateGallery: + guard let apiuid = Int(cookieClient.apiuid), let gid = Int(state.gallery.id) + else { return .none } + return .run { [state] send in + let response = await RateGalleryRequest( + apiuid: apiuid, + apikey: state.apiKey, + gid: gid, + token: state.gallery.token, + rating: state.userRating + ) .response() - await send(.anyGalleryOpsDone(response)) - }.cancellable(id: CancelID.rateGallery) - - case .favorGallery(let favIndex): - return .run { [state] send in - let response = await FavorGalleryRequest( - gid: state.gallery.id, - token: state.gallery.token, - favIndex: favIndex - ) + await send(.anyGalleryOpsDone(response)) + }.cancellable(id: CancelID.rateGallery) + + case .favorGallery(let favIndex): + return .run { [state] send in + let response = await FavorGalleryRequest( + gid: state.gallery.id, + token: state.gallery.token, + favIndex: favIndex + ) .response() - await send(.anyGalleryOpsDone(response)) - } - .cancellable(id: CancelID.favorGallery) + await send(.anyGalleryOpsDone(response)) + } + .cancellable(id: CancelID.favorGallery) - case .unfavorGallery: - return .run { [galleryID = state.gallery.id] send in - let response = await UnfavorGalleryRequest(gid: galleryID).response() - await send(.anyGalleryOpsDone(response)) - } - .cancellable(id: CancelID.unfavorGallery) - - case .postComment(let galleryURL): - guard !state.commentContent.isEmpty else { return .none } - return .run { [commentContent = state.commentContent] send in - let response = await CommentGalleryRequest( - content: commentContent, galleryURL: galleryURL - ) + case .unfavorGallery: + return .run { [galleryID = state.gallery.id] send in + let response = await UnfavorGalleryRequest(gid: galleryID).response() + await send(.anyGalleryOpsDone(response)) + } + .cancellable(id: CancelID.unfavorGallery) + + case .postComment(let galleryURL): + guard !state.commentContent.isEmpty else { return .none } + return .run { [commentContent = state.commentContent] send in + let response = await CommentGalleryRequest( + content: commentContent, galleryURL: galleryURL + ) .response() - await send(.anyGalleryOpsDone(response)) - } - .cancellable(id: CancelID.postComment) - - case .voteTag(let tag, let vote): - guard let apiuid = Int(cookieClient.apiuid), let gid = Int(state.gallery.id) - else { return .none } - return .run { [state] send in - let response = await VoteGalleryTagRequest( - apiuid: apiuid, - apikey: state.apiKey, - gid: gid, - token: state.gallery.token, - tag: tag, - vote: vote - ) + await send(.anyGalleryOpsDone(response)) + } + .cancellable(id: CancelID.postComment) + + case .voteTag(let tag, let vote): + guard let apiuid = Int(cookieClient.apiuid), let gid = Int(state.gallery.id) + else { return .none } + return .run { [state] send in + let response = await VoteGalleryTagRequest( + apiuid: apiuid, + apikey: state.apiKey, + gid: gid, + token: state.gallery.token, + tag: tag, + vote: vote + ) .response() - await send(.anyGalleryOpsDone(response)) - } - .cancellable(id: CancelID.voteTag) - - case .anyGalleryOpsDone(let result): - if case .success = result { - return .merge( - .send(.fetchGalleryDetail), - .run(operation: { _ in hapticsClient.generateNotificationFeedback(.success) }) - ) - } - return .run(operation: { _ in hapticsClient.generateNotificationFeedback(.error) }) + await send(.anyGalleryOpsDone(response)) + } + .cancellable(id: CancelID.voteTag) - case .reading(.onPerformDismiss): - return .send(.setNavigation(nil)) + case .anyGalleryOpsDone(let result): + if case .success = result { + return .merge( + .send(.fetchGalleryDetail), + .run(operation: { _ in hapticsClient.generateNotificationFeedback(.success) }) + ) + } + return .run(operation: { _ in hapticsClient.generateNotificationFeedback(.error) }) - case .reading: - return .none + case .reading(.onPerformDismiss): + return .send(.setNavigation(nil)) - case .archives: - return .none + case .reading: + return .none - case .torrents: - return .none + case .archives: + return .none - case .previews: - return .none + case .torrents: + return .none - case .comments(.performCommentActionDone(let result)): - return .send(.anyGalleryOpsDone(result)) + case .previews: + return .none - case .comments(.detail(let recursiveAction)): - guard state.commentsState.wrappedValue != nil else { return .none } - return self.reduce( - into: &state.commentsState.wrappedValue!.detailState.wrappedValue!, action: recursiveAction - ) - .map({ Action.comments(.detail($0)) }) + case .comments(.performCommentActionDone(let result)): + return .send(.anyGalleryOpsDone(result)) - case .comments: - return .none + case .comments(.detail(let recursiveAction)): + guard state.commentsState.wrappedValue != nil else { return .none } + return self.reduce( + into: &state.commentsState.wrappedValue!.detailState.wrappedValue!, action: recursiveAction + ) + .map({ Action.comments(.detail($0)) }) - case .galleryInfos: - return .none + case .comments: + return .none - case .detailSearch(.detail(let recursiveAction)): - guard state.detailSearchState.wrappedValue != nil else { return .none } - return self.reduce( - into: &state.detailSearchState.wrappedValue!.detailState.wrappedValue!, action: recursiveAction - ) - .map({ Action.detailSearch(.detail($0)) }) + case .galleryInfos: + return .none - case .detailSearch: - return .none - } + case .detailSearch(.detail(let recursiveAction)): + guard state.detailSearchState.wrappedValue != nil else { return .none } + return self.reduce( + into: &state.detailSearchState.wrappedValue!.detailState.wrappedValue!, action: recursiveAction + ) + .map({ Action.detailSearch(.detail($0)) }) + + case .detailSearch: + return .none } - .ifLet( - \.commentsState.wrappedValue, - action: /Action.comments, - then: CommentsReducer.init - ) - .ifLet( - \.detailSearchState.wrappedValue, - action: /Action.detailSearch, - then: DetailSearchReducer.init - ) + } + .ifLet( + \.commentsState.wrappedValue, + action: \.comments, + then: CommentsReducer.init + ) + .ifLet( + \.detailSearchState.wrappedValue, + action: \.detailSearch, + then: DetailSearchReducer.init + ) + } + + func hapticsReducer( + @ReducerBuilder reducer: () -> some Reducer + ) -> some Reducer { + reducer() .haptics( unwrapping: \.route, - case: /Route.detailSearch, + case: \.detailSearch, hapticsClient: hapticsClient, style: .soft ) .haptics( unwrapping: \.route, - case: /Route.postComment, + case: \.postComment, hapticsClient: hapticsClient ) .haptics( unwrapping: \.route, - case: /Route.tagDetail, + case: \.tagDetail, hapticsClient: hapticsClient ) .haptics( unwrapping: \.route, - case: /Route.torrents, + case: \.torrents, hapticsClient: hapticsClient ) .haptics( unwrapping: \.route, - case: /Route.archives, + case: \.archives, hapticsClient: hapticsClient ) .haptics( unwrapping: \.route, - case: /Route.reading, + case: \.reading, hapticsClient: hapticsClient ) .haptics( unwrapping: \.route, - case: /Route.share, + case: \.share, hapticsClient: hapticsClient ) + } + + var body: some Reducer { + RecurseReducer { (self) in + BindingReducer() + .onChange(of: \.route) { _, newValue in + Reduce({ _, _ in newValue == nil ? .send(.clearSubStates) : .none }) + } + + coreReducer(self: self) - Scope(state: \.readingState, action: /Action.reading, child: ReadingReducer.init) - Scope(state: \.archivesState, action: /Action.archives, child: ArchivesReducer.init) - Scope(state: \.torrentsState, action: /Action.torrents, child: TorrentsReducer.init) - Scope(state: \.previewsState, action: /Action.previews, child: PreviewsReducer.init) - Scope(state: \.galleryInfosState, action: /Action.galleryInfos, child: GalleryInfosReducer.init) + Scope(state: \.readingState, action: \.reading, child: ReadingReducer.init) + Scope(state: \.archivesState, action: \.archives, child: ArchivesReducer.init) + Scope(state: \.torrentsState, action: \.torrents, child: TorrentsReducer.init) + Scope(state: \.previewsState, action: \.previews, child: PreviewsReducer.init) + Scope(state: \.galleryInfosState, action: \.galleryInfos, child: GalleryInfosReducer.init) } } } diff --git a/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift b/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift index 3730e13a..1b9c8313 100644 --- a/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift +++ b/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift @@ -9,10 +9,10 @@ import ComposableArchitecture @Reducer struct DetailSearchReducer { - @CasePathable + @dynamicMemberLookup @CasePathable enum Route: Equatable { - case filters - case quickSearch + case filters(EquatableVoid = .unique) + case quickSearch(EquatableVoid = .unique) case detail(String) } @@ -185,16 +185,16 @@ struct DetailSearchReducer { } .haptics( unwrapping: \.route, - case: /Route.quickSearch, + case: \.quickSearch, hapticsClient: hapticsClient ) .haptics( unwrapping: \.route, - case: /Route.filters, + case: \.filters, hapticsClient: hapticsClient ) - Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) - Scope(state: \.quickDetailSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) + Scope(state: \.filtersState, action: \.filters, child: FiltersReducer.init) + Scope(state: \.quickDetailSearchState, action: \.quickSearch, child: QuickSearchReducer.init) } } diff --git a/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift b/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift index 8354ac07..7a65a612 100644 --- a/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift +++ b/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift @@ -43,7 +43,7 @@ struct DetailSearchView: View { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) - .sheet(unwrapping: $store.route, case: /DetailSearchReducer.Route.quickSearch) { _ in + .sheet(item: $store.route.sending(\.setNavigation).quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickDetailSearchState, action: \.quickSearch) ) { keyword in @@ -53,7 +53,7 @@ struct DetailSearchView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: $store.route, case: /DetailSearchReducer.Route.filters) { _ in + .sheet(item: $store.route.sending(\.setNavigation).filters) { _ in FiltersView(store: store.scope(state: \.filtersState, action: \.filters)) .accentColor(setting.accentColor).autoBlur(radius: blurRadius) } @@ -80,7 +80,7 @@ struct DetailSearchView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: $store.route, case: /DetailSearchReducer.Route.detail) { route in + .sheet(item: $store.route.sending(\.setNavigation).detail, id: \.self) { route in NavigationView { DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), @@ -97,7 +97,7 @@ struct DetailSearchView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: $store.route, case: /DetailSearchReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: \.detail) { route in DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -110,10 +110,10 @@ struct DetailSearchView: View { CustomToolbarItem { ToolbarFeaturesMenu { FiltersButton { - store.send(.setNavigation(.filters)) + store.send(.setNavigation(.filters())) } QuickSearchButton { - store.send(.setNavigation(.quickSearch)) + store.send(.setNavigation(.quickSearch())) } } } diff --git a/EhPanda/View/Detail/DetailView.swift b/EhPanda/View/Detail/DetailView.swift index 901addbd..5f065d6d 100644 --- a/EhPanda/View/Detail/DetailView.swift +++ b/EhPanda/View/Detail/DetailView.swift @@ -30,7 +30,7 @@ struct DetailView: View { self.tagTranslator = tagTranslator } - var body: some View { + var content: some View { ZStack { ScrollView(showsIndicators: false) { let content = @@ -44,7 +44,7 @@ struct DetailView: View { showFullTitleAction: { store.send(.toggleShowFullTitle) }, favorAction: { store.send(.favorGallery($0)) }, unfavorAction: { store.send(.unfavorGallery) }, - navigateReadingAction: { store.send(.setNavigation(.reading)) }, + navigateReadingAction: { store.send(.setNavigation(.reading())) }, navigateUploaderAction: { if let uploader = store.galleryDetail?.uploader { let keyword = "uploader:" + "\"\(uploader)\"" @@ -92,7 +92,7 @@ struct DetailView: View { navigatePreviewsAction: { store.send(.setNavigation(.previews)) }, navigateReadingAction: { store.send(.updateReadingProgress($0)) - store.send(.setNavigation(.reading)) + store.send(.setNavigation(.reading())) } ) } @@ -103,7 +103,7 @@ struct DetailView: View { store.send(.setNavigation(.comments(galleryURL))) } }, - navigatePostCommentAction: { store.send(.setNavigation(.postComment)) } + navigatePostCommentAction: { store.send(.setNavigation(.postComment())) } ) } .padding(.bottom, 20) @@ -117,91 +117,110 @@ struct DetailView: View { } } .opacity(store.galleryDetail == nil ? 0 : 1) + LoadingView() .opacity( store.galleryDetail == nil && store.loadingState == .loading ? 1 : 0 ) - let error = (/LoadingState.failed).extract(from: store.loadingState) + + let error = store.loadingState.failed let retryAction: () -> Void = { store.send(.fetchGalleryDetail) } ErrorView(error: error ?? .unknown, action: error?.isRetryable != false ? retryAction : nil) .opacity(store.galleryDetail == nil && error != nil ? 1 : 0) } - .fullScreenCover(unwrapping: $store.route, case: /DetailReducer.Route.reading) { _ in - ReadingView( - store: store.scope(state: \.readingState, action: \.reading), - gid: gid, setting: $setting, blurRadius: blurRadius - ) - .accentColor(setting.accentColor) - .autoBlur(radius: blurRadius) - } - .sheet(unwrapping: $store.route, case: /DetailReducer.Route.archives) { route in - let (galleryURL, archiveURL) = route.wrappedValue - ArchivesView( - store: store.scope(state: \.archivesState, action: \.archives), - gid: gid, user: user, galleryURL: galleryURL, archiveURL: archiveURL - ) - .accentColor(setting.accentColor) - .autoBlur(radius: blurRadius) - } - .sheet(unwrapping: $store.route, case: /DetailReducer.Route.torrents) { _ in - TorrentsView( - store: store.scope(state: \.torrentsState, action: \.torrents), - gid: gid, token: store.gallery.token, blurRadius: blurRadius - ) - .accentColor(setting.accentColor) - .autoBlur(radius: blurRadius) - } - .sheet(unwrapping: $store.route, case: /DetailReducer.Route.share) { route in - ActivityView(activityItems: [route.wrappedValue]) + } + + func modalModifiers(@ViewBuilder content: () -> Content) -> some View { + content() + .fullScreenCover(item: $store.route.sending(\.setNavigation).reading) { _ in + ReadingView( + store: store.scope(state: \.readingState, action: \.reading), + gid: gid, + setting: $setting, + blurRadius: blurRadius + ) + .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) - } - .sheet(unwrapping: $store.route, case: /DetailReducer.Route.postComment) { _ in - PostCommentView( - title: L10n.Localizable.PostCommentView.Title.postComment, - content: $store.commentContent, - isFocused: $store.postCommentFocused, - postAction: { - if let galleryURL = store.gallery.galleryURL { - store.send(.postComment(galleryURL)) - } - store.send(.setNavigation(nil)) - }, - cancelAction: { store.send(.setNavigation(nil)) }, - onAppearAction: { store.send(.onPostCommentAppear) } - ) - .accentColor(setting.accentColor) - .autoBlur(radius: blurRadius) - } - .sheet(unwrapping: $store.route, case: /DetailReducer.Route.newDawn) { route in - NewDawnView(greeting: route.wrappedValue).autoBlur(radius: blurRadius) - } - .sheet(unwrapping: $store.route, case: /DetailReducer.Route.tagDetail) { route in - TagDetailView(detail: route.wrappedValue).autoBlur(radius: blurRadius) - } - .animation(.default, value: store.showsUserRating) - .animation(.default, value: store.showsFullTitle) - .animation(.default, value: store.galleryDetail) - .onAppear { - DispatchQueue.main.async { - store.send(.onAppear(gid, setting.showsNewDawnGreeting)) } - } - .background(navigationLinks) - .toolbar(content: toolbar) + .sheet(item: $store.route.sending(\.setNavigation).archives, id: \.0.absoluteString) { urls in + let (galleryURL, archiveURL) = urls + ArchivesView( + store: store.scope(state: \.archivesState, action: \.archives), + gid: gid, + user: user, + galleryURL: galleryURL, + archiveURL: archiveURL + ) + .accentColor(setting.accentColor) + .autoBlur(radius: blurRadius) + } + .sheet(item: $store.route.sending(\.setNavigation).torrents) { _ in + TorrentsView( + store: store.scope(state: \.torrentsState, action: \.torrents), + gid: gid, + token: store.gallery.token, + blurRadius: blurRadius + ) + .accentColor(setting.accentColor) + .autoBlur(radius: blurRadius) + } + .sheet(item: $store.route.sending(\.setNavigation).share, id: \.absoluteString) { url in + ActivityView(activityItems: [url]) + .autoBlur(radius: blurRadius) + } + .sheet(item: $store.route.sending(\.setNavigation).postComment) { _ in + PostCommentView( + title: L10n.Localizable.PostCommentView.Title.postComment, + content: $store.commentContent, + isFocused: $store.postCommentFocused, + postAction: { + if let galleryURL = store.gallery.galleryURL { + store.send(.postComment(galleryURL)) + } + store.send(.setNavigation(nil)) + }, + cancelAction: { store.send(.setNavigation(nil)) }, + onAppearAction: { store.send(.onPostCommentAppear) } + ) + .accentColor(setting.accentColor) + .autoBlur(radius: blurRadius) + } + .sheet(item: $store.route.sending(\.setNavigation).newDawn) { greeting in + NewDawnView(greeting: greeting) + .autoBlur(radius: blurRadius) + } + .sheet(item: $store.route.sending(\.setNavigation).tagDetail, id: \.title) { detail in + TagDetailView(detail: detail) + .autoBlur(radius: blurRadius) + } + } + + var body: some View { + modalModifiers(content: { content }) + .animation(.default, value: store.showsUserRating) + .animation(.default, value: store.showsFullTitle) + .animation(.default, value: store.galleryDetail) + .onAppear { + DispatchQueue.main.async { + store.send(.onAppear(gid, setting.showsNewDawnGreeting)) + } + } + .background(navigationLinks) + .toolbar(content: toolbar) } } // MARK: NavigationLinks private extension DetailView { @ViewBuilder var navigationLinks: some View { - NavigationLink(unwrapping: $store.route, case: /DetailReducer.Route.previews) { _ in + NavigationLink(unwrapping: $store.route, case: \.previews) { _ in PreviewsView( store: store.scope(state: \.previewsState, action: \.previews), gid: gid, setting: $setting, blurRadius: blurRadius ) } - NavigationLink(unwrapping: $store.route, case: /DetailReducer.Route.comments) { route in + NavigationLink(unwrapping: $store.route, case: \.comments) { route in if let commentStore = store.scope(state: \.commentsState.wrappedValue, action: \.comments) { CommentsView( store: commentStore, gid: gid, token: store.gallery.token, apiKey: store.apiKey, @@ -211,7 +230,7 @@ private extension DetailView { ) } } - NavigationLink(unwrapping: $store.route, case: /DetailReducer.Route.detailSearch) { route in + NavigationLink(unwrapping: $store.route, case: \.detailSearch) { route in if let detailSearchStore = store.scope(state: \.detailSearchState.wrappedValue, action: \.detailSearch) { DetailSearchView( store: detailSearchStore, keyword: route.wrappedValue, user: user, setting: $setting, @@ -219,7 +238,7 @@ private extension DetailView { ) } } - NavigationLink(unwrapping: $store.route, case: /DetailReducer.Route.galleryInfos) { route in + NavigationLink(unwrapping: $store.route, case: \.galleryInfos) { route in let (gallery, galleryDetail) = route.wrappedValue GalleryInfosView( store: store.scope(state: \.galleryInfosState, action: \.galleryInfos), @@ -245,7 +264,7 @@ private extension DetailView { } .disabled(store.galleryDetail?.archiveURL == nil || !CookieUtil.didLogin) Button { - store.send(.setNavigation(.torrents)) + store.send(.setNavigation(.torrents())) } label: { let base = L10n.Localizable.DetailView.ToolbarItem.Button.torrents let torrentCount = store.galleryDetail?.torrentCount ?? 0 diff --git a/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift b/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift index 88f65386..ba30dddb 100644 --- a/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift +++ b/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift @@ -10,6 +10,7 @@ import ComposableArchitecture @Reducer struct GalleryInfosReducer { + @CasePathable enum Route { case hud } diff --git a/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift b/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift index fa017d10..4d55c735 100644 --- a/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift +++ b/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift @@ -117,7 +117,7 @@ struct GalleryInfosView: View { .progressHUD( config: store.hudConfig, unwrapping: $store.route, - case: /GalleryInfosReducer.Route.hud + case: \.hud ) .navigationTitle(L10n.Localizable.GalleryInfosView.Title.galleryInfos) } diff --git a/EhPanda/View/Detail/Previews/PreviewsReducer.swift b/EhPanda/View/Detail/Previews/PreviewsReducer.swift index c943ed3d..f25e6f30 100644 --- a/EhPanda/View/Detail/Previews/PreviewsReducer.swift +++ b/EhPanda/View/Detail/Previews/PreviewsReducer.swift @@ -10,8 +10,9 @@ import ComposableArchitecture @Reducer struct PreviewsReducer { - enum Route { - case reading + @CasePathable + enum Route: Equatable { + case reading(EquatableVoid = .init()) } private enum CancelID: CaseIterable { @@ -144,10 +145,10 @@ struct PreviewsReducer { } .haptics( unwrapping: \.route, - case: /Route.reading, + case: \.reading, hapticsClient: hapticsClient ) - Scope(state: \.readingState, action: /Action.reading, child: ReadingReducer.init) + Scope(state: \.readingState, action: \.reading, child: ReadingReducer.init) } } diff --git a/EhPanda/View/Detail/Previews/PreviewsView.swift b/EhPanda/View/Detail/Previews/PreviewsView.swift index ec45446d..97c01490 100644 --- a/EhPanda/View/Detail/Previews/PreviewsView.swift +++ b/EhPanda/View/Detail/Previews/PreviewsView.swift @@ -45,13 +45,18 @@ struct PreviewsView: View { ) Button { store.send(.updateReadingProgress(index)) - store.send(.setNavigation(.reading)) + store.send(.setNavigation(.reading())) } label: { KFImage.url(url, cacheKey: store.previewURLs[index]?.absoluteString) - .placeholder { Placeholder(style: .activity(ratio: Defaults.ImageSize.previewAspect)) } - .imageModifier(modifier).fade(duration: 0.25).resizable().scaledToFit() + .placeholder({ Placeholder(style: .activity(ratio: Defaults.ImageSize.previewAspect)) }) + .imageModifier(modifier) + .fade(duration: 0.25) + .resizable() + .scaledToFit() } - Text("\(index)").font(DeviceUtil.isPadWidth ? .callout : .caption).foregroundColor(.secondary) + Text("\(index)") + .font(DeviceUtil.isPadWidth ? .callout : .caption) + .foregroundColor(.secondary) } .onAppear { if store.databaseLoadingState != .loading @@ -66,7 +71,7 @@ struct PreviewsView: View { .padding(.bottom) .id(store.databaseLoadingState) } - .fullScreenCover(unwrapping: $store.route, case: /PreviewsReducer.Route.reading) { _ in + .fullScreenCover(item: $store.route.sending(\.setNavigation).reading) { _ in ReadingView( store: store.scope(state: \.readingState, action: \.reading), gid: gid, setting: $setting, blurRadius: blurRadius diff --git a/EhPanda/View/Detail/Torrents/TorrentsReducer.swift b/EhPanda/View/Detail/Torrents/TorrentsReducer.swift index e46757e8..b520718d 100644 --- a/EhPanda/View/Detail/Torrents/TorrentsReducer.swift +++ b/EhPanda/View/Detail/Torrents/TorrentsReducer.swift @@ -31,7 +31,7 @@ struct TorrentsReducer { enum Action: BindableAction, Equatable { case binding(BindingAction) - case setNavigation(Route) + case setNavigation(Route?) case copyText(String) case presentTorrentActivity(String, Data) @@ -114,7 +114,7 @@ struct TorrentsReducer { } .haptics( unwrapping: \.route, - case: /Route.share, + case: \.share, hapticsClient: hapticsClient ) } diff --git a/EhPanda/View/Detail/Torrents/TorrentsView.swift b/EhPanda/View/Detail/Torrents/TorrentsView.swift index de558340..512fc2fd 100644 --- a/EhPanda/View/Detail/Torrents/TorrentsView.swift +++ b/EhPanda/View/Detail/Torrents/TorrentsView.swift @@ -36,21 +36,24 @@ struct TorrentsView: View { } } } - LoadingView().opacity(store.loadingState == .loading && store.torrents.isEmpty ? 1 : 0) - let error = (/LoadingState.failed).extract(from: store.loadingState) + + LoadingView() + .opacity(store.loadingState == .loading && store.torrents.isEmpty ? 1 : 0) + + let error = store.loadingState.failed ErrorView(error: error ?? .unknown) { store.send(.fetchGalleryTorrents(gid, token)) } .opacity(error != nil && store.torrents.isEmpty ? 1 : 0) } - .sheet(unwrapping: $store.route, case: /TorrentsReducer.Route.share) { route in + .sheet(item: $store.route.sending(\.setNavigation).share, id: \.absoluteString) { route in ActivityView(activityItems: [route.wrappedValue]) .autoBlur(radius: blurRadius) } .progressHUD( config: store.hudConfig, unwrapping: $store.route, - case: /TorrentsReducer.Route.hud + case: \.hud ) .animation(.default, value: store.torrents) .onAppear { diff --git a/EhPanda/View/Favorites/FavoritesReducer.swift b/EhPanda/View/Favorites/FavoritesReducer.swift index e140de18..a7b85d60 100644 --- a/EhPanda/View/Favorites/FavoritesReducer.swift +++ b/EhPanda/View/Favorites/FavoritesReducer.swift @@ -13,7 +13,7 @@ import ComposableArchitecture struct FavoritesReducer { @CasePathable enum Route: Equatable { - case quickSearch + case quickSearch(EquatableVoid = .init()) case detail(String) } @@ -193,11 +193,11 @@ struct FavoritesReducer { } .haptics( unwrapping: \.route, - case: /Route.quickSearch, + case: \.quickSearch, hapticsClient: hapticsClient ) - Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) - Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) + Scope(state: \.detailState.wrappedValue!, action: \.detail, child: DetailReducer.init) + Scope(state: \.quickSearchState, action: \.quickSearch, child: QuickSearchReducer.init) } } diff --git a/EhPanda/View/Favorites/FavoritesView.swift b/EhPanda/View/Favorites/FavoritesView.swift index 712f2239..25300ad6 100644 --- a/EhPanda/View/Favorites/FavoritesView.swift +++ b/EhPanda/View/Favorites/FavoritesView.swift @@ -54,7 +54,7 @@ struct FavoritesView: View { NotLoginView(action: { store.send(.onNotLoginViewButtonTapped) }) } } - .sheet(unwrapping: $store.route, case: /FavoritesReducer.Route.quickSearch) { _ in + .sheet(item: $store.route.sending(\.setNavigation).quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickSearchState, action: \.quickSearch) ) { keyword in @@ -87,7 +87,7 @@ struct FavoritesView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: $store.route, case: /FavoritesReducer.Route.detail) { route in + .sheet(item: $store.route.sending(\.setNavigation).detail, id: \.self) { route in NavigationView { DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), @@ -105,7 +105,7 @@ struct FavoritesView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: $store.route, case: /FavoritesReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: \.detail) { route in DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -127,7 +127,7 @@ struct FavoritesView: View { } } QuickSearchButton(hideText: true) { - store.send(.setNavigation(.quickSearch)) + store.send(.setNavigation(.quickSearch())) } } } diff --git a/EhPanda/View/Home/Frontpage/FrontpageReducer.swift b/EhPanda/View/Home/Frontpage/FrontpageReducer.swift index c5f29449..e3490b49 100644 --- a/EhPanda/View/Home/Frontpage/FrontpageReducer.swift +++ b/EhPanda/View/Home/Frontpage/FrontpageReducer.swift @@ -11,7 +11,7 @@ import ComposableArchitecture struct FrontpageReducer { @CasePathable enum Route: Equatable { - case filters + case filters(EquatableVoid = .init()) case detail(String) } @@ -163,11 +163,11 @@ struct FrontpageReducer { } .haptics( unwrapping: \.route, - case: /Route.filters, + case: \.filters, hapticsClient: hapticsClient ) - Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) - Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.filtersState, action: \.filters, child: FiltersReducer.init) + Scope(state: \.detailState.wrappedValue!, action: \.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Home/Frontpage/FrontpageView.swift b/EhPanda/View/Home/Frontpage/FrontpageView.swift index cfccd45f..64191521 100644 --- a/EhPanda/View/Home/Frontpage/FrontpageView.swift +++ b/EhPanda/View/Home/Frontpage/FrontpageView.swift @@ -42,7 +42,7 @@ struct FrontpageView: View { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) - .sheet(unwrapping: $store.route, case: /FrontpageReducer.Route.filters) { _ in + .sheet(item: $store.route.sending(\.setNavigation).filters) { _ in FiltersView(store: store.scope(state: \.filtersState, action: \.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } @@ -60,7 +60,7 @@ struct FrontpageView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: $store.route, case: /FrontpageReducer.Route.detail) { route in + .sheet(item: $store.route.sending(\.setNavigation).detail, id: \.self) { route in NavigationView { DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), @@ -77,7 +77,7 @@ struct FrontpageView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: $store.route, case: /FrontpageReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: \.detail) { route in DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -89,7 +89,7 @@ struct FrontpageView: View { private func toolbar() -> some ToolbarContent { CustomToolbarItem { FiltersButton(hideText: true) { - store.send(.setNavigation(.filters)) + store.send(.setNavigation(.filters())) } } } diff --git a/EhPanda/View/Home/History/HistoryReducer.swift b/EhPanda/View/Home/History/HistoryReducer.swift index 97a632c2..3070977f 100644 --- a/EhPanda/View/Home/History/HistoryReducer.swift +++ b/EhPanda/View/Home/History/HistoryReducer.swift @@ -101,6 +101,6 @@ struct HistoryReducer { } } - Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: \.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Home/History/HistoryView.swift b/EhPanda/View/Home/History/HistoryView.swift index 775e0359..30cad029 100644 --- a/EhPanda/View/Home/History/HistoryView.swift +++ b/EhPanda/View/Home/History/HistoryView.swift @@ -54,7 +54,7 @@ struct HistoryView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: $store.route, case: /HistoryReducer.Route.detail) { route in + .sheet(item: $store.route.sending(\.setNavigation).detail, id: \.self) { route in NavigationView { DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), @@ -71,7 +71,7 @@ struct HistoryView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: $store.route, case: /HistoryReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: \.detail) { route in DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -91,7 +91,7 @@ struct HistoryView: View { .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.clear, unwrapping: $store.route, - case: /HistoryReducer.Route.clearHistory + case: \.clearHistory ) { Button(L10n.Localizable.ConfirmationDialog.Button.clear, role: .destructive) { store.send(.clearHistoryGalleries) diff --git a/EhPanda/View/Home/HomeReducer.swift b/EhPanda/View/Home/HomeReducer.swift index 88b44cff..189d3d18 100644 --- a/EhPanda/View/Home/HomeReducer.swift +++ b/EhPanda/View/Home/HomeReducer.swift @@ -264,11 +264,11 @@ struct HomeReducer { } } - Scope(state: \.frontpageState, action: /Action.frontpage, child: FrontpageReducer.init) - Scope(state: \.toplistsState, action: /Action.toplists, child: ToplistsReducer.init) - Scope(state: \.popularState, action: /Action.popular, child: PopularReducer.init) - Scope(state: \.watchedState, action: /Action.watched, child: WatchedReducer.init) - Scope(state: \.historyState, action: /Action.history, child: HistoryReducer.init) - Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.frontpageState, action: \.frontpage, child: FrontpageReducer.init) + Scope(state: \.toplistsState, action: \.toplists, child: ToplistsReducer.init) + Scope(state: \.popularState, action: \.popular, child: PopularReducer.init) + Scope(state: \.watchedState, action: \.watched, child: WatchedReducer.init) + Scope(state: \.historyState, action: \.history, child: HistoryReducer.init) + Scope(state: \.detailState.wrappedValue!, action: \.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Home/HomeView.swift b/EhPanda/View/Home/HomeView.swift index 1bf6eceb..3fde0f22 100644 --- a/EhPanda/View/Home/HomeView.swift +++ b/EhPanda/View/Home/HomeView.swift @@ -73,13 +73,15 @@ struct HomeView: View { } } .opacity(store.popularGalleries.isEmpty ? 0 : 1).zIndex(2) + LoadingView() .opacity( store.popularLoadingState == .loading && store.popularGalleries.isEmpty ? 1 : 0 ) .zIndex(0) - let error = (/LoadingState.failed).extract(from: store.popularLoadingState) + + let error = store.popularLoadingState.failed ErrorView(error: error ?? .unknown) { store.send(.fetchAllGalleries) } @@ -98,7 +100,7 @@ struct HomeView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: $store.route, case: /HomeReducer.Route.detail) { route in + .sheet(item: $store.route.sending(\.setNavigation).detail, id: \.self) { route in NavigationView { DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), @@ -137,7 +139,7 @@ private extension HomeView { sectionLink } var detailViewLink: some View { - NavigationLink(unwrapping: $store.route, case: /HomeReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: \.detail) { route in DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -146,7 +148,7 @@ private extension HomeView { } } var miscGridLink: some View { - NavigationLink(unwrapping: $store.route, case: /HomeReducer.Route.misc) { route in + NavigationLink(unwrapping: $store.route, case: \.misc) { route in switch route.wrappedValue { case .popular: PopularView( @@ -167,7 +169,7 @@ private extension HomeView { } } var sectionLink: some View { - NavigationLink(unwrapping: $store.route, case: /HomeReducer.Route.section) { route in + NavigationLink(unwrapping: $store.route, case: \.section) { route in switch route.wrappedValue { case .frontpage: FrontpageView( diff --git a/EhPanda/View/Home/Popular/PopularReducer.swift b/EhPanda/View/Home/Popular/PopularReducer.swift index b1444906..6e56f3ca 100644 --- a/EhPanda/View/Home/Popular/PopularReducer.swift +++ b/EhPanda/View/Home/Popular/PopularReducer.swift @@ -9,9 +9,9 @@ import ComposableArchitecture @Reducer struct PopularReducer { - @CasePathable + @dynamicMemberLookup @CasePathable enum Route: Equatable { - case filters + case filters(EquatableVoid = .unique) case detail(String) } @@ -112,11 +112,11 @@ struct PopularReducer { } .haptics( unwrapping: \.route, - case: /Route.filters, + case: \.filters, hapticsClient: hapticsClient ) - Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) - Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.filtersState, action: \.filters, child: FiltersReducer.init) + Scope(state: \.detailState.wrappedValue!, action: \.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Home/Popular/PopularView.swift b/EhPanda/View/Home/Popular/PopularView.swift index 4044af41..cebfaf22 100644 --- a/EhPanda/View/Home/Popular/PopularView.swift +++ b/EhPanda/View/Home/Popular/PopularView.swift @@ -39,7 +39,7 @@ struct PopularView: View { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) - .sheet(unwrapping: $store.route, case: /PopularReducer.Route.filters) { _ in + .sheet(item: $store.route.sending(\.setNavigation).filters) { _ in FiltersView(store: store.scope(state: \.filtersState, action: \.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } @@ -57,7 +57,7 @@ struct PopularView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: $store.route, case: /PopularReducer.Route.detail) { route in + .sheet(item: $store.route.sending(\.setNavigation).detail, id: \.self) { route in NavigationView { DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), @@ -74,7 +74,7 @@ struct PopularView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: $store.route, case: /PopularReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: \.detail) { route in DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -86,7 +86,7 @@ struct PopularView: View { private func toolbar() -> some ToolbarContent { CustomToolbarItem { FiltersButton(hideText: true) { - store.send(.setNavigation(.filters)) + store.send(.setNavigation(.filters())) } } } diff --git a/EhPanda/View/Home/Toplists/ToplistsReducer.swift b/EhPanda/View/Home/Toplists/ToplistsReducer.swift index c48cf9ed..52da374f 100644 --- a/EhPanda/View/Home/Toplists/ToplistsReducer.swift +++ b/EhPanda/View/Home/Toplists/ToplistsReducer.swift @@ -9,7 +9,7 @@ import ComposableArchitecture @Reducer struct ToplistsReducer { - @CasePathable + @dynamicMemberLookup @CasePathable enum Route: Equatable { case detail(String) } @@ -216,6 +216,6 @@ struct ToplistsReducer { } } - Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.detailState.wrappedValue!, action: \.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Home/Toplists/ToplistsView.swift b/EhPanda/View/Home/Toplists/ToplistsView.swift index 1772a39b..ca63ee79 100644 --- a/EhPanda/View/Home/Toplists/ToplistsView.swift +++ b/EhPanda/View/Home/Toplists/ToplistsView.swift @@ -68,7 +68,7 @@ struct ToplistsView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: $store.route, case: /ToplistsReducer.Route.detail) { route in + .sheet(item: $store.route.sending(\.setNavigation).detail, id: \.self) { route in NavigationView { DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), @@ -85,7 +85,7 @@ struct ToplistsView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: $store.route, case: /ToplistsReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: \.detail) { route in DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, diff --git a/EhPanda/View/Home/Watched/WatchedReducer.swift b/EhPanda/View/Home/Watched/WatchedReducer.swift index 1955541d..3f63d345 100644 --- a/EhPanda/View/Home/Watched/WatchedReducer.swift +++ b/EhPanda/View/Home/Watched/WatchedReducer.swift @@ -11,8 +11,8 @@ import ComposableArchitecture struct WatchedReducer { @CasePathable enum Route: Equatable { - case filters - case quickSearch + case filters(EquatableVoid = .init()) + case quickSearch(EquatableVoid = .init()) case detail(String) } @@ -179,17 +179,17 @@ struct WatchedReducer { } .haptics( unwrapping: \.route, - case: /Route.quickSearch, + case: \.quickSearch, hapticsClient: hapticsClient ) .haptics( unwrapping: \.route, - case: /Route.filters, + case: \.filters, hapticsClient: hapticsClient ) - Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) - Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) - Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.filtersState, action: \.filters, child: FiltersReducer.init) + Scope(state: \.quickSearchState, action: \.quickSearch, child: QuickSearchReducer.init) + Scope(state: \.detailState.wrappedValue!, action: \.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Home/Watched/WatchedView.swift b/EhPanda/View/Home/Watched/WatchedView.swift index 7b249503..10e8946f 100644 --- a/EhPanda/View/Home/Watched/WatchedView.swift +++ b/EhPanda/View/Home/Watched/WatchedView.swift @@ -47,7 +47,7 @@ struct WatchedView: View { NotLoginView(action: { store.send(.onNotLoginViewButtonTapped) }) } } - .sheet(unwrapping: $store.route, case: /WatchedReducer.Route.quickSearch) { _ in + .sheet(item: $store.route.sending(\.setNavigation).quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickSearchState, action: \.quickSearch) ) { keyword in @@ -57,7 +57,7 @@ struct WatchedView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: $store.route, case: /WatchedReducer.Route.filters) { _ in + .sheet(item: $store.route.sending(\.setNavigation).filters) { _ in FiltersView(store: store.scope(state: \.filtersState, action: \.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } @@ -84,7 +84,7 @@ struct WatchedView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: $store.route, case: /WatchedReducer.Route.detail) { route in + .sheet(item: $store.route.sending(\.setNavigation).detail, id: \.self) { route in NavigationView { DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), @@ -101,7 +101,7 @@ struct WatchedView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: $store.route, case: /WatchedReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: \.detail) { route in DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -114,10 +114,10 @@ struct WatchedView: View { CustomToolbarItem { ToolbarFeaturesMenu { FiltersButton { - store.send(.setNavigation(.filters)) + store.send(.setNavigation(.filters())) } QuickSearchButton { - store.send(.setNavigation(.quickSearch)) + store.send(.setNavigation(.quickSearch())) } } } diff --git a/EhPanda/View/Migration/MigrationView.swift b/EhPanda/View/Migration/MigrationView.swift index c046e0e0..f26f461f 100644 --- a/EhPanda/View/Migration/MigrationView.swift +++ b/EhPanda/View/Migration/MigrationView.swift @@ -26,7 +26,7 @@ struct MigrationView: View { reversedPrimary.ignoresSafeArea() LoadingView(title: L10n.Localizable.LoadingView.Title.preparingDatabase) .opacity(store.databaseState == .loading ? 1 : 0) - let error = (/LoadingState.failed).extract(from: store.databaseState) + let error = store.databaseState.failed let errorNonNil = error ?? .databaseCorrupted(nil) AlertView(symbol: errorNonNil.symbol, message: errorNonNil.localizedDescription) { AlertViewButton(title: L10n.Localizable.ErrorView.Button.dropDatabase) { @@ -35,7 +35,7 @@ struct MigrationView: View { .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.dropDatabase, unwrapping: $store.route, - case: /MigrationReducer.Route.dropDialog + case: \.dropDialog ) { Button(L10n.Localizable.ConfirmationDialog.Button.dropDatabase, role: .destructive) { store.send(.dropDatabase) diff --git a/EhPanda/View/Reading/ReadingReducer.swift b/EhPanda/View/Reading/ReadingReducer.swift index b2f5f4ea..b747f3d9 100644 --- a/EhPanda/View/Reading/ReadingReducer.swift +++ b/EhPanda/View/Reading/ReadingReducer.swift @@ -14,8 +14,8 @@ struct ReadingReducer { @CasePathable enum Route: Equatable { case hud - case share(ShareItem) - case readingSetting + case share(IdentifiableBox) + case readingSetting(EquatableVoid = .init()) } enum ShareItem: Equatable { @@ -303,9 +303,9 @@ struct ReadingReducer { } case .share(let isAnimated): if isAnimated, let data = image.kf.data(format: .GIF) { - return .send(.setNavigation(.share(.data(data)))) + return .send(.setNavigation(.share(.init(value: .data(data))))) } else { - return .send(.setNavigation(.share(.image(image)))) + return .send(.setNavigation(.share(.init(value: .image(image))))) } } } else { @@ -635,12 +635,12 @@ struct ReadingReducer { } .haptics( unwrapping: \.route, - case: /Route.readingSetting, + case: \.readingSetting, hapticsClient: hapticsClient ) .haptics( unwrapping: \.route, - case: /Route.share, + case: \.share, hapticsClient: hapticsClient ) } diff --git a/EhPanda/View/Reading/ReadingView.swift b/EhPanda/View/Reading/ReadingView.swift index 88b289dd..5522421a 100644 --- a/EhPanda/View/Reading/ReadingView.swift +++ b/EhPanda/View/Reading/ReadingView.swift @@ -39,157 +39,185 @@ struct ReadingView: View { } var body: some View { + changeTriggers(content: { content }) + .sheet(item: $store.route.sending(\.setNavigation).readingSetting) { _ in + NavigationView { + ReadingSettingView( + readingDirection: $setting.readingDirection, + prefetchLimit: $setting.prefetchLimit, + enablesLandscape: $setting.enablesLandscape, + contentDividerHeight: $setting.contentDividerHeight, + maximumScaleFactor: $setting.maximumScaleFactor, + doubleTapScaleFactor: $setting.doubleTapScaleFactor + ) + .toolbar { + CustomToolbarItem(placement: .cancellationAction) { + if !DeviceUtil.isPad && DeviceUtil.isLandscape { + Button { + store.send(.setNavigation(nil)) + } label: { + Image(systemSymbol: .chevronDown) + } + } + } + } + } + .accentColor(setting.accentColor) + .tint(setting.accentColor) + .autoBlur(radius: blurRadius) + .navigationViewStyle(.stack) + } + .sheet(item: $store.route.sending(\.setNavigation).share) { shareItemBox in + ActivityView(activityItems: [shareItemBox.wrappedValue.associatedValue]) + .accentColor(setting.accentColor) + .autoBlur(radius: blurRadius) + } + .progressHUD( + config: store.hudConfig, + unwrapping: $store.route, + case: \.hud + ) + + .animation(.linear(duration: 0.1), value: gestureHandler.offset) + .animation(.default, value: liveTextHandler.enablesLiveText) + .animation(.default, value: liveTextHandler.liveTextGroups) + .animation(.default, value: gestureHandler.scale) + .animation(.default, value: store.showsPanel) + .statusBar(hidden: !store.showsPanel) + .onDisappear { + liveTextHandler.cancelRequests() + setAutoPlayPolocy(.off) + } + .onAppear { store.send(.onAppear(gid, setting.enablesLandscape)) } + } + + var content: some View { ZStack { backgroundColor.ignoresSafeArea() + ZStack { if setting.readingDirection == .vertical { AdvancedList( - page: page, data: store.state.containerDataSource(setting: setting), - id: \.self, spacing: setting.contentDividerHeight, + page: page, + data: store.state.containerDataSource(setting: setting), + id: \.self, + spacing: setting.contentDividerHeight, gesture: SimultaneousGesture(magnificationGesture, tapGesture), content: imageStack ) .disabled(gestureHandler.scale != 1) } else { Pager( - page: page, data: store.state.containerDataSource(setting: setting), - id: \.self, content: imageStack + page: page, + data: store.state.containerDataSource(setting: setting), + id: \.self, + content: imageStack ) .horizontal(setting.readingDirection == .rightToLeft ? .endToStart : .startToEnd) - .swipeInteractionArea(.allAvailable).allowsDragging(gestureHandler.scale == 1) + .swipeInteractionArea(.allAvailable) + .allowsDragging(gestureHandler.scale == 1) } } .scaleEffect(gestureHandler.scale, anchor: gestureHandler.scaleAnchor) - .offset(gestureHandler.offset).gesture(tapGesture).gesture(dragGesture) - .gesture(magnificationGesture).ignoresSafeArea() + .offset(gestureHandler.offset) + .gesture(tapGesture) + .gesture(dragGesture) + .gesture(magnificationGesture) + .ignoresSafeArea() .id(store.databaseLoadingState) .id(store.forceRefreshID) + ControlPanel( showsPanel: $store.showsPanel, showsSliderPreview: $store.showsSliderPreview, sliderValue: $pageHandler.sliderValue, setting: $setting, enablesLiveText: $liveTextHandler.enablesLiveText, autoPlayPolicy: .init(get: { autoPlayHandler.policy }, set: { setAutoPlayPolocy($0) }), - range: 1...Float(store.gallery.pageCount), previewURLs: store.previewURLs, + range: 1...Float(store.gallery.pageCount), + previewURLs: store.previewURLs, dismissGesture: controlPanelDismissGesture, dismissAction: { store.send(.onPerformDismiss) }, - navigateSettingAction: { store.send(.setNavigation(.readingSetting)) }, + navigateSettingAction: { store.send(.setNavigation(.readingSetting())) }, reloadAllImagesAction: { store.send(.reloadAllWebImages) }, retryAllFailedImagesAction: { store.send(.retryAllFailedWebImages) }, fetchPreviewURLsAction: { store.send(.fetchPreviewURLs($0)) } ) } - .sheet(unwrapping: $store.route, case: /ReadingReducer.Route.readingSetting) { _ in - NavigationView { - ReadingSettingView( - readingDirection: $setting.readingDirection, - prefetchLimit: $setting.prefetchLimit, - enablesLandscape: $setting.enablesLandscape, - contentDividerHeight: $setting.contentDividerHeight, - maximumScaleFactor: $setting.maximumScaleFactor, - doubleTapScaleFactor: $setting.doubleTapScaleFactor + } + + @ViewBuilder + private func changeTriggers(@ViewBuilder content: () -> Content) -> some View { + content() + // Page + .onChange(of: page.index) { _, newValue in + Logger.info("page.index changed", context: ["pageIndex": newValue]) + let newValue = pageHandler.mapFromPager( + index: newValue, pageCount: store.gallery.pageCount, setting: setting ) - .toolbar { - CustomToolbarItem(placement: .cancellationAction) { - if !DeviceUtil.isPad && DeviceUtil.isLandscape { - Button { - store.send(.setNavigation(nil)) - } label: { - Image(systemSymbol: .chevronDown) - } - } - } + pageHandler.sliderValue = .init(newValue) + if store.databaseLoadingState == .idle { + store.send(.syncReadingProgress(.init(newValue))) } } - .accentColor(setting.accentColor).tint(setting.accentColor) - .autoBlur(radius: blurRadius).navigationViewStyle(.stack) - } - .sheet(unwrapping: $store.route, case: /ReadingReducer.Route.share) { route in - ActivityView(activityItems: [route.wrappedValue.associatedValue]) - .accentColor(setting.accentColor).autoBlur(radius: blurRadius) - } - .progressHUD( - config: store.hudConfig, - unwrapping: $store.route, - case: /ReadingReducer.Route.hud - ) - - // Page - .onChange(of: page.index) { _, newValue in - Logger.info("page.index changed", context: ["pageIndex": newValue]) - let newValue = pageHandler.mapFromPager( - index: newValue, pageCount: store.gallery.pageCount, setting: setting - ) - pageHandler.sliderValue = .init(newValue) - if store.databaseLoadingState == .idle { - store.send(.syncReadingProgress(.init(newValue))) - } - } - .onChange(of: pageHandler.sliderValue) { _, newValue in - Logger.info("pageHandler.sliderValue changed", context: ["sliderValue": newValue]) - if !store.showsSliderPreview { - setPageIndex(sliderValue: newValue) + .onChange(of: pageHandler.sliderValue) { _, newValue in + Logger.info("pageHandler.sliderValue changed", context: ["sliderValue": newValue]) + if !store.showsSliderPreview { + setPageIndex(sliderValue: newValue) + } } - } - .onChange(of: store.showsSliderPreview) { _, newValue in - Logger.info("store.showsSliderPreview changed", context: ["isShown": newValue]) - if !newValue { setPageIndex(sliderValue: pageHandler.sliderValue) } - setAutoPlayPolocy(.off) - } - .onChange(of: store.readingProgress) { _, newValue in - Logger.info("store.readingProgress changed", context: ["readingProgress": newValue]) - pageHandler.sliderValue = .init(newValue) - } - - // AutoPlay - .onChange(of: store.route) { _, newValue in - Logger.info("store.route changed", context: ["route": newValue]) - if ![.hud, .none].contains(newValue) { + .onChange(of: store.showsSliderPreview) { _, newValue in + Logger.info("store.showsSliderPreview changed", context: ["isShown": newValue]) + if !newValue { setPageIndex(sliderValue: pageHandler.sliderValue) } setAutoPlayPolocy(.off) } - } + .onChange(of: store.readingProgress) { _, newValue in + Logger.info("store.readingProgress changed", context: ["readingProgress": newValue]) + pageHandler.sliderValue = .init(newValue) + } - // LiveText - .onChange(of: liveTextHandler.enablesLiveText) { _, newValue in - Logger.info("liveTextHandler.enablesLiveText changed", context: ["isEnabled": newValue]) - if newValue { store.webImageLoadSuccessIndices.forEach(analyzeImageForLiveText) } - } - .onChange(of: store.webImageLoadSuccessIndices) { _, newValue in - Logger.info("store.webImageLoadSuccessIndices changed", context: [ - "count": store.webImageLoadSuccessIndices.count - ]) - if liveTextHandler.enablesLiveText { - newValue.forEach(analyzeImageForLiveText) + // AutoPlay + .onChange(of: store.route) { _, newValue in + Logger.info("store.route changed", context: ["route": newValue]) + if ![.hud, .none].contains(newValue) { + setAutoPlayPolocy(.off) + } } - } - // Orientation - .onChange(of: setting.enablesLandscape) { _, newValue in - Logger.info("setting.enablesLandscape changed", context: ["newValue": newValue]) - store.send(.setOrientationPortrait(!newValue)) - } + // LiveText + .onChange(of: liveTextHandler.enablesLiveText) { _, newValue in + Logger.info("liveTextHandler.enablesLiveText changed", context: ["isEnabled": newValue]) + if newValue { store.webImageLoadSuccessIndices.forEach(analyzeImageForLiveText) } + } + .onChange(of: store.webImageLoadSuccessIndices) { _, newValue in + Logger.info("store.webImageLoadSuccessIndices changed", context: [ + "count": store.webImageLoadSuccessIndices.count + ]) + if liveTextHandler.enablesLiveText { + newValue.forEach(analyzeImageForLiveText) + } + } - .animation(.linear(duration: 0.1), value: gestureHandler.offset) - .animation(.default, value: liveTextHandler.enablesLiveText) - .animation(.default, value: liveTextHandler.liveTextGroups) - .animation(.default, value: gestureHandler.scale) - .animation(.default, value: store.showsPanel) - .statusBar(hidden: !store.showsPanel) - .onDisappear { - liveTextHandler.cancelRequests() - setAutoPlayPolocy(.off) - } - .onAppear { store.send(.onAppear(gid, setting.enablesLandscape)) } + // Orientation + .onChange(of: setting.enablesLandscape) { _, newValue in + Logger.info("setting.enablesLandscape changed", context: ["newValue": newValue]) + store.send(.setOrientationPortrait(!newValue)) + } } @ViewBuilder private func imageStack(index: Int) -> some View { let imageStackConfig = store.state.imageContainerConfigs(index: index, setting: setting) let isDualPage = setting.enablesDualPageMode && setting.readingDirection != .vertical && DeviceUtil.isLandscape HorizontalImageStack( - index: index, isDualPage: isDualPage, isDatabaseLoading: store.databaseLoadingState != .idle, - backgroundColor: backgroundColor, config: imageStackConfig, imageURLs: store.imageURLs, - originalImageURLs: store.originalImageURLs, loadingStates: store.imageURLLoadingStates, - enablesLiveText: liveTextHandler.enablesLiveText, liveTextGroups: liveTextHandler.liveTextGroups, + index: index, + isDualPage: isDualPage, + isDatabaseLoading: store.databaseLoadingState != .idle, + backgroundColor: backgroundColor, + config: imageStackConfig, + imageURLs: store.imageURLs, + originalImageURLs: store.originalImageURLs, + loadingStates: store.imageURLLoadingStates, + enablesLiveText: liveTextHandler.enablesLiveText, + liveTextGroups: liveTextHandler.liveTextGroups, focusedLiveTextGroup: liveTextHandler.focusedLiveTextGroup, liveTextTapAction: liveTextHandler.setFocusedLiveTextGroup, fetchAction: { store.send(.fetchImageURLs($0)) }, @@ -542,7 +570,7 @@ private struct ImageContainer: View { } } private func reloadImage() { - if let error = (/LoadingState.failed).extract(from: loadingState) { + if let error = loadingState.failed { if case .webImageFailed = error { loadRetryAction(index) } else { diff --git a/EhPanda/View/Search/SearchReducer.swift b/EhPanda/View/Search/SearchReducer.swift index 239727b4..c0c1df19 100644 --- a/EhPanda/View/Search/SearchReducer.swift +++ b/EhPanda/View/Search/SearchReducer.swift @@ -11,8 +11,8 @@ import ComposableArchitecture struct SearchReducer { @CasePathable enum Route: Equatable { - case filters - case quickSearch + case filters(EquatableVoid = .init()) + case quickSearch(EquatableVoid = .init()) case detail(String) } @@ -185,17 +185,17 @@ struct SearchReducer { } .haptics( unwrapping: \.route, - case: /Route.quickSearch, + case: \.quickSearch, hapticsClient: hapticsClient ) .haptics( unwrapping: \.route, - case: /Route.filters, + case: \.filters, hapticsClient: hapticsClient ) - Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) - Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) - Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.filtersState, action: \.filters, child: FiltersReducer.init) + Scope(state: \.quickSearchState, action: \.quickSearch, child: QuickSearchReducer.init) + Scope(state: \.detailState.wrappedValue!, action: \.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Search/SearchRootReducer.swift b/EhPanda/View/Search/SearchRootReducer.swift index 92b74755..6444fc9b 100644 --- a/EhPanda/View/Search/SearchRootReducer.swift +++ b/EhPanda/View/Search/SearchRootReducer.swift @@ -12,8 +12,8 @@ struct SearchRootReducer { @CasePathable enum Route: Equatable { case search - case filters - case quickSearch + case filters(EquatableVoid = .init()) + case quickSearch(EquatableVoid = .init()) case detail(String) } @@ -188,18 +188,18 @@ struct SearchRootReducer { } .haptics( unwrapping: \.route, - case: /Route.quickSearch, + case: \.quickSearch, hapticsClient: hapticsClient ) .haptics( unwrapping: \.route, - case: /Route.filters, + case: \.filters, hapticsClient: hapticsClient ) - Scope(state: \.searchState, action: /Action.search, child: SearchReducer.init) - Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) - Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) - Scope(state: \.detailState.wrappedValue!, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.searchState, action: \.search, child: SearchReducer.init) + Scope(state: \.filtersState, action: \.filters, child: FiltersReducer.init) + Scope(state: \.quickSearchState, action: \.quickSearch, child: QuickSearchReducer.init) + Scope(state: \.detailState.wrappedValue!, action: \.detail, child: DetailReducer.init) } } diff --git a/EhPanda/View/Search/SearchRootView.swift b/EhPanda/View/Search/SearchRootView.swift index 90bee1ab..3f701626 100644 --- a/EhPanda/View/Search/SearchRootView.swift +++ b/EhPanda/View/Search/SearchRootView.swift @@ -35,7 +35,7 @@ struct SearchRootView: View { historyGalleries: store.historyGalleries, quickSearchWords: store.quickSearchWords, navigateGalleryAction: { store.send(.setNavigation(.detail($0))) }, - navigateQuickSearchAction: { store.send(.setNavigation(.quickSearch)) }, + navigateQuickSearchAction: { store.send(.setNavigation(.quickSearch())) }, searchKeywordAction: { keyword in store.send(.setKeyword(keyword)) store.send(.setNavigation(.search)) @@ -43,11 +43,11 @@ struct SearchRootView: View { removeKeywordAction: { store.send(.removeHistoryKeyword($0)) } ) } - .sheet(unwrapping: $store.route, case: /SearchRootReducer.Route.filters) { _ in + .sheet(item: $store.route.sending(\.setNavigation).filters) { _ in FiltersView(store: store.scope(state: \.filtersState, action: \.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } - .sheet(unwrapping: $store.route, case: /SearchRootReducer.Route.quickSearch) { _ in + .sheet(item: $store.route.sending(\.setNavigation).quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickSearchState, action: \.quickSearch) ) { keyword in @@ -80,12 +80,15 @@ struct SearchRootView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: $store.route, case: /SearchRootReducer.Route.detail) { route in + .sheet(item: $store.route.sending(\.setNavigation).detail, id: \.self) { gid in NavigationView { DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), - gid: route.wrappedValue, user: user, setting: $setting, - blurRadius: blurRadius, tagTranslator: tagTranslator + gid: gid, + user: user, + setting: $setting, + blurRadius: blurRadius, + tagTranslator: tagTranslator ) } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) @@ -100,10 +103,10 @@ struct SearchRootView: View { CustomToolbarItem(tint: .primary) { ToolbarFeaturesMenu(symbolRenderingMode: .hierarchical) { FiltersButton { - store.send(.setNavigation(.filters)) + store.send(.setNavigation(.filters())) } QuickSearchButton { - store.send(.setNavigation(.quickSearch)) + store.send(.setNavigation(.quickSearch())) } } } @@ -118,7 +121,7 @@ private extension SearchRootView { searchViewLink } var detailViewLink: some View { - NavigationLink(unwrapping: $store.route, case: /SearchRootReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: \.detail) { route in DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -127,7 +130,7 @@ private extension SearchRootView { } } var searchViewLink: some View { - NavigationLink(unwrapping: $store.route, case: /SearchRootReducer.Route.search) { _ in + NavigationLink(unwrapping: $store.route, case: \.search) { _ in SearchView( store: store.scope(state: \.searchState, action: \.search), keyword: store.keyword, user: user, setting: $setting, diff --git a/EhPanda/View/Search/SearchView.swift b/EhPanda/View/Search/SearchView.swift index e86ae28b..0411efd3 100644 --- a/EhPanda/View/Search/SearchView.swift +++ b/EhPanda/View/Search/SearchView.swift @@ -43,7 +43,7 @@ struct SearchView: View { tagTranslator.lookup(word: $0, returnOriginal: !setting.translatesTags) } ) - .sheet(unwrapping: $store.route, case: /SearchReducer.Route.quickSearch) { _ in + .sheet(item: $store.route.sending(\.setNavigation).quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickSearchState, action: \.quickSearch) ) { keyword in @@ -53,7 +53,7 @@ struct SearchView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: $store.route, case: /SearchReducer.Route.filters) { _ in + .sheet(item: $store.route.sending(\.setNavigation).filters) { _ in FiltersView(store: store.scope(state: \.filtersState, action: \.filters)) .accentColor(setting.accentColor).autoBlur(radius: blurRadius) } @@ -80,7 +80,7 @@ struct SearchView: View { if DeviceUtil.isPad { content - .sheet(unwrapping: $store.route, case: /SearchReducer.Route.detail) { route in + .sheet(item: $store.route.sending(\.setNavigation).detail, id: \.self) { route in NavigationView { DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), @@ -97,7 +97,7 @@ struct SearchView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: $store.route, case: /SearchReducer.Route.detail) { route in + NavigationLink(unwrapping: $store.route, case: \.detail) { route in DetailView( store: store.scope(state: \.detailState.wrappedValue!, action: \.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -110,10 +110,10 @@ struct SearchView: View { CustomToolbarItem { ToolbarFeaturesMenu { FiltersButton { - store.send(.setNavigation(.filters)) + store.send(.setNavigation(.filters())) } QuickSearchButton { - store.send(.setNavigation(.quickSearch)) + store.send(.setNavigation(.quickSearch())) } } } diff --git a/EhPanda/View/Search/Support/QuickSearchView.swift b/EhPanda/View/Search/Support/QuickSearchView.swift index f39a503c..caa9d993 100644 --- a/EhPanda/View/Search/Support/QuickSearchView.swift +++ b/EhPanda/View/Search/Support/QuickSearchView.swift @@ -53,7 +53,7 @@ struct QuickSearchView: View { .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.delete, unwrapping: $store.route, - case: /QuickSearchReducer.Route.deleteWord, + case: \.deleteWord, matching: word ) { route in Button(L10n.Localizable.ConfirmationDialog.Button.delete, role: .destructive) { @@ -121,7 +121,7 @@ struct QuickSearchView: View { } } @ViewBuilder private var navigationLinks: some View { - NavigationLink(unwrapping: $store.route, case: /QuickSearchReducer.Route.newWord) { _ in + NavigationLink(unwrapping: $store.route, case: \.newWord) { _ in EditWordView( title: L10n.Localizable.QuickSearchView.Title.newWord, word: $store.editingWord, @@ -133,7 +133,7 @@ struct QuickSearchView: View { } ) } - NavigationLink(unwrapping: $store.route, case: /QuickSearchReducer.Route.editWord) { _ in + NavigationLink(unwrapping: $store.route, case: \.editWord) { _ in EditWordView( title: L10n.Localizable.QuickSearchView.Title.editWord, word: $store.editingWord, diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift index 8910b8cc..f187c801 100644 --- a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift @@ -11,7 +11,7 @@ import ComposableArchitecture @Reducer struct AccountSettingReducer { - @CasePathable + @dynamicMemberLookup @CasePathable enum Route: Equatable { case hud case login @@ -103,12 +103,12 @@ struct AccountSettingReducer { } .haptics( unwrapping: \.route, - case: /Route.webView, + case: \.webView, hapticsClient: hapticsClient ) - Scope(state: \.loginState, action: /Action.login, child: LoginReducer.init) - Scope(state: \.ehSettingState, action: /Action.ehSetting, child: EhSettingReducer.init) + Scope(state: \.loginState, action: \.login, child: LoginReducer.init) + Scope(state: \.ehSettingState, action: \.ehSetting, child: EhSettingReducer.init) } } diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift index 99bd5aa0..0301d3c2 100644 --- a/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift @@ -61,10 +61,10 @@ struct AccountSettingView: View { .progressHUD( config: store.hudConfig, unwrapping: $store.route, - case: /AccountSettingReducer.Route.hud + case: \.hud ) - .sheet(unwrapping: $store.route, case: /AccountSettingReducer.Route.webView) { route in - WebView(url: route.wrappedValue) + .sheet(item: $store.route.sending(\.setNavigation).webView, id: \.absoluteString) { url in + WebView(url: url) .autoBlur(radius: blurRadius) } .onAppear { store.send(.loadCookies) } @@ -76,13 +76,13 @@ struct AccountSettingView: View { // MARK: NavigationLinks private extension AccountSettingView { @ViewBuilder var navigationLinks: some View { - NavigationLink(unwrapping: $store.route, case: /AccountSettingReducer.Route.login) { _ in + NavigationLink(unwrapping: $store.route, case: \.login) { _ in LoginView( store: store.scope(state: \.loginState, action: \.login), bypassesSNIFiltering: bypassesSNIFiltering, blurRadius: blurRadius ) } - NavigationLink(unwrapping: $store.route, case: /AccountSettingReducer.Route.ehSetting) { _ in + NavigationLink(unwrapping: $store.route, case: \.ehSetting) { _ in EhSettingView( store: store.scope(state: \.ehSettingState, action: \.ehSetting), bypassesSNIFiltering: bypassesSNIFiltering, blurRadius: blurRadius @@ -130,7 +130,8 @@ private struct AccountSection: View { ) .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.logout, - unwrapping: $route, case: /AccountSettingReducer.Route.logout + unwrapping: $route, + case: \.logout ) { Button( L10n.Localizable.ConfirmationDialog.Button.logout, diff --git a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift index 1b45dbe4..f4395078 100644 --- a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift +++ b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift @@ -9,6 +9,7 @@ import ComposableArchitecture @Reducer struct AppearanceSettingReducer { + @CasePathable enum Route { case appIcon } diff --git a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift index b3c4f084..5bd579d8 100644 --- a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift +++ b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift @@ -105,7 +105,7 @@ struct AppearanceSettingView: View { } private var navigationLink: some View { - NavigationLink(unwrapping: $store.route, case: /AppearanceSettingReducer.Route.appIcon) { _ in + NavigationLink(unwrapping: $store.route, case: \.appIcon) { _ in AppIconView(appIconType: $appIconType) } } diff --git a/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift b/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift index 0a37e613..92d7ab89 100644 --- a/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift +++ b/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift @@ -148,7 +148,7 @@ struct EhSettingReducer { } .haptics( unwrapping: \.route, - case: /Route.webView, + case: \.webView, hapticsClient: hapticsClient ) } diff --git a/EhPanda/View/Setting/EhSetting/EhSettingView.swift b/EhPanda/View/Setting/EhSetting/EhSettingView.swift index 27c953a4..0ae77475 100644 --- a/EhPanda/View/Setting/EhSetting/EhSettingView.swift +++ b/EhPanda/View/Setting/EhSetting/EhSettingView.swift @@ -51,8 +51,8 @@ struct EhSettingView: View { store.send(.setDefaultProfile(profileSet)) } } - .sheet(unwrapping: $store.route, case: /EhSettingReducer.Route.webView) { route in - WebView(url: route.wrappedValue) + .sheet(item: $store.route.sending(\.setNavigation).webView, id: \.absoluteString) { url in + WebView(url: url) .autoBlur(radius: blurRadius) } .toolbar(content: toolbar) @@ -187,7 +187,7 @@ private struct EhProfileSection: View { .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.delete, unwrapping: $route, - case: /EhSettingReducer.Route.deleteProfile + case: \.deleteProfile ) { Button( L10n.Localizable.ConfirmationDialog.Button.delete, diff --git a/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift b/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift index a2151e0c..c8254b56 100644 --- a/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift +++ b/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift @@ -11,6 +11,7 @@ import ComposableArchitecture @Reducer struct GeneralSettingReducer { + @CasePathable enum Route { case logs case clearCache @@ -106,6 +107,6 @@ struct GeneralSettingReducer { } } - Scope(state: \.logsState, action: /Action.logs, child: LogsReducer.init) + Scope(state: \.logsState, action: \.logs, child: LogsReducer.init) } } diff --git a/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift b/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift index a1961c14..43dbabff 100644 --- a/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift +++ b/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift @@ -105,7 +105,7 @@ struct GeneralSettingView: View { .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.removeCustomTranslations, unwrapping: $store.route, - case: /GeneralSettingReducer.Route.removeCustomTranslations + case: \.removeCustomTranslations ) { Button(L10n.Localizable.ConfirmationDialog.Button.remove, role: .destructive) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { @@ -163,7 +163,7 @@ struct GeneralSettingView: View { .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.clear, unwrapping: $store.route, - case: /GeneralSettingReducer.Route.clearCache + case: \.clearCache ) { Button(L10n.Localizable.ConfirmationDialog.Button.clear, role: .destructive) { store.send(.clearWebImageCache) @@ -184,7 +184,7 @@ struct GeneralSettingView: View { } private var navigationLink: some View { - NavigationLink(unwrapping: $store.route, case: /GeneralSettingReducer.Route.logs) { _ in + NavigationLink(unwrapping: $store.route, case: \.logs) { _ in LogsView(store: store.scope(state: \.logsState, action: \.logs)) } } diff --git a/EhPanda/View/Setting/Login/LoginReducer.swift b/EhPanda/View/Setting/Login/LoginReducer.swift index e289703e..5d7db616 100644 --- a/EhPanda/View/Setting/Login/LoginReducer.swift +++ b/EhPanda/View/Setting/Login/LoginReducer.swift @@ -14,6 +14,7 @@ struct LoginReducer { case login } + @CasePathable enum Route: Equatable { case webView(URL) } @@ -98,7 +99,7 @@ struct LoginReducer { } .haptics( unwrapping: \.route, - case: /Route.webView, + case: \.webView, hapticsClient: hapticsClient ) } diff --git a/EhPanda/View/Setting/Login/LoginView.swift b/EhPanda/View/Setting/Login/LoginView.swift index e9ee2188..2b13c1c6 100644 --- a/EhPanda/View/Setting/Login/LoginView.swift +++ b/EhPanda/View/Setting/Login/LoginView.swift @@ -42,19 +42,26 @@ struct LoginView: View { ) } .padding(.horizontal, proxy.size.width * 0.2) + Button { store.send(.login) } label: { Image(systemSymbol: .chevronForwardCircleFill) } - .overlay { ProgressView().tint(nil).opacity(store.loginState == .loading ? 1 : 0) } - .imageScale(.large).font(.largeTitle).foregroundColor(store.loginButtonColor) + .overlay { + ProgressView() + .tint(nil) + .opacity(store.loginState == .loading ? 1 : 0) + } + .imageScale(.large) + .font(.largeTitle) + .foregroundColor(store.loginButtonColor) .disabled(store.loginButtonDisabled).padding(.top, 30) } } } .synchronize($store.focusedField, $focusedField) - .sheet(unwrapping: $store.route, case: /LoginReducer.Route.webView) { route in + .sheet(item: $store.route.sending(\.setNavigation).webView, id: \.absoluteString) { route in WebView(url: route.wrappedValue) { store.send(.loginDone(.success(nil))) } diff --git a/EhPanda/View/Setting/Logs/LogsView.swift b/EhPanda/View/Setting/Logs/LogsView.swift index 16b6550d..4b2b8d8b 100644 --- a/EhPanda/View/Setting/Logs/LogsView.swift +++ b/EhPanda/View/Setting/Logs/LogsView.swift @@ -34,8 +34,10 @@ struct LogsView: View { .foregroundColor(.primary) } .opacity(store.logs.isEmpty ? 0 : 1) + LoadingView().opacity(store.loadingState == .loading && store.logs.isEmpty ? 1 : 0) - let error = (/LoadingState.failed).extract(from: store.loadingState) + + let error = store.loadingState.failed ErrorView(error: error ?? .notFound) { store.send(.fetchLogs) } @@ -54,7 +56,7 @@ struct LogsView: View { } private var navigationLink: some View { - NavigationLink(unwrapping: $store.route, case: /LogsReducer.Route.log) { route in + NavigationLink(unwrapping: $store.route, case: \.log) { route in LogView(log: route.wrappedValue) } } diff --git a/EhPanda/View/Setting/SettingReducer.swift b/EhPanda/View/Setting/SettingReducer.swift index 7fc0c0e2..91e6885b 100644 --- a/EhPanda/View/Setting/SettingReducer.swift +++ b/EhPanda/View/Setting/SettingReducer.swift @@ -507,8 +507,8 @@ struct SettingReducer { } } - Scope(state: \.accountSettingState, action: /Action.account, child: AccountSettingReducer.init) - Scope(state: \.generalSettingState, action: /Action.general, child: GeneralSettingReducer.init) - Scope(state: \.appearanceSettingState, action: /Action.appearance, child: AppearanceSettingReducer.init) + Scope(state: \.accountSettingState, action: \.account, child: AccountSettingReducer.init) + Scope(state: \.generalSettingState, action: \.general, child: GeneralSettingReducer.init) + Scope(state: \.appearanceSettingState, action: \.appearance, child: AppearanceSettingReducer.init) } } diff --git a/EhPanda/View/Setting/SettingView.swift b/EhPanda/View/Setting/SettingView.swift index f8b638e7..9a82573a 100644 --- a/EhPanda/View/Setting/SettingView.swift +++ b/EhPanda/View/Setting/SettingView.swift @@ -40,7 +40,7 @@ struct SettingView: View { // MARK: NavigationLinks private extension SettingView { @ViewBuilder var navigationLinks: some View { - NavigationLink(unwrapping: $store.route, case: /SettingReducer.Route.account) { _ in + NavigationLink(unwrapping: $store.route, case: \.account) { _ in AccountSettingView( store: store.scope(state: \.accountSettingState, action: \.account), galleryHost: $store.setting.galleryHost, @@ -50,7 +50,7 @@ private extension SettingView { ) .tint(store.setting.accentColor) } - NavigationLink(unwrapping: $store.route, case: /SettingReducer.Route.general) { _ in + NavigationLink(unwrapping: $store.route, case: \.general) { _ in GeneralSettingView( store: store.scope(state: \.generalSettingState, action: \.general), tagTranslatorLoadingState: store.tagTranslatorLoadingState, @@ -67,7 +67,7 @@ private extension SettingView { ) .tint(store.setting.accentColor) } - NavigationLink(unwrapping: $store.route, case: /SettingReducer.Route.appearance) { _ in + NavigationLink(unwrapping: $store.route, case: \.appearance) { _ in AppearanceSettingView( store: store.scope(state: \.appearanceSettingState, action: \.appearance), preferredColorScheme: $store.setting.preferredColorScheme, @@ -80,7 +80,7 @@ private extension SettingView { ) .tint(store.setting.accentColor) } - NavigationLink(unwrapping: $store.route, case: /SettingReducer.Route.reading) { _ in + NavigationLink(unwrapping: $store.route, case: \.reading) { _ in ReadingSettingView( readingDirection: $store.setting.readingDirection, prefetchLimit: $store.setting.prefetchLimit, @@ -91,13 +91,13 @@ private extension SettingView { ) .tint(store.setting.accentColor) } - NavigationLink(unwrapping: $store.route, case: /SettingReducer.Route.laboratory) { _ in + NavigationLink(unwrapping: $store.route, case: \.laboratory) { _ in LaboratorySettingView( bypassesSNIFiltering: $store.setting.bypassesSNIFiltering ) .tint(store.setting.accentColor) } - NavigationLink(unwrapping: $store.route, case: /SettingReducer.Route.about) { _ in + NavigationLink(unwrapping: $store.route, case: \.about) { _ in AboutView().tint(store.setting.accentColor) } } diff --git a/EhPanda/View/Support/Components/GenericList.swift b/EhPanda/View/Support/Components/GenericList.swift index 71e98d91..2b02c52b 100644 --- a/EhPanda/View/Support/Components/GenericList.swift +++ b/EhPanda/View/Support/Components/GenericList.swift @@ -58,10 +58,13 @@ struct GenericList: View { } } .opacity(loadingState == .idle ? 1 : 0).zIndex(2) - LoadingView().opacity(loadingState == .loading ? 1 : 0).zIndex(0) - let error = (/LoadingState.failed).extract(from: loadingState) - ErrorView(error: error ?? .unknown, action: fetchAction) - .opacity([.idle, .loading].contains(loadingState) ? 0 : 1).zIndex(1) + + LoadingView() + .opacity(loadingState == .loading ? 1 : 0).zIndex(0) + + ErrorView(error: loadingState.failed ?? .unknown, action: fetchAction) + .opacity([.idle, .loading].contains(loadingState) ? 0 : 1) + .zIndex(1) } .animation(.default, value: loadingState) .animation(.default, value: galleries) diff --git a/EhPanda/View/Support/Components/TagCloudView.swift b/EhPanda/View/Support/Components/TagCloudView.swift index 2d7c9753..e2522bc2 100644 --- a/EhPanda/View/Support/Components/TagCloudView.swift +++ b/EhPanda/View/Support/Components/TagCloudView.swift @@ -46,8 +46,8 @@ private extension TagCloudView { ForEach(data, id: id) { content in self.content(content) .padding([.trailing, .bottom], spacing) - .alignmentGuide(.leading, computeValue: { dimensions in - if abs(width - dimensions.width) > proxy.size.width { + .alignmentGuide(.leading, computeValue: { [proxyWidth = proxy.size.width] dimensions in + if abs(width - dimensions.width) > proxyWidth { width = 0 height -= dimensions.height } diff --git a/EhPanda/View/Support/FiltersReducer.swift b/EhPanda/View/Support/FiltersReducer.swift index 112ea0a4..03bb1908 100644 --- a/EhPanda/View/Support/FiltersReducer.swift +++ b/EhPanda/View/Support/FiltersReducer.swift @@ -9,6 +9,7 @@ import ComposableArchitecture @Reducer struct FiltersReducer { + @CasePathable enum Route { case resetFilters } diff --git a/EhPanda/View/Support/FiltersView.swift b/EhPanda/View/Support/FiltersView.swift index 483998bb..51562717 100644 --- a/EhPanda/View/Support/FiltersView.swift +++ b/EhPanda/View/Support/FiltersView.swift @@ -87,7 +87,8 @@ private struct BasicSection: View { } .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.reset, - unwrapping: $route, case: /FiltersReducer.Route.resetFilters + unwrapping: $route, + case: \.resetFilters ) { Button( L10n.Localizable.ConfirmationDialog.Button.reset, diff --git a/EhPanda/View/TabBar/TabBarView.swift b/EhPanda/View/TabBar/TabBarView.swift index ebd0ad53..d224e990 100644 --- a/EhPanda/View/TabBar/TabBarView.swift +++ b/EhPanda/View/TabBar/TabBarView.swift @@ -71,11 +71,11 @@ struct TabBarView: View { } .font(.system(size: 80)).opacity(store.appLockState.isAppLocked ? 1 : 0) } - .sheet(unwrapping: $store.appRouteState.route, case: /AppRouteReducer.Route.newDawn) { route in - NewDawnView(greeting: route.wrappedValue) + .sheet(item: $store.appRouteState.route.sending(\.appRoute.setNavigation).newDawn) { greeting in + NewDawnView(greeting: greeting) .autoBlur(radius: store.appLockState.blurRadius) } - .sheet(unwrapping: $store.appRouteState.route, case: /AppRouteReducer.Route.setting) { _ in + .sheet(item: $store.appRouteState.route.sending(\.appRoute.setNavigation).setting) { _ in SettingView( store: store.scope(state: \.settingState, action: \.setting), blurRadius: store.appLockState.blurRadius @@ -83,7 +83,7 @@ struct TabBarView: View { .accentColor(store.settingState.setting.accentColor) .autoBlur(radius: store.appLockState.blurRadius) } - .sheet(unwrapping: $store.appRouteState.route, case: /AppRouteReducer.Route.detail) { route in + .sheet(item: $store.appRouteState.route.sending(\.appRoute.setNavigation).detail, id: \.self) { route in NavigationView { DetailView( store: store.scope( @@ -104,7 +104,7 @@ struct TabBarView: View { .progressHUD( config: store.appRouteState.hudConfig, unwrapping: $store.appRouteState.route, - case: /AppRouteReducer.Route.hud + case: \.hud ) .onChange(of: scenePhase) { _, newValue in store.send(.onScenePhaseChange(newValue)) } .onOpenURL { store.send(.appRoute(.handleDeepLink($0))) }