From e2836ce0e4eca20bc85555014d314355dfefdaef Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sun, 9 Apr 2023 16:27:13 +0800 Subject: [PATCH 01/29] Update CI --- .github/workflows/deploy.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1ad012e4..4c4cfbdc 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,7 +5,7 @@ on: - main types: [closed] env: - DEVELOPER_DIR: /Applications/Xcode_14.1.app + DEVELOPER_DIR: /Applications/Xcode_14.2.app APP_VERSION: '2.6.1' SCHEME_NAME: 'EhPanda' ALTSTORE_JSON_PATH: './AltStore.json' @@ -34,7 +34,7 @@ jobs: run: xcodebuild -version - name: Run tests run: xcodebuild clean test -scheme ${{ env.SCHEME_NAME }} -sdk iphonesimulator - -destination 'platform=iOS Simulator,name=iPhone 14' + -destination 'platform=iOS Simulator,name=iPhone 14 Pro' - name: Bump version id: bump-version uses: yanamura/ios-bump-version@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 025be285..744fa29f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: Test on: [push] env: SCHEME_NAME: 'EhPanda' - DEVELOPER_DIR: /Applications/Xcode_14.1.app + DEVELOPER_DIR: /Applications/Xcode_14.2.app jobs: Test: runs-on: macos-12 @@ -16,4 +16,4 @@ jobs: - name: Run tests run: xcodebuild clean test -scheme ${{ env.SCHEME_NAME }} -sdk iphonesimulator - -destination 'platform=iOS Simulator,name=iPhone 14' + -destination 'platform=iOS Simulator,name=iPhone 14 Pro' From fb24be312763d637baded594e44cbe0ac3566320 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Mon, 8 May 2023 23:41:27 +0800 Subject: [PATCH 02/29] Update dependencies & fix build errors --- EhPanda.xcodeproj/project.pbxproj | 52 +++++-------- .../xcshareddata/swiftpm/Package.resolved | 75 +++++++++++-------- .../xcshareddata/xcschemes/EhPanda.xcscheme | 2 +- EhPanda/App/Tools/Clients/DFClient.swift | 1 + EhPanda/App/Tools/Clients/FileClient.swift | 1 + .../Tools/Clients/UserDefaultsClient.swift | 1 + .../Tools/Extensions/Reducer_Extension.swift | 7 -- .../SwiftUINavigation_Extension.swift | 4 +- .../View/Detail/DataFlow/ArchivesStore.swift | 1 + .../View/Detail/DataFlow/CommentsStore.swift | 1 + .../View/Detail/DataFlow/DetailStore.swift | 16 ++++ .../View/Detail/DataFlow/PreviewsStore.swift | 1 + EhPanda/View/Detail/DetailView.swift | 2 +- EhPanda/View/Home/DataFlow/HistoryStore.swift | 1 + EhPanda/View/Home/HomeView.swift | 4 +- EhPanda/View/Migration/MigrationStore.swift | 1 + EhPanda/View/Reading/ReadingView.swift | 2 +- .../DataFlow/AccountSettingStore.swift | 1 + .../Setting/DataFlow/EhSettingStore.swift | 1 + .../View/Setting/DataFlow/SettingStore.swift | 1 + 20 files changed, 95 insertions(+), 80 deletions(-) diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 70f00ef3..69e84c4d 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -63,7 +63,6 @@ AB2EB99F280251D600011A8A /* TTProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = AB2EB99E280251D600011A8A /* TTProgressHUD */; }; AB2EB9A2280251F600011A8A /* AlertKit in Frameworks */ = {isa = PBXBuildFile; productRef = AB2EB9A1280251F600011A8A /* AlertKit */; }; AB2EB9A52802521700011A8A /* DeprecatedAPI in Frameworks */ = {isa = PBXBuildFile; productRef = AB2EB9A42802521700011A8A /* DeprecatedAPI */; }; - AB2EB9AB2802525700011A8A /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = AB2EB9AA2802525700011A8A /* SwiftyBeaver */; }; AB3072D2276D734800EFF242 /* SubSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3072D1276D734800EFF242 /* SubSection.swift */; }; AB3072D4276E19AA00EFF242 /* FrontpageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3072D3276E19AA00EFF242 /* FrontpageView.swift */; }; AB31CD3027B666E200F40E0A /* TestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB31CD2F27B666E200F40E0A /* TestError.swift */; }; @@ -242,7 +241,6 @@ ABD49D6A277EEF73003D1A07 /* SettingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D69277EEF73003D1A07 /* SettingStore.swift */; }; ABD5FDD4263D05110021A4C6 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = ABD5FDD3263D05110021A4C6 /* .swiftlint.yml */; }; ABD7005926B1C31500DC59C9 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = ABD7005826B1C31500DC59C9 /* Kanna */; }; - ABD970B727A2A6BD001693B0 /* Rswift in Frameworks */ = {isa = PBXBuildFile; productRef = ABD970B627A2A6BD001693B0 /* Rswift */; }; ABD9770E27B65A7300983DE7 /* ListParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD9770D27B65A7300983DE7 /* ListParserTests.swift */; }; ABD9771027B65E3400983DE7 /* GalleryDetailParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD9770F27B65E3400983DE7 /* GalleryDetailParserTests.swift */; }; ABD9771327B6612400983DE7 /* GreetingParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD9771227B6612400983DE7 /* GreetingParserTests.swift */; }; @@ -276,6 +274,7 @@ ABF45AF725F3313D00ECB568 /* SettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF45ADD25F3313D00ECB568 /* SettingView.swift */; }; ABF75F3F25A19CD200544D29 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF75F3E25A19CD200544D29 /* User.swift */; }; ABF9720A26DE6E1300118887 /* GalleryDetailWithGreeting.html in Resources */ = {isa = PBXBuildFile; fileRef = ABF9720926DE6E1300118887 /* GalleryDetailWithGreeting.html */; }; + EAE63E2129E2A6330048C601 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = EAE63E2029E2A6330048C601 /* SwiftyBeaver */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -603,17 +602,16 @@ buildActionMask = 2147483647; files = ( AB2EB99F280251D600011A8A /* TTProgressHUD in Frameworks */, - ABD970B727A2A6BD001693B0 /* Rswift in Frameworks */, AB2EB9A52802521700011A8A /* DeprecatedAPI in Frameworks */, AB17574027678B3400FD64E2 /* UIImageColors in Frameworks */, AB2EB9A2280251F600011A8A /* AlertKit in Frameworks */, ABD7005926B1C31500DC59C9 /* Kanna in Frameworks */, AB60D0E9274C7ECE00F899AB /* WaterfallGrid in Frameworks */, ABC4A0792751B40E00968A4F /* Kingfisher in Frameworks */, + EAE63E2129E2A6330048C601 /* SwiftyBeaver in Frameworks */, AB26F59927ACDB4200AB3468 /* FilePicker in Frameworks */, AB6505A026B0027800F91E9D /* SwiftUIPager in Frameworks */, ABD49D5D277C6C9D003D1A07 /* SFSafeSymbols in Frameworks */, - AB2EB9AB2802525700011A8A /* SwiftyBeaver in Frameworks */, ABAC82FE26BC4A96009F5026 /* OpenCC in Frameworks */, AB86AC1027831AD100E61E6A /* ComposableArchitecture in Frameworks */, ABBB2636278FB888007B6149 /* SwiftUINavigation in Frameworks */, @@ -1432,13 +1430,12 @@ ABD49D5C277C6C9D003D1A07 /* SFSafeSymbols */, AB86AC0F27831AD100E61E6A /* ComposableArchitecture */, ABBB2635278FB888007B6149 /* SwiftUINavigation */, - ABD970B627A2A6BD001693B0 /* Rswift */, AB26F59827ACDB4200AB3468 /* FilePicker */, AB1FA94827C62BC80063EF55 /* CommonMark */, AB2EB99E280251D600011A8A /* TTProgressHUD */, AB2EB9A1280251F600011A8A /* AlertKit */, AB2EB9A42802521700011A8A /* DeprecatedAPI */, - AB2EB9AA2802525700011A8A /* SwiftyBeaver */, + EAE63E2029E2A6330048C601 /* SwiftyBeaver */, ); productName = EhPanda; productReference = ABC3C7542593696C00E0C11B /* EhPanda.app */; @@ -1468,8 +1465,9 @@ ABC3C74C2593696C00E0C11B /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1300; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1430; TargetAttributes = { AB5BE67526B95FDD007D4A55 = { CreatedOnToolsVersion = 13.0; @@ -1510,13 +1508,12 @@ ABD49D5B277C6C9D003D1A07 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, AB86AC0E27831AD100E61E6A /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, ABBB2634278FB888007B6149 /* XCRemoteSwiftPackageReference "swiftui-navigation" */, - ABD970B527A2A6BD001693B0 /* XCRemoteSwiftPackageReference "R.swift.Library" */, AB26F59727ACDB4200AB3468 /* XCRemoteSwiftPackageReference "FilePicker" */, AB1FA94727C62BC80063EF55 /* XCRemoteSwiftPackageReference "SwiftCommonMark" */, AB2EB99D280251D600011A8A /* XCRemoteSwiftPackageReference "TTProgressHUD" */, AB2EB9A0280251F600011A8A /* XCRemoteSwiftPackageReference "AlertKit" */, AB2EB9A32802521700011A8A /* XCRemoteSwiftPackageReference "DeprecatedAPI" */, - AB2EB9A92802525700011A8A /* XCRemoteSwiftPackageReference "SwiftyBeaver" */, + EAE63E1F29E2A6330048C601 /* XCRemoteSwiftPackageReference "SwiftyBeaver" */, ); productRefGroup = ABC3C7552593696C00E0C11B /* Products */; projectDirPath = ""; @@ -2307,20 +2304,12 @@ kind = branch; }; }; - AB2EB9A92802525700011A8A /* XCRemoteSwiftPackageReference "SwiftyBeaver" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/EhPanda-Team/SwiftyBeaver.git"; - requirement = { - branch = custom; - kind = branch; - }; - }; AB60D0E7274C7ECE00F899AB /* XCRemoteSwiftPackageReference "WaterfallGrid" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/paololeonardi/WaterfallGrid.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.1; + minimumVersion = 1.0.0; }; }; AB65059E26B0027800F91E9D /* XCRemoteSwiftPackageReference "SwiftUIPager" */ = { @@ -2336,7 +2325,7 @@ repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.32.0; + minimumVersion = 0.0.0; }; }; ABAC82FC26BC4866009F5026 /* XCRemoteSwiftPackageReference "SwiftyOpenCC" */ = { @@ -2352,7 +2341,7 @@ repositoryURL = "https://github.com/pointfreeco/swiftui-navigation.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.1.0; + minimumVersion = 0.0.0; }; }; ABC4A0772751B40E00968A4F /* XCRemoteSwiftPackageReference "Kingfisher" */ = { @@ -2360,15 +2349,15 @@ repositoryURL = "https://github.com/onevcat/Kingfisher.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 7.1.2; + minimumVersion = 7.0.0; }; }; ABD49D5B277C6C9D003D1A07 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols.git"; requirement = { - branch = stable; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 4.0.0; }; }; ABD7005726B1C31500DC59C9 /* XCRemoteSwiftPackageReference "Kanna" */ = { @@ -2379,12 +2368,12 @@ minimumVersion = 5.0.0; }; }; - ABD970B527A2A6BD001693B0 /* XCRemoteSwiftPackageReference "R.swift.Library" */ = { + EAE63E1F29E2A6330048C601 /* XCRemoteSwiftPackageReference "SwiftyBeaver" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/mac-cain13/R.swift.Library.git"; + repositoryURL = "https://github.com/SwiftyBeaver/SwiftyBeaver.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 5.4.0; + minimumVersion = 2.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -2425,11 +2414,6 @@ package = AB2EB9A32802521700011A8A /* XCRemoteSwiftPackageReference "DeprecatedAPI" */; productName = DeprecatedAPI; }; - AB2EB9AA2802525700011A8A /* SwiftyBeaver */ = { - isa = XCSwiftPackageProductDependency; - package = AB2EB9A92802525700011A8A /* XCRemoteSwiftPackageReference "SwiftyBeaver" */; - productName = SwiftyBeaver; - }; AB60D0E8274C7ECE00F899AB /* WaterfallGrid */ = { isa = XCSwiftPackageProductDependency; package = AB60D0E7274C7ECE00F899AB /* XCRemoteSwiftPackageReference "WaterfallGrid" */; @@ -2470,10 +2454,10 @@ package = ABD7005726B1C31500DC59C9 /* XCRemoteSwiftPackageReference "Kanna" */; productName = Kanna; }; - ABD970B627A2A6BD001693B0 /* Rswift */ = { + EAE63E2029E2A6330048C601 /* SwiftyBeaver */ = { isa = XCSwiftPackageProductDependency; - package = ABD970B527A2A6BD001693B0 /* XCRemoteSwiftPackageReference "R.swift.Library" */; - productName = Rswift; + package = EAE63E1F29E2A6330048C601 /* XCRemoteSwiftPackageReference "SwiftyBeaver" */; + productName = SwiftyBeaver; }; /* End XCSwiftPackageProductDependency section */ diff --git a/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9e0508b0..c1c2905d 100644 --- a/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "4cf088c29a20f52be0f2ca54992b492c54e0076b", - "version" : "0.5.3" + "revision" : "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab", + "version" : "0.9.1" } }, { @@ -59,35 +59,35 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Kingfisher.git", "state" : { - "revision" : "32e4acdf6971f58f5ad552389cf2d7d016334eaf", - "version" : "7.2.0" + "revision" : "af4be924ad984cf4d16f4ae4df424e79a443d435", + "version" : "7.6.2" } }, { - "identity" : "r.swift.library", + "identity" : "sfsafesymbols", "kind" : "remoteSourceControl", - "location" : "https://github.com/mac-cain13/R.swift.Library.git", + "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols.git", "state" : { - "revision" : "8998cfe77f4fce79ee6dfab0c88a7d551659d8fb", - "version" : "5.4.0" + "revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c", + "version" : "4.1.1" } }, { - "identity" : "sfsafesymbols", + "identity" : "swift-case-paths", "kind" : "remoteSourceControl", - "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols.git", + "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "branch" : "stable", - "revision" : "de65e5545623c32c86da259cb0987c381bc6a9e2" + "revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984", + "version" : "0.14.1" } }, { - "identity" : "swift-case-paths", + "identity" : "swift-clocks", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", + "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "241301b67d8551c26d8f09bd2c0e52cc49f18007", - "version" : "0.8.0" + "revision" : "20b25ca0dd88ebfb9111ec937814ddc5a8880172", + "version" : "0.2.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "48254824bb4248676bf7ce56014ff57b142b77eb", - "version" : "1.0.2" + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", "state" : { - "revision" : "2828dc44f6e3f81d84bcaba72c1ab1c0121d66f6", - "version" : "0.34.0" + "revision" : "3e8eee1efe99d06e99426d421733b858b332186b", + "version" : "0.52.0" } }, { @@ -113,8 +113,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "51698ece74ecf31959d3fa81733f0a5363ef1b4e", - "version" : "0.3.0" + "revision" : "805c57f32a5934ee420f30ba129f00aa8c7575a1", + "version" : "0.10.0" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "6bb1034e8a1bfbf46dfb766b6c09b7b17e1cba10", + "version" : "0.2.0" } }, { @@ -122,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-identified-collections", "state" : { - "revision" : "680bf440178a78a627b1c2c64c0855f6523ad5b9", - "version" : "0.3.2" + "revision" : "f52eee28bdc6065aa2f8424067e6f04c74bda6e6", + "version" : "0.7.1" } }, { @@ -140,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swiftui-navigation.git", "state" : { - "revision" : "2694c03284a368168b3e0b8d7ab52626802d2246", - "version" : "0.1.0" + "revision" : "47dd574b900ba5ba679f56ea00d4d282fc7305a6", + "version" : "0.7.1" } }, { @@ -149,17 +158,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/fermoya/SwiftUIPager.git", "state" : { - "revision" : "6e4170ef7cafd04b3d8ab8dc5b0469036b2bcd6a", - "version" : "2.4.0" + "revision" : "4ddc04c801aac143090bb14cf26603a3bf9c74cb", + "version" : "2.5.0" } }, { "identity" : "swiftybeaver", "kind" : "remoteSourceControl", - "location" : "https://github.com/EhPanda-Team/SwiftyBeaver.git", + "location" : "https://github.com/SwiftyBeaver/SwiftyBeaver.git", "state" : { - "branch" : "custom", - "revision" : "2e8a65567ca877a7fdf6a63ab53b2c49a927af87" + "revision" : "1080914828ef1c9ca9cd2bad50667b3d847dabff", + "version" : "2.0.0" } }, { @@ -203,8 +212,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "50a70a9d3583fe228ce672e8923010c8df2deddd", - "version" : "0.2.1" + "revision" : "ab8c9f45843694dd16be4297e6d44c0634fd9913", + "version" : "0.8.4" } } ], diff --git a/EhPanda.xcodeproj/xcshareddata/xcschemes/EhPanda.xcscheme b/EhPanda.xcodeproj/xcshareddata/xcschemes/EhPanda.xcscheme index 621870f0..e791012e 100644 --- a/EhPanda.xcodeproj/xcshareddata/xcschemes/EhPanda.xcscheme +++ b/EhPanda.xcodeproj/xcshareddata/xcschemes/EhPanda.xcscheme @@ -1,6 +1,6 @@ Reducer) -> Reducer { - var `self`: Reducer! - self = Reducer { state, action, environment in - reducer(self).run(&state, action, environment) - } - return self - } func onBecomeNonNil( unwrapping enum: @escaping (State) -> Enum?, case casePath: CasePath, diff --git a/EhPanda/App/Tools/Extensions/SwiftUINavigation_Extension.swift b/EhPanda/App/Tools/Extensions/SwiftUINavigation_Extension.swift index fa3113aa..45c35763 100644 --- a/EhPanda/App/Tools/Extensions/SwiftUINavigation_Extension.swift +++ b/EhPanda/App/Tools/Extensions/SwiftUINavigation_Extension.swift @@ -34,7 +34,9 @@ extension NavigationLink { } extension View { - @ViewBuilder func sheet( + @MainActor + @ViewBuilder + func sheet( unwrapping enum: Binding, case casePath: CasePath, onDismiss: (() -> Void)? = nil, diff --git a/EhPanda/View/Detail/DataFlow/ArchivesStore.swift b/EhPanda/View/Detail/DataFlow/ArchivesStore.swift index 4c7de883..60ae7d32 100644 --- a/EhPanda/View/Detail/DataFlow/ArchivesStore.swift +++ b/EhPanda/View/Detail/DataFlow/ArchivesStore.swift @@ -5,6 +5,7 @@ // Created by 荒木辰造 on R 4/01/19. // +import Foundation import TTProgressHUD import ComposableArchitecture diff --git a/EhPanda/View/Detail/DataFlow/CommentsStore.swift b/EhPanda/View/Detail/DataFlow/CommentsStore.swift index e1e94597..80fa5a37 100644 --- a/EhPanda/View/Detail/DataFlow/CommentsStore.swift +++ b/EhPanda/View/Detail/DataFlow/CommentsStore.swift @@ -5,6 +5,7 @@ // Created by 荒木辰造 on R 4/01/16. // +import Foundation import TTProgressHUD import ComposableArchitecture diff --git a/EhPanda/View/Detail/DataFlow/DetailStore.swift b/EhPanda/View/Detail/DataFlow/DetailStore.swift index 542506de..f4b63b95 100644 --- a/EhPanda/View/Detail/DataFlow/DetailStore.swift +++ b/EhPanda/View/Detail/DataFlow/DetailStore.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Foundation import ComposableArchitecture struct DetailState: Equatable { @@ -121,6 +122,21 @@ struct DetailEnvironment { let uiApplicationClient: UIApplicationClient } +extension Reducer { + static func recurse( + _ reducer: @escaping + (Reducer) + -> Reducer + ) + -> Reducer { + var `self`: Reducer! + self = Reducer { state, action, environment in + reducer(self).run(&state, action, environment) + } + return self + } +} + let detailReducer = Reducer.recurse { (self) in Reducer.combine( .init { state, action, environment in diff --git a/EhPanda/View/Detail/DataFlow/PreviewsStore.swift b/EhPanda/View/Detail/DataFlow/PreviewsStore.swift index 41f41ade..98c145c2 100644 --- a/EhPanda/View/Detail/DataFlow/PreviewsStore.swift +++ b/EhPanda/View/Detail/DataFlow/PreviewsStore.swift @@ -5,6 +5,7 @@ // Created by 荒木辰造 on R 4/01/16. // +import Foundation import ComposableArchitecture struct PreviewsState: Equatable { diff --git a/EhPanda/View/Detail/DetailView.swift b/EhPanda/View/Detail/DetailView.swift index 69b09477..240f3ff6 100644 --- a/EhPanda/View/Detail/DetailView.swift +++ b/EhPanda/View/Detail/DetailView.swift @@ -117,7 +117,7 @@ struct DetailView: View { && viewStore.loadingState == .loading ? 1 : 0 ) let error = (/LoadingState.failed).extract(from: viewStore.loadingState) - let retryAction = { viewStore.send(.fetchGalleryDetail) } + let retryAction: () -> Void = { viewStore.send(.fetchGalleryDetail) } ErrorView(error: error ?? .unknown, action: error?.isRetryable != false ? retryAction : nil) .opacity(viewStore.galleryDetail == nil && error != nil ? 1 : 0) } diff --git a/EhPanda/View/Home/DataFlow/HistoryStore.swift b/EhPanda/View/Home/DataFlow/HistoryStore.swift index e5bd43bf..5128a5e7 100644 --- a/EhPanda/View/Home/DataFlow/HistoryStore.swift +++ b/EhPanda/View/Home/DataFlow/HistoryStore.swift @@ -5,6 +5,7 @@ // Created by 荒木辰造 on R 4/01/09. // +import Foundation import ComposableArchitecture struct HistoryState: Equatable { diff --git a/EhPanda/View/Home/HomeView.swift b/EhPanda/View/Home/HomeView.swift index 1336e143..42e5f41d 100644 --- a/EhPanda/View/Home/HomeView.swift +++ b/EhPanda/View/Home/HomeView.swift @@ -37,7 +37,7 @@ struct HomeView: View { ZStack { ScrollView(showsIndicators: false) { VStack { - if viewStore.popularLoadingState == .loading || !viewStore.popularGalleries.isEmpty { + if !viewStore.popularGalleries.isEmpty { CardSlideSection( galleries: viewStore.popularGalleries, pageIndex: viewStore.binding(\.$cardPageIndex), @@ -51,7 +51,7 @@ struct HomeView: View { .equatable().allowsHitTesting(viewStore.allowsCardHitTesting) } Group { - if viewStore.frontpageLoadingState == .loading || viewStore.frontpageGalleries.count > 1 { + if viewStore.frontpageGalleries.count > 1 { CoverWallSection( galleries: viewStore.frontpageGalleries, isLoading: viewStore.frontpageLoadingState == .loading, diff --git a/EhPanda/View/Migration/MigrationStore.swift b/EhPanda/View/Migration/MigrationStore.swift index b3a1f944..79ba9973 100644 --- a/EhPanda/View/Migration/MigrationStore.swift +++ b/EhPanda/View/Migration/MigrationStore.swift @@ -5,6 +5,7 @@ // Created by 荒木辰造 on R 4/02/03. // +import Foundation import ComposableArchitecture struct MigrationState: Equatable { diff --git a/EhPanda/View/Reading/ReadingView.swift b/EhPanda/View/Reading/ReadingView.swift index c4d2f99e..116b40bc 100644 --- a/EhPanda/View/Reading/ReadingView.swift +++ b/EhPanda/View/Reading/ReadingView.swift @@ -57,7 +57,7 @@ struct ReadingView: View { page: page, data: viewStore.state.containerDataSource(setting: setting), id: \.self, content: imageStack ) - .horizontal(setting.readingDirection == .rightToLeft ? .rightToLeft : .leftToRight) + .horizontal(setting.readingDirection == .rightToLeft ? .endToStart : .startToEnd) .swipeInteractionArea(.allAvailable).allowsDragging(gestureHandler.scale == 1) } } diff --git a/EhPanda/View/Setting/DataFlow/AccountSettingStore.swift b/EhPanda/View/Setting/DataFlow/AccountSettingStore.swift index 90af4ac2..183f07d5 100644 --- a/EhPanda/View/Setting/DataFlow/AccountSettingStore.swift +++ b/EhPanda/View/Setting/DataFlow/AccountSettingStore.swift @@ -5,6 +5,7 @@ // Created by 荒木辰造 on R 3/12/31. // +import Foundation import TTProgressHUD import ComposableArchitecture diff --git a/EhPanda/View/Setting/DataFlow/EhSettingStore.swift b/EhPanda/View/Setting/DataFlow/EhSettingStore.swift index 0847a0cb..fb221026 100644 --- a/EhPanda/View/Setting/DataFlow/EhSettingStore.swift +++ b/EhPanda/View/Setting/DataFlow/EhSettingStore.swift @@ -5,6 +5,7 @@ // Created by 荒木辰造 on R 4/01/01. // +import Foundation import ComposableArchitecture struct EhSettingState: Equatable { diff --git a/EhPanda/View/Setting/DataFlow/SettingStore.swift b/EhPanda/View/Setting/DataFlow/SettingStore.swift index 49360e34..431d36b8 100644 --- a/EhPanda/View/Setting/DataFlow/SettingStore.swift +++ b/EhPanda/View/Setting/DataFlow/SettingStore.swift @@ -5,6 +5,7 @@ // Created by 荒木辰造 on R 3/12/31. // +import Foundation import ComposableArchitecture struct SettingState: Equatable { From 873bd9ca8a6b3afa3f17299a01b1020ebbdaa98d Mon Sep 17 00:00:00 2001 From: Chihchy Date: Mon, 8 May 2023 23:41:52 +0800 Subject: [PATCH 03/29] Update Copyright description --- EhPanda/App/en.lproj/Constant.strings | 2 +- README.md | 2 +- READMEs/README.chs.md | 2 +- READMEs/README.cht.md | 2 +- READMEs/README.de.md | 2 +- READMEs/README.jpn.md | 2 +- READMEs/README.ko.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/EhPanda/App/en.lproj/Constant.strings b/EhPanda/App/en.lproj/Constant.strings index f77b7e70..e959b5c2 100644 --- a/EhPanda/App/en.lproj/Constant.strings +++ b/EhPanda/App/en.lproj/Constant.strings @@ -13,7 +13,7 @@ "website.response.gallery_unavailable" = "This gallery has been removed or is unavailable."; // MARK: EhPanda -"EhPanda.copyright" = "Copyright © 2022 EhPanda Team"; +"EhPanda.copyright" = "Copyright © 2023 EhPanda Team"; // Contacts "EhPanda.contacts.link.website" = "https://ehpanda.app"; diff --git a/README.md b/README.md index 60c50b41..3906d699 100644 --- a/README.md +++ b/README.md @@ -44,4 +44,4 @@ The content in this application is derived from E-Hentai, which is user-generate https://ehpanda.app ## App Icon -Copyright © 2022 荒木辰造. All rights reserved. +Copyright © 2023 荒木辰造. All rights reserved. diff --git a/READMEs/README.chs.md b/READMEs/README.chs.md index 806bc0df..f643a643 100644 --- a/READMEs/README.chs.md +++ b/READMEs/README.chs.md @@ -44,4 +44,4 @@ https://ehpanda.app: [main.js](https://github.com/EhPanda-Team/ehpanda-website/b https://ehpanda.app ## 应用程序图标 -Copyright © 2022 荒木辰造. All rights reserved. +Copyright © 2023 荒木辰造. All rights reserved. diff --git a/READMEs/README.cht.md b/READMEs/README.cht.md index 57da354b..f60435b9 100644 --- a/READMEs/README.cht.md +++ b/READMEs/README.cht.md @@ -44,4 +44,4 @@ https://ehpanda.app: [main.js](https://github.com/EhPanda-Team/ehpanda-website/b https://ehpanda.app ## 應用程式圖示 -Copyright © 2022 荒木辰造. All rights reserved. +Copyright © 2023 荒木辰造. All rights reserved. diff --git a/READMEs/README.de.md b/READMEs/README.de.md index 3cf04e80..6e6056ad 100644 --- a/READMEs/README.de.md +++ b/READMEs/README.de.md @@ -44,4 +44,4 @@ Der Inhalt der von dieser App verwaltet wird, wird von E-Hentai geladen. Hierbei https://ehpanda.app ## App Icon -Copyright © 2022 荒木辰造. All rights reserved. +Copyright © 2023 荒木辰造. All rights reserved. diff --git a/READMEs/README.jpn.md b/READMEs/README.jpn.md index 96cc4d07..f645538d 100644 --- a/READMEs/README.jpn.md +++ b/READMEs/README.jpn.md @@ -44,4 +44,4 @@ https://ehpanda.app: [main.js](https://github.com/EhPanda-Team/ehpanda-website/b https://ehpanda.app ## アプリアイコン -Copyright © 2022 荒木辰造. All rights reserved. +Copyright © 2023 荒木辰造. All rights reserved. diff --git a/READMEs/README.ko.md b/READMEs/README.ko.md index ec368209..f7414a9d 100644 --- a/READMEs/README.ko.md +++ b/READMEs/README.ko.md @@ -44,4 +44,4 @@ iOS / iPadOS 버전이 16.0 이상인지 확인해주세요. https://ehpanda.app ## 앱 아이콘 -Copyright © 2022 荒木辰造. All rights reserved. +Copyright © 2023 荒木辰造. All rights reserved. From ca4418fc477b658b75925e650deb9c89c1434070 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Mon, 8 May 2023 23:47:43 +0800 Subject: [PATCH 04/29] Rename BindableState & Effect --- .../App/Tools/Clients/AppDelegateClient.swift | 10 +-- .../Tools/Clients/AuthorizationClient.swift | 2 +- .../App/Tools/Clients/ClipboardClient.swift | 4 +- EhPanda/App/Tools/Clients/CookiesClient.swift | 18 ++--- EhPanda/App/Tools/Clients/DFClient.swift | 2 +- .../App/Tools/Clients/DatabaseClient.swift | 72 +++++++++---------- EhPanda/App/Tools/Clients/FileClient.swift | 6 +- EhPanda/App/Tools/Clients/HapticClient.swift | 4 +- EhPanda/App/Tools/Clients/ImageClient.swift | 10 +-- EhPanda/App/Tools/Clients/LibraryClient.swift | 10 +-- EhPanda/App/Tools/Clients/LoggerClient.swift | 4 +- .../Tools/Clients/UIApplicationClient.swift | 12 ++-- .../Tools/Clients/UserDefaultsClient.swift | 2 +- .../Tools/Extensions/Reducer_Extension.swift | 2 +- EhPanda/DataFlow/AppLockStore.swift | 2 +- EhPanda/DataFlow/AppRouteStore.swift | 6 +- EhPanda/DataFlow/AppStore.swift | 12 ++-- EhPanda/Network/Request.swift | 2 +- .../View/Detail/DataFlow/ArchivesStore.swift | 4 +- .../View/Detail/DataFlow/CommentsStore.swift | 8 +-- .../Detail/DataFlow/DetailSearchStore.swift | 6 +- .../View/Detail/DataFlow/DetailStore.swift | 8 +-- .../Detail/DataFlow/GalleryInfosStore.swift | 2 +- .../View/Detail/DataFlow/PreviewsStore.swift | 2 +- .../View/Detail/DataFlow/TorrentsStore.swift | 2 +- EhPanda/View/Favorites/FavoritesStore.swift | 6 +- .../View/Home/DataFlow/FrontpageStore.swift | 6 +- EhPanda/View/Home/DataFlow/HistoryStore.swift | 6 +- EhPanda/View/Home/DataFlow/HomeStore.swift | 8 +-- EhPanda/View/Home/DataFlow/PopularStore.swift | 4 +- .../View/Home/DataFlow/ToplistsStore.swift | 12 ++-- EhPanda/View/Home/DataFlow/WatchedStore.swift | 6 +- EhPanda/View/Migration/MigrationStore.swift | 2 +- EhPanda/View/Reading/ReadingStore.swift | 16 ++--- EhPanda/View/Search/SearchRootStore.swift | 4 +- EhPanda/View/Search/SearchStore.swift | 6 +- .../Search/Support/QuickSearchStore.swift | 8 +-- .../DataFlow/AccountSettingStore.swift | 6 +- .../DataFlow/AppearanceSettingStore.swift | 2 +- .../Setting/DataFlow/EhSettingStore.swift | 8 +-- .../DataFlow/GeneralSettingStore.swift | 2 +- .../View/Setting/DataFlow/LoginStore.swift | 10 +-- EhPanda/View/Setting/DataFlow/LogsStore.swift | 2 +- .../View/Setting/DataFlow/SettingStore.swift | 16 ++--- EhPanda/View/Support/FiltersStore.swift | 12 ++-- 45 files changed, 177 insertions(+), 177 deletions(-) diff --git a/EhPanda/App/Tools/Clients/AppDelegateClient.swift b/EhPanda/App/Tools/Clients/AppDelegateClient.swift index fdc3adc8..39833e09 100644 --- a/EhPanda/App/Tools/Clients/AppDelegateClient.swift +++ b/EhPanda/App/Tools/Clients/AppDelegateClient.swift @@ -9,8 +9,8 @@ import SwiftUI import ComposableArchitecture struct AppDelegateClient { - let setOrientation: (UIInterfaceOrientation) -> Effect - let setOrientationMask: (UIInterfaceOrientationMask) -> Effect + let setOrientation: (UIInterfaceOrientation) -> EffectTask + let setOrientationMask: (UIInterfaceOrientationMask) -> EffectTask } extension AppDelegateClient { @@ -27,13 +27,13 @@ extension AppDelegateClient { } ) - func setPortraitOrientation() -> Effect { + func setPortraitOrientation() -> EffectTask { setOrientation(.portrait) } - func setAllOrientationMask() -> Effect { + func setAllOrientationMask() -> EffectTask { setOrientationMask([.all]) } - func setPortraitOrientationMask() -> Effect { + func setPortraitOrientationMask() -> EffectTask { setOrientationMask([.portrait, .portraitUpsideDown]) } } diff --git a/EhPanda/App/Tools/Clients/AuthorizationClient.swift b/EhPanda/App/Tools/Clients/AuthorizationClient.swift index 3d8cfa1f..bc315977 100644 --- a/EhPanda/App/Tools/Clients/AuthorizationClient.swift +++ b/EhPanda/App/Tools/Clients/AuthorizationClient.swift @@ -11,7 +11,7 @@ import ComposableArchitecture struct AuthorizationClient { let passcodeNotSet: () -> Bool - let localAuthroize: (String) -> Effect + let localAuthroize: (String) -> EffectTask } extension AuthorizationClient { diff --git a/EhPanda/App/Tools/Clients/ClipboardClient.swift b/EhPanda/App/Tools/Clients/ClipboardClient.swift index f71768dd..2e952375 100644 --- a/EhPanda/App/Tools/Clients/ClipboardClient.swift +++ b/EhPanda/App/Tools/Clients/ClipboardClient.swift @@ -12,8 +12,8 @@ import UniformTypeIdentifiers struct ClipboardClient { let url: () -> URL? let changeCount: () -> Int - let saveText: (String) -> Effect - let saveImage: (UIImage, Bool) -> Effect + let saveText: (String) -> EffectTask + let saveImage: (UIImage, Bool) -> EffectTask } extension ClipboardClient { diff --git a/EhPanda/App/Tools/Clients/CookiesClient.swift b/EhPanda/App/Tools/Clients/CookiesClient.swift index 4642395c..34b21dd0 100644 --- a/EhPanda/App/Tools/Clients/CookiesClient.swift +++ b/EhPanda/App/Tools/Clients/CookiesClient.swift @@ -9,7 +9,7 @@ import Foundation import ComposableArchitecture struct CookiesClient { - let clearAll: () -> Effect + let clearAll: () -> EffectTask let getCookie: (URL, String) -> CookieValue private let removeCookie: (URL, String) -> Void private let checkExistence: (URL, String) -> Bool @@ -108,7 +108,7 @@ extension CookiesClient { guard let cookie = newCookie else { return } HTTPCookieStorage.shared.setCookie(cookie) } - func setOrEditCookie(for url: URL, key: String, value: String) -> Effect { + func setOrEditCookie(for url: URL, key: String, value: String) -> EffectTask { .fireAndForget { if checkExistence(url, key) { editCookie(for: url, key: key, value: value) @@ -138,18 +138,18 @@ extension CookiesClient { && !getCookie(url, Defaults.Cookie.ipbPassHash).rawValue.isEmpty && getCookie(url, Defaults.Cookie.igneous).rawValue.isEmpty } - func removeYay() -> Effect { + func removeYay() -> EffectTask { .fireAndForget { removeCookie(Defaults.URL.exhentai, Defaults.Cookie.yay) } } - func ignoreOffensive() -> Effect { + func ignoreOffensive() -> EffectTask { .merge( setOrEditCookie(for: Defaults.URL.ehentai, key: Defaults.Cookie.ignoreOffensive, value: "1"), setOrEditCookie(for: Defaults.URL.exhentai, key: Defaults.Cookie.ignoreOffensive, value: "1") ) } - func fulfillAnotherHostField() -> Effect { + func fulfillAnotherHostField() -> EffectTask { let ehURL = Defaults.URL.ehentai let exURL = Defaults.URL.exhentai let memberIdKey = Defaults.Cookie.ipbMemberId @@ -201,13 +201,13 @@ extension CookiesClient { // MARK: SetCookies extension CookiesClient { - func setCookies(state: CookiesState) -> Effect { - let effects: [Effect] = state.allCases.map { subState in + func setCookies(state: CookiesState) -> EffectTask { + let effects: [EffectTask] = state.allCases.map { subState in setOrEditCookie(for: state.host.url, key: subState.key, value: subState.editingText) } return effects.isEmpty ? .none : .merge(effects) } - func setCredentials(response: HTTPURLResponse) -> Effect { + func setCredentials(response: HTTPURLResponse) -> EffectTask { .fireAndForget { guard let setString = response.allHeaderFields["Set-Cookie"] as? String else { return } setString.components(separatedBy: ", ") @@ -226,7 +226,7 @@ extension CookiesClient { } } } - func setSkipServer(response: HTTPURLResponse) -> Effect { + func setSkipServer(response: HTTPURLResponse) -> EffectTask { .fireAndForget { guard let setString = response.allHeaderFields["Set-Cookie"] as? String else { return } setString.components(separatedBy: ", ") diff --git a/EhPanda/App/Tools/Clients/DFClient.swift b/EhPanda/App/Tools/Clients/DFClient.swift index c864c7d9..3ab7c121 100644 --- a/EhPanda/App/Tools/Clients/DFClient.swift +++ b/EhPanda/App/Tools/Clients/DFClient.swift @@ -10,7 +10,7 @@ import Kingfisher import ComposableArchitecture struct DFClient { - let setActive: (Bool) -> Effect + let setActive: (Bool) -> EffectTask } extension DFClient { diff --git a/EhPanda/App/Tools/Clients/DatabaseClient.swift b/EhPanda/App/Tools/Clients/DatabaseClient.swift index 077fa973..3af2c561 100644 --- a/EhPanda/App/Tools/Clients/DatabaseClient.swift +++ b/EhPanda/App/Tools/Clients/DatabaseClient.swift @@ -11,8 +11,8 @@ import CoreData import ComposableArchitecture struct DatabaseClient { - let prepareDatabase: () -> Effect, Never> - let dropDatabase: () -> Effect, Never> + let prepareDatabase: () -> EffectTask> + let dropDatabase: () -> EffectTask> private let saveContext: () -> Void private let materializedObjects: (NSManagedObjectContext, NSPredicate) -> [NSManagedObject] } @@ -200,7 +200,7 @@ extension DatabaseClient { } return entity } - func fetchAppEnv() -> Effect { + func fetchAppEnv() -> EffectTask { Future { promise in DispatchQueue.main.async { promise(.success(fetchOrCreate(entityType: AppEnvMO.self).toEntity())) @@ -213,7 +213,7 @@ extension DatabaseClient { func fetchAppEnvSynchronously() -> AppEnv { fetchOrCreate(entityType: AppEnvMO.self).toEntity() } - func fetchGalleryState(gid: String) -> Effect { + func fetchGalleryState(gid: String) -> EffectTask { guard gid.isValidGID else { return .none } return Future { promise in DispatchQueue.main.async { @@ -226,7 +226,7 @@ extension DatabaseClient { .receive(on: DispatchQueue.main) .eraseToEffect() } - func fetchHistoryGalleries(fetchLimit: Int = 0) -> Effect<[Gallery], Never> { + func fetchHistoryGalleries(fetchLimit: Int = 0) -> EffectTask<[Gallery]> { Future { promise in DispatchQueue.main.async { let predicate = NSPredicate(format: "lastOpenDate != nil") @@ -258,13 +258,13 @@ extension DatabaseClient { return fetchAppEnvSynchronously().watchedFilter } } - func fetchHistoryKeywords() -> Effect<[String], Never> { + func fetchHistoryKeywords() -> EffectTask<[String]> { fetchAppEnv().map(\.historyKeywords) } - func fetchQuickSearchWords() -> Effect<[QuickSearchWord], Never> { + func fetchQuickSearchWords() -> EffectTask<[QuickSearchWord]> { fetchAppEnv().map(\.quickSearchWords) } - func fetchGalleryPreviewURLs(gid: String) -> Effect<[Int: URL], Never> { + func fetchGalleryPreviewURLs(gid: String) -> EffectTask<[Int: URL]> { guard gid.isValidGID else { return .none } return fetchGalleryState(gid: gid).map(\.previewURLs) } @@ -272,7 +272,7 @@ extension DatabaseClient { // MARK: UpdateGallery extension DatabaseClient { - func updateGallery(gid: String, key: String, value: Any?) -> Effect { + func updateGallery(gid: String, key: String, value: Any?) -> EffectTask { guard gid.isValidGID else { return .none } return .fireAndForget { DispatchQueue.main.async { @@ -283,11 +283,11 @@ extension DatabaseClient { } } } - func updateLastOpenDate(gid: String, date: Date = .now) -> Effect { + func updateLastOpenDate(gid: String, date: Date = .now) -> EffectTask { guard gid.isValidGID else { return .none } return updateGallery(gid: gid, key: "lastOpenDate", value: date) } - func clearHistoryGalleries() -> Effect { + func clearHistoryGalleries() -> EffectTask { .fireAndForget { DispatchQueue.main.async { let predicate = NSPredicate(format: "lastOpenDate != nil") @@ -299,7 +299,7 @@ extension DatabaseClient { } } } - func cacheGalleries(_ galleries: [Gallery]) -> Effect { + func cacheGalleries(_ galleries: [Gallery]) -> EffectTask { .fireAndForget { DispatchQueue.main.async { for gallery in galleries.filter({ $0.id.isValidGID }) { @@ -332,7 +332,7 @@ extension DatabaseClient { // MARK: UpdateGalleryDetail extension DatabaseClient { - func cacheGalleryDetail(_ detail: GalleryDetail) -> Effect { + func cacheGalleryDetail(_ detail: GalleryDetail) -> EffectTask { guard detail.gid.isValidGID else { return .none } return .fireAndForget { DispatchQueue.main.async { @@ -370,7 +370,7 @@ extension DatabaseClient { // MARK: UpdateGalleryState extension DatabaseClient { - func updateGalleryState(gid: String, commitChanges: @escaping (GalleryStateMO) -> Void) -> Effect { + func updateGalleryState(gid: String, commitChanges: @escaping (GalleryStateMO) -> Void) -> EffectTask { guard gid.isValidGID else { return .none } return .fireAndForget { DispatchQueue.main.async { @@ -381,30 +381,30 @@ extension DatabaseClient { } } } - func updateGalleryState(gid: String, key: String, value: Any?) -> Effect { + func updateGalleryState(gid: String, key: String, value: Any?) -> EffectTask { guard gid.isValidGID else { return .none } return updateGalleryState(gid: gid) { stateMO in stateMO.setValue(value, forKeyPath: key) } } - func updateGalleryTags(gid: String, tags: [GalleryTag]) -> Effect { + func updateGalleryTags(gid: String, tags: [GalleryTag]) -> EffectTask { guard gid.isValidGID else { return .none } return updateGalleryState(gid: gid, key: "tags", value: tags.toData()) } - func updatePreviewConfig(gid: String, config: PreviewConfig) -> Effect { + func updatePreviewConfig(gid: String, config: PreviewConfig) -> EffectTask { guard gid.isValidGID else { return .none } return updateGalleryState(gid: gid, key: "previewConfig", value: config.toData()) } - func updateReadingProgress(gid: String, progress: Int) -> Effect { + func updateReadingProgress(gid: String, progress: Int) -> EffectTask { guard gid.isValidGID else { return .none } return updateGalleryState(gid: gid, key: "readingProgress", value: Int64(progress)) } - func updateComments(gid: String, comments: [GalleryComment]) -> Effect { + func updateComments(gid: String, comments: [GalleryComment]) -> EffectTask { guard gid.isValidGID else { return .none } return updateGalleryState(gid: gid, key: "comments", value: comments.toData()) } - func removeImageURLs(gid: String) -> Effect { + func removeImageURLs(gid: String) -> EffectTask { guard gid.isValidGID else { return .none } return updateGalleryState(gid: gid) { galleryStateMO in galleryStateMO.imageURLs = nil @@ -413,7 +413,7 @@ extension DatabaseClient { galleryStateMO.originalImageURLs = nil } } - func removeImageURLs() -> Effect { + func removeImageURLs() -> EffectTask { .fireAndForget { DispatchQueue.main.async { batchUpdate(entityType: GalleryStateMO.self) { galleryStateMOs in @@ -427,14 +427,14 @@ extension DatabaseClient { } } } - func removeExpiredImageURLs() -> Effect { + func removeExpiredImageURLs() -> EffectTask { fetchHistoryGalleries() .map { $0.filter { Date().timeIntervalSince($0.lastOpenDate ?? .distantPast) > .oneWeek } } .map { $0.map { removeImageURLs(gid: $0.id) } } - .map(Effect.merge) + .map(EffectTask.merge) .fireAndForget() } - func updateThumbnailURLs(gid: String, thumbnailURLs: [Int: URL]) -> Effect { + func updateThumbnailURLs(gid: String, thumbnailURLs: [Int: URL]) -> EffectTask { guard gid.isValidGID else { return .none } return updateGalleryState(gid: gid) { galleryStateMO in update(gid: gid, storedData: &galleryStateMO.thumbnailURLs, new: thumbnailURLs) @@ -442,14 +442,14 @@ extension DatabaseClient { } func updateImageURLs( gid: String, imageURLs: [Int: URL], originalImageURLs: [Int: URL] - ) -> Effect { + ) -> EffectTask { guard gid.isValidGID else { return .none } return updateGalleryState(gid: gid) { galleryStateMO in update(gid: gid, storedData: &galleryStateMO.imageURLs, new: imageURLs) update(gid: gid, storedData: &galleryStateMO.originalImageURLs, new: originalImageURLs) } } - func updatePreviewURLs(gid: String, previewURLs: [Int: URL]) -> Effect { + func updatePreviewURLs(gid: String, previewURLs: [Int: URL]) -> EffectTask { guard gid.isValidGID else { return .none } return updateGalleryState(gid: gid) { galleryStateMO in update(gid: gid, storedData: &galleryStateMO.previewURLs, new: previewURLs) @@ -473,7 +473,7 @@ extension DatabaseClient { // MARK: UpdateAppEnv extension DatabaseClient { - func updateAppEnv(key: String, value: Any?) -> Effect { + func updateAppEnv(key: String, value: Any?) -> EffectTask { .fireAndForget { DispatchQueue.main.async { update( @@ -483,10 +483,10 @@ extension DatabaseClient { } } } - func updateSetting(_ setting: Setting) -> Effect { + func updateSetting(_ setting: Setting) -> EffectTask { updateAppEnv(key: "setting", value: setting.toData()) } - func updateFilter(_ filter: Filter, range: FilterRange) -> Effect { + func updateFilter(_ filter: Filter, range: FilterRange) -> EffectTask { let key: String switch range { case .search: @@ -498,21 +498,21 @@ extension DatabaseClient { } return updateAppEnv(key: key, value: filter.toData()) } - func updateTagTranslator(_ tagTranslator: TagTranslator) -> Effect { + func updateTagTranslator(_ tagTranslator: TagTranslator) -> EffectTask { updateAppEnv(key: "tagTranslator", value: tagTranslator.toData()) } - func updateUser(_ user: User) -> Effect { + func updateUser(_ user: User) -> EffectTask { updateAppEnv(key: "user", value: user.toData()) } - func updateHistoryKeywords(_ keywords: [String]) -> Effect { + func updateHistoryKeywords(_ keywords: [String]) -> EffectTask { updateAppEnv(key: "historyKeywords", value: keywords.toData()) } - func updateQuickSearchWords(_ words: [QuickSearchWord]) -> Effect { + func updateQuickSearchWords(_ words: [QuickSearchWord]) -> EffectTask { updateAppEnv(key: "quickSearchWords", value: words.toData()) } // Update User - func updateUserProperty(_ commitChanges: @escaping (inout User) -> Void) -> Effect { + func updateUserProperty(_ commitChanges: @escaping (inout User) -> Void) -> EffectTask { fetchAppEnv().map(\.user) .map { (user: User) -> User in var user = user @@ -522,12 +522,12 @@ extension DatabaseClient { .flatMap(updateUser) .eraseToEffect() } - func updateGreeting(_ greeting: Greeting) -> Effect { + func updateGreeting(_ greeting: Greeting) -> EffectTask { updateUserProperty { user in user.greeting = greeting } } - func updateGalleryFunds(galleryPoints: String, credits: String) -> Effect { + func updateGalleryFunds(galleryPoints: String, credits: String) -> EffectTask { updateUserProperty { user in user.credits = credits user.galleryPoints = galleryPoints diff --git a/EhPanda/App/Tools/Clients/FileClient.swift b/EhPanda/App/Tools/Clients/FileClient.swift index c19da82d..3c321b07 100644 --- a/EhPanda/App/Tools/Clients/FileClient.swift +++ b/EhPanda/App/Tools/Clients/FileClient.swift @@ -11,9 +11,9 @@ import ComposableArchitecture struct FileClient { let createFile: (String, Data?) -> Bool - let fetchLogs: () -> Effect, Never> - let deleteLog: (String) -> Effect, Never> - let importTagTranslator: (URL) -> Effect, Never> + let fetchLogs: () -> EffectTask> + let deleteLog: (String) -> EffectTask> + let importTagTranslator: (URL) -> EffectTask> } extension FileClient { diff --git a/EhPanda/App/Tools/Clients/HapticClient.swift b/EhPanda/App/Tools/Clients/HapticClient.swift index f779eba8..3f5d00fc 100644 --- a/EhPanda/App/Tools/Clients/HapticClient.swift +++ b/EhPanda/App/Tools/Clients/HapticClient.swift @@ -9,8 +9,8 @@ import SwiftUI import ComposableArchitecture struct HapticClient { - let generateFeedback: (UIImpactFeedbackGenerator.FeedbackStyle) -> Effect - let generateNotificationFeedback: (UINotificationFeedbackGenerator.FeedbackType) -> Effect + let generateFeedback: (UIImpactFeedbackGenerator.FeedbackStyle) -> EffectTask + let generateNotificationFeedback: (UINotificationFeedbackGenerator.FeedbackType) -> EffectTask } extension HapticClient { diff --git a/EhPanda/App/Tools/Clients/ImageClient.swift b/EhPanda/App/Tools/Clients/ImageClient.swift index 84b612e5..c11ca8a2 100644 --- a/EhPanda/App/Tools/Clients/ImageClient.swift +++ b/EhPanda/App/Tools/Clients/ImageClient.swift @@ -12,10 +12,10 @@ import Kingfisher import ComposableArchitecture struct ImageClient { - let prefetchImages: ([URL]) -> Effect - let saveImageToPhotoLibrary: (UIImage, Bool) -> Effect - let downloadImage: (URL) -> Effect, Never> - let retrieveImage: (String) -> Effect, Never> + let prefetchImages: ([URL]) -> EffectTask + let saveImageToPhotoLibrary: (UIImage, Bool) -> EffectTask + let downloadImage: (URL) -> EffectTask> + let retrieveImage: (String) -> EffectTask> } extension ImageClient { @@ -78,7 +78,7 @@ extension ImageClient { } ) - func fetchImage(url: URL) -> Effect, Never> { + func fetchImage(url: URL) -> EffectTask> { if KingfisherManager.shared.cache.isCached(forKey: url.absoluteString) { return retrieveImage(url.absoluteString) } else { diff --git a/EhPanda/App/Tools/Clients/LibraryClient.swift b/EhPanda/App/Tools/Clients/LibraryClient.swift index a476725b..d6cca102 100644 --- a/EhPanda/App/Tools/Clients/LibraryClient.swift +++ b/EhPanda/App/Tools/Clients/LibraryClient.swift @@ -14,11 +14,11 @@ import UIImageColors import ComposableArchitecture struct LibraryClient { - let initializeLogger: () -> Effect - let initializeWebImage: () -> Effect - let clearWebImageDiskCache: () -> Effect - let analyzeImageColors: (UIImage) -> Effect - let calculateWebImageDiskCacheSize: () -> Effect + let initializeLogger: () -> EffectTask + let initializeWebImage: () -> EffectTask + let clearWebImageDiskCache: () -> EffectTask + let analyzeImageColors: (UIImage) -> EffectTask + let calculateWebImageDiskCacheSize: () -> EffectTask } extension LibraryClient { diff --git a/EhPanda/App/Tools/Clients/LoggerClient.swift b/EhPanda/App/Tools/Clients/LoggerClient.swift index 6f22c8ee..c230b9db 100644 --- a/EhPanda/App/Tools/Clients/LoggerClient.swift +++ b/EhPanda/App/Tools/Clients/LoggerClient.swift @@ -8,8 +8,8 @@ import ComposableArchitecture struct LoggerClient { - let info: (Any, Any?) -> Effect - let error: (Any, Any?) -> Effect + let info: (Any, Any?) -> EffectTask + let error: (Any, Any?) -> EffectTask } extension LoggerClient { diff --git a/EhPanda/App/Tools/Clients/UIApplicationClient.swift b/EhPanda/App/Tools/Clients/UIApplicationClient.swift index df705f47..97603507 100644 --- a/EhPanda/App/Tools/Clients/UIApplicationClient.swift +++ b/EhPanda/App/Tools/Clients/UIApplicationClient.swift @@ -10,11 +10,11 @@ import Combine import ComposableArchitecture struct UIApplicationClient { - let openURL: (URL) -> Effect - let hideKeyboard: () -> Effect + let openURL: (URL) -> EffectTask + let hideKeyboard: () -> EffectTask let alternateIconName: () -> String? - let setAlternateIconName: (String?) -> Effect, Never> - let setUserInterfaceStyle: (UIUserInterfaceStyle) -> Effect + let setAlternateIconName: (String?) -> EffectTask> + let setUserInterfaceStyle: (UIUserInterfaceStyle) -> EffectTask } extension UIApplicationClient { @@ -51,13 +51,13 @@ extension UIApplicationClient { } } ) - func openSettings() -> Effect { + func openSettings() -> EffectTask { if let url = URL(string: UIApplication.openSettingsURLString) { return openURL(url) } return .none } - func openFileApp() -> Effect { + func openFileApp() -> EffectTask { if let dirPath = FileUtil.logsDirectoryURL?.path, let dirURL = URL(string: "shareddocuments://" + dirPath) { diff --git a/EhPanda/App/Tools/Clients/UserDefaultsClient.swift b/EhPanda/App/Tools/Clients/UserDefaultsClient.swift index 7fb2af7d..8b0b3937 100644 --- a/EhPanda/App/Tools/Clients/UserDefaultsClient.swift +++ b/EhPanda/App/Tools/Clients/UserDefaultsClient.swift @@ -9,7 +9,7 @@ import Foundation import ComposableArchitecture struct UserDefaultsClient { - let setValue: (Any, AppUserDefaults) -> Effect + let setValue: (Any, AppUserDefaults) -> EffectTask } extension UserDefaultsClient { diff --git a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift index c8376e37..0058ff88 100644 --- a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift +++ b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift @@ -24,7 +24,7 @@ extension Reducer { unwrapping enum: @escaping (State) -> Enum?, case casePath: CasePath, perform additionalEffects: @escaping (inout State, Action, Environment) - -> Effect + -> EffectTask ) -> Self { .init { state, action, environment in let previousCase = Binding.constant(`enum`(state)).case(casePath).wrappedValue diff --git a/EhPanda/DataFlow/AppLockStore.swift b/EhPanda/DataFlow/AppLockStore.swift index 1743bea5..fedbe50a 100644 --- a/EhPanda/DataFlow/AppLockStore.swift +++ b/EhPanda/DataFlow/AppLockStore.swift @@ -9,7 +9,7 @@ import SwiftUI import ComposableArchitecture struct AppLockState: Equatable { - @BindableState var blurRadius: Double = 0 + @BindingState var blurRadius: Double = 0 var becameInactiveDate: Date? var isAppLocked = false diff --git a/EhPanda/DataFlow/AppRouteStore.swift b/EhPanda/DataFlow/AppRouteStore.swift index c5e1acb8..38ea7ef2 100644 --- a/EhPanda/DataFlow/AppRouteStore.swift +++ b/EhPanda/DataFlow/AppRouteStore.swift @@ -21,7 +21,7 @@ struct AppRouteState: Equatable { _detailState = .init(.init()) } - @BindableState var route: Route? + @BindingState var route: Route? var hudConfig: TTProgressHUDConfig = .loading @Heap var detailState: DetailState! @@ -89,7 +89,7 @@ let appRouteReducer = Reducer] = [ + var effects: [EffectTask] = [ environment.userDefaultsClient .setValue(currentChangeCount, .clipboardChangeCount).fireAndForget() ] @@ -119,7 +119,7 @@ let appRouteReducer = Reducer]() + var effects = [EffectTask]() state.detailState = .init() effects.append(.init(value: .detail(.fetchDatabaseInfos(gid)))) if let pageIndex = pageIndex { diff --git a/EhPanda/DataFlow/AppStore.swift b/EhPanda/DataFlow/AppStore.swift index 70661104..150fddb9 100644 --- a/EhPanda/DataFlow/AppStore.swift +++ b/EhPanda/DataFlow/AppStore.swift @@ -89,7 +89,7 @@ let appReducerCore = Reducer { state, actio return .none case .appRoute(.clearSubStates): - var effects = [Effect]() + var effects = [EffectTask]() if environment.deviceClient.isPad() { state.settingState.route = nil effects.append(.init(value: .setting(.clearSubStates))) @@ -100,7 +100,7 @@ let appReducerCore = Reducer { state, actio return .none case .appLock(.unlockApp): - var effects: [Effect] = [ + var effects: [EffectTask] = [ .init(value: .setting(.fetchGreeting)) ] if state.settingState.setting.detectsLinksFromClipboard { @@ -112,8 +112,8 @@ let appReducerCore = Reducer { state, actio return .none case .tabBar(.setTabBarItemType(let type)): - var effects = [Effect]() - let hapticEffect: Effect = environment.hapticClient + var effects = [EffectTask]() + let hapticEffect: EffectTask = environment.hapticClient .generateFeedback(.soft).fireAndForget() if type == state.tabBarState.tabBarItemType { switch type { @@ -156,7 +156,7 @@ let appReducerCore = Reducer { state, actio return .none case .home(.watched(.onNotLoginViewButtonTapped)), .favorites(.onNotLoginViewButtonTapped): - var effects: [Effect] = [ + var effects: [EffectTask] = [ environment.hapticClient.generateFeedback(.soft).fireAndForget(), .init(value: .tabBar(.setTabBarItemType(.setting))) ] @@ -183,7 +183,7 @@ let appReducerCore = Reducer { state, actio return .none case .setting(.loadUserSettingsDone): - var effects = [Effect]() + var effects = [EffectTask]() let threshold = state.settingState.setting.autoLockPolicy.rawValue let blurRadius = state.settingState.setting.backgroundBlurRadius if threshold >= 0 { diff --git a/EhPanda/Network/Request.swift b/EhPanda/Network/Request.swift index d6858032..32249759 100644 --- a/EhPanda/Network/Request.swift +++ b/EhPanda/Network/Request.swift @@ -16,7 +16,7 @@ protocol Request { var publisher: AnyPublisher { get } } extension Request { - var effect: Effect, Never> { + var effect: EffectTask> { publisher.receive(on: DispatchQueue.main).catchToEffect() } diff --git a/EhPanda/View/Detail/DataFlow/ArchivesStore.swift b/EhPanda/View/Detail/DataFlow/ArchivesStore.swift index 60ae7d32..0933e5d4 100644 --- a/EhPanda/View/Detail/DataFlow/ArchivesStore.swift +++ b/EhPanda/View/Detail/DataFlow/ArchivesStore.swift @@ -18,8 +18,8 @@ struct ArchivesState: Equatable { let id = String(describing: ArchivesState.self) } - @BindableState var route: Route? - @BindableState var selectedArchive: GalleryArchive.HathArchive? + @BindingState var route: Route? + @BindingState var selectedArchive: GalleryArchive.HathArchive? var loadingState: LoadingState = .idle var hathArchives = [GalleryArchive.HathArchive]() diff --git a/EhPanda/View/Detail/DataFlow/CommentsStore.swift b/EhPanda/View/Detail/DataFlow/CommentsStore.swift index 80fa5a37..0a296d01 100644 --- a/EhPanda/View/Detail/DataFlow/CommentsStore.swift +++ b/EhPanda/View/Detail/DataFlow/CommentsStore.swift @@ -23,9 +23,9 @@ struct CommentsState: Equatable { _detailState = .init(.init()) } - @BindableState var route: Route? - @BindableState var commentContent = "" - @BindableState var postCommentFocused = false + @BindingState var route: Route? + @BindingState var commentContent = "" + @BindingState var postCommentFocused = false var hudConfig: TTProgressHUDConfig = .loading var scrollCommentID: String? @@ -137,7 +137,7 @@ let commentsReducer = Reducer]() + var effects = [EffectTask]() if let pageIndex = pageIndex { effects.append(.init(value: .updateReadingProgress(gid, pageIndex))) effects.append( diff --git a/EhPanda/View/Detail/DataFlow/DetailSearchStore.swift b/EhPanda/View/Detail/DataFlow/DetailSearchStore.swift index 3c91db39..d8a214df 100644 --- a/EhPanda/View/Detail/DataFlow/DetailSearchStore.swift +++ b/EhPanda/View/Detail/DataFlow/DetailSearchStore.swift @@ -21,8 +21,8 @@ struct DetailSearchState: Equatable { _detailState = .init(.init()) } - @BindableState var route: Route? - @BindableState var keyword = "" + @BindingState var route: Route? + @BindingState var keyword = "" var lastKeyword = "" var galleries = [Gallery]() @@ -151,7 +151,7 @@ let detailSearchReducer = Reducer] = [ + var effects: [EffectTask] = [ environment.databaseClient.cacheGalleries(galleries).fireAndForget() ] if galleries.isEmpty, pageNumber.hasNextPage() { diff --git a/EhPanda/View/Detail/DataFlow/DetailStore.swift b/EhPanda/View/Detail/DataFlow/DetailStore.swift index f4b63b95..7bd6ffc0 100644 --- a/EhPanda/View/Detail/DataFlow/DetailStore.swift +++ b/EhPanda/View/Detail/DataFlow/DetailStore.swift @@ -32,9 +32,9 @@ struct DetailState: Equatable { _detailSearchState = .init(nil) } - @BindableState var route: Route? - @BindableState var commentContent = "" - @BindableState var postCommentFocused = false + @BindingState var route: Route? + @BindingState var commentContent = "" + @BindingState var postCommentFocused = false var showsNewDawnGreeting = false var showsUserRating = false @@ -279,7 +279,7 @@ let detailReducer = Reducer.recurs state.loadingState = .idle switch result { case .success(let (galleryDetail, galleryState, apiKey, greeting)): - var effects: [Effect] = [ + var effects: [EffectTask] = [ .init(value: .syncGalleryTags), .init(value: .syncGalleryDetail), .init(value: .syncGalleryPreviewURLs), diff --git a/EhPanda/View/Detail/DataFlow/GalleryInfosStore.swift b/EhPanda/View/Detail/DataFlow/GalleryInfosStore.swift index d6ae0812..bd0022ff 100644 --- a/EhPanda/View/Detail/DataFlow/GalleryInfosStore.swift +++ b/EhPanda/View/Detail/DataFlow/GalleryInfosStore.swift @@ -13,7 +13,7 @@ struct GalleryInfosState: Equatable { case hud } - @BindableState var route: Route? + @BindingState var route: Route? var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded } diff --git a/EhPanda/View/Detail/DataFlow/PreviewsStore.swift b/EhPanda/View/Detail/DataFlow/PreviewsStore.swift index 98c145c2..a5e7d129 100644 --- a/EhPanda/View/Detail/DataFlow/PreviewsStore.swift +++ b/EhPanda/View/Detail/DataFlow/PreviewsStore.swift @@ -16,7 +16,7 @@ struct PreviewsState: Equatable { let id = String(describing: PreviewsState.self) } - @BindableState var route: Route? + @BindingState var route: Route? var gallery: Gallery = .empty var loadingState: LoadingState = .idle diff --git a/EhPanda/View/Detail/DataFlow/TorrentsStore.swift b/EhPanda/View/Detail/DataFlow/TorrentsStore.swift index b80547f8..1bdfe7a0 100644 --- a/EhPanda/View/Detail/DataFlow/TorrentsStore.swift +++ b/EhPanda/View/Detail/DataFlow/TorrentsStore.swift @@ -18,7 +18,7 @@ struct TorrentsState: Equatable { let id = String(describing: TorrentsState.self) } - @BindableState var route: Route? + @BindingState var route: Route? var torrents = [GalleryTorrent]() var loadingState: LoadingState = .idle var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded diff --git a/EhPanda/View/Favorites/FavoritesStore.swift b/EhPanda/View/Favorites/FavoritesStore.swift index e7f5d89e..417db226 100644 --- a/EhPanda/View/Favorites/FavoritesStore.swift +++ b/EhPanda/View/Favorites/FavoritesStore.swift @@ -20,8 +20,8 @@ struct FavoritesState: Equatable { _detailState = .init(.init()) } - @BindableState var route: Route? - @BindableState var keyword = "" + @BindingState var route: Route? + @BindingState var keyword = "" var index = -1 var sortOrder: FavoritesSortOrder? @@ -172,7 +172,7 @@ let favoritesReducer = Reducer] = [ + var effects: [EffectTask] = [ environment.databaseClient.cacheGalleries(galleries).fireAndForget() ] if galleries.isEmpty, pageNumber.hasNextPage() { diff --git a/EhPanda/View/Home/DataFlow/FrontpageStore.swift b/EhPanda/View/Home/DataFlow/FrontpageStore.swift index de934c37..58629b8e 100644 --- a/EhPanda/View/Home/DataFlow/FrontpageStore.swift +++ b/EhPanda/View/Home/DataFlow/FrontpageStore.swift @@ -20,8 +20,8 @@ struct FrontpageState: Equatable { _detailState = .init(.init()) } - @BindableState var route: Route? - @BindableState var keyword = "" + @BindingState var route: Route? + @BindingState var keyword = "" var filteredGalleries: [Gallery] { guard !keyword.isEmpty else { return galleries } @@ -138,7 +138,7 @@ let frontpageReducer = Reducer] = [ + var effects: [EffectTask] = [ environment.databaseClient.cacheGalleries(galleries).fireAndForget() ] if galleries.isEmpty, pageNumber.hasNextPage() { diff --git a/EhPanda/View/Home/DataFlow/HistoryStore.swift b/EhPanda/View/Home/DataFlow/HistoryStore.swift index 5128a5e7..efa3457d 100644 --- a/EhPanda/View/Home/DataFlow/HistoryStore.swift +++ b/EhPanda/View/Home/DataFlow/HistoryStore.swift @@ -18,9 +18,9 @@ struct HistoryState: Equatable { _detailState = .init(.init()) } - @BindableState var route: Route? - @BindableState var keyword = "" - @BindableState var clearDialogPresented = false + @BindingState var route: Route? + @BindingState var keyword = "" + @BindingState var clearDialogPresented = false var filteredGalleries: [Gallery] { guard !keyword.isEmpty else { return galleries } diff --git a/EhPanda/View/Home/DataFlow/HomeStore.swift b/EhPanda/View/Home/DataFlow/HomeStore.swift index a51fc357..c42f41ad 100644 --- a/EhPanda/View/Home/DataFlow/HomeStore.swift +++ b/EhPanda/View/Home/DataFlow/HomeStore.swift @@ -21,9 +21,9 @@ struct HomeState: Equatable { _detailState = .init(.init()) } - @BindableState var route: Route? - @BindableState var cardPageIndex = 1 - @BindableState var currentCardID = "" + @BindingState var route: Route? + @BindingState var cardPageIndex = 1 + @BindingState var currentCardID = "" var allowsCardHitTesting = true var rawCardColors = [String: [Color]]() var cardColors: [Color] { @@ -152,7 +152,7 @@ let homeReducer = Reducer.combine( case .fetchAllToplistsGalleries: return .merge( ToplistsType.allCases.map({ HomeAction.fetchToplistsGalleries($0.categoryIndex) }) - .map(Effect.init) + .map(EffectTask.init) ) case .fetchPopularGalleries: diff --git a/EhPanda/View/Home/DataFlow/PopularStore.swift b/EhPanda/View/Home/DataFlow/PopularStore.swift index d770451c..b9a72108 100644 --- a/EhPanda/View/Home/DataFlow/PopularStore.swift +++ b/EhPanda/View/Home/DataFlow/PopularStore.swift @@ -20,8 +20,8 @@ struct PopularState: Equatable { _detailState = .init(.init()) } - @BindableState var route: Route? - @BindableState var keyword = "" + @BindingState var route: Route? + @BindingState var keyword = "" var filteredGalleries: [Gallery] { guard !keyword.isEmpty else { return galleries } diff --git a/EhPanda/View/Home/DataFlow/ToplistsStore.swift b/EhPanda/View/Home/DataFlow/ToplistsStore.swift index 281a4f39..961c93bb 100644 --- a/EhPanda/View/Home/DataFlow/ToplistsStore.swift +++ b/EhPanda/View/Home/DataFlow/ToplistsStore.swift @@ -19,11 +19,11 @@ struct ToplistsState: Equatable { _detailState = .init(.init()) } - @BindableState var route: Route? - @BindableState var keyword = "" - @BindableState var jumpPageIndex = "" - @BindableState var jumpPageAlertFocused = false - @BindableState var jumpPageAlertPresented = false + @BindingState var route: Route? + @BindingState var keyword = "" + @BindingState var jumpPageIndex = "" + @BindingState var jumpPageAlertFocused = false + @BindingState var jumpPageAlertPresented = false var type: ToplistsType = .yesterday @@ -188,7 +188,7 @@ let toplistsReducer = Reducer] = [ + var effects: [EffectTask] = [ environment.databaseClient.cacheGalleries(galleries).fireAndForget() ] if galleries.isEmpty, pageNumber.hasNextPage() { diff --git a/EhPanda/View/Home/DataFlow/WatchedStore.swift b/EhPanda/View/Home/DataFlow/WatchedStore.swift index ca63cd39..18e9c07a 100644 --- a/EhPanda/View/Home/DataFlow/WatchedStore.swift +++ b/EhPanda/View/Home/DataFlow/WatchedStore.swift @@ -21,8 +21,8 @@ struct WatchedState: Equatable { _detailState = .init(.init()) } - @BindableState var route: Route? - @BindableState var keyword = "" + @BindingState var route: Route? + @BindingState var keyword = "" var galleries = [Gallery]() var pageNumber = PageNumber() @@ -147,7 +147,7 @@ let watchedReducer = Reducer.co state.pageNumber = pageNumber state.insertGalleries(galleries) - var effects: [Effect] = [ + var effects: [EffectTask] = [ environment.databaseClient.cacheGalleries(galleries).fireAndForget() ] if galleries.isEmpty, pageNumber.hasNextPage() { diff --git a/EhPanda/View/Migration/MigrationStore.swift b/EhPanda/View/Migration/MigrationStore.swift index 79ba9973..dcbd3966 100644 --- a/EhPanda/View/Migration/MigrationStore.swift +++ b/EhPanda/View/Migration/MigrationStore.swift @@ -13,7 +13,7 @@ struct MigrationState: Equatable { case dropDialog } - @BindableState var route: Route? + @BindingState var route: Route? var databaseState: LoadingState = .loading } diff --git a/EhPanda/View/Reading/ReadingStore.swift b/EhPanda/View/Reading/ReadingStore.swift index e84a1c4e..6a67760c 100644 --- a/EhPanda/View/Reading/ReadingStore.swift +++ b/EhPanda/View/Reading/ReadingStore.swift @@ -36,7 +36,7 @@ struct ReadingState: Equatable { let id = String(describing: ReadingState.CancelID.self) } - @BindableState var route: Route? + @BindingState var route: Route? var gallery: Gallery = .empty var galleryDetail: GalleryDetail? @@ -60,8 +60,8 @@ struct ReadingState: Equatable { var mpvImageKeys = [Int: String]() var mpvSkipServerIdentifiers = [Int: String]() - @BindableState var showsPanel = false - @BindableState var showsSliderPreview = false + @BindingState var showsPanel = false + @BindingState var showsSliderPreview = false // Update func update(stored: inout [Int: T], new: [Int: T], replaceExisting: Bool = true) { @@ -192,7 +192,7 @@ let readingReducer = Reducer { return .none case .setOrientationPortrait(let isPortrait): - var effects = [Effect]() + var effects = [EffectTask]() if isPortrait { effects.append(environment.appDelegateClient.setPortraitOrientationMask().fireAndForget()) effects.append(environment.appDelegateClient.setPortraitOrientation().fireAndForget()) @@ -205,7 +205,7 @@ let readingReducer = Reducer { return environment.hapticClient.generateFeedback(.light).fireAndForget() case .onAppear(let gid, let enablesLandscape): - var effects: [Effect] = [ + var effects: [EffectTask] = [ .init(value: .fetchDatabaseInfos(gid)) ] if enablesLandscape { @@ -310,7 +310,7 @@ let readingReducer = Reducer { .fireAndForget() case .teardown: - var effects: [Effect] = [ + var effects: [EffectTask] = [ .cancel(id: ReadingState.CancelID()) ] if !environment.deviceClient.isPad() { @@ -394,7 +394,7 @@ let readingReducer = Reducer { } var prefetchImageURLs = [URL]() var fetchImageURLIndices = [Int]() - var effects = [Effect]() + var effects = [EffectTask]() let previousUpperBound = max(index - 2, 1) let previousLowerBound = max(previousUpperBound - prefetchLimit / 2, 1) if previousUpperBound - previousLowerBound > 0 { @@ -494,7 +494,7 @@ let readingReducer = Reducer { case .refetchNormalImageURLsDone(let index, let result): switch result { case .success(let (imageURLs, response)): - var effects = [Effect]() + var effects = [EffectTask]() if let response = response { effects.append(environment.cookiesClient.setSkipServer(response: response).fireAndForget()) } diff --git a/EhPanda/View/Search/SearchRootStore.swift b/EhPanda/View/Search/SearchRootStore.swift index aeb71a50..3d65e8d1 100644 --- a/EhPanda/View/Search/SearchRootStore.swift +++ b/EhPanda/View/Search/SearchRootStore.swift @@ -19,8 +19,8 @@ struct SearchRootState: Equatable { _detailState = .init(.init()) } - @BindableState var route: Route? - @BindableState var keyword = "" + @BindingState var route: Route? + @BindingState var keyword = "" var historyGalleries = [Gallery]() var historyKeywords = [String]() diff --git a/EhPanda/View/Search/SearchStore.swift b/EhPanda/View/Search/SearchStore.swift index e41c327e..24abeedf 100644 --- a/EhPanda/View/Search/SearchStore.swift +++ b/EhPanda/View/Search/SearchStore.swift @@ -21,8 +21,8 @@ struct SearchState: Equatable { _detailState = .init(.init()) } - @BindableState var route: Route? - @BindableState var keyword = "" + @BindingState var route: Route? + @BindingState var keyword = "" var lastKeyword = "" var galleries = [Gallery]() @@ -152,7 +152,7 @@ let searchReducer = Reducer.combin state.pageNumber = pageNumber state.insertGalleries(galleries) - var effects: [Effect] = [ + var effects: [EffectTask] = [ environment.databaseClient.cacheGalleries(galleries).fireAndForget() ] if galleries.isEmpty, pageNumber.hasNextPage() { diff --git a/EhPanda/View/Search/Support/QuickSearchStore.swift b/EhPanda/View/Search/Support/QuickSearchStore.swift index 33de4c30..aa32108b 100644 --- a/EhPanda/View/Search/Support/QuickSearchStore.swift +++ b/EhPanda/View/Search/Support/QuickSearchStore.swift @@ -22,10 +22,10 @@ struct QuickSearchState: Equatable { let id = String(describing: QuickSearchState.self) } - @BindableState var route: Route? - @BindableState var focusedField: FocusField? - @BindableState var editingWord: QuickSearchWord = .empty - @BindableState var listEditMode: EditMode = .inactive + @BindingState var route: Route? + @BindingState var focusedField: FocusField? + @BindingState var editingWord: QuickSearchWord = .empty + @BindingState var listEditMode: EditMode = .inactive var isListEditing: Bool { get { listEditMode == .active } set { listEditMode = newValue ? .active : .inactive } diff --git a/EhPanda/View/Setting/DataFlow/AccountSettingStore.swift b/EhPanda/View/Setting/DataFlow/AccountSettingStore.swift index 183f07d5..e48034ee 100644 --- a/EhPanda/View/Setting/DataFlow/AccountSettingStore.swift +++ b/EhPanda/View/Setting/DataFlow/AccountSettingStore.swift @@ -65,9 +65,9 @@ struct AccountSettingState: Equatable { case webView(URL) } - @BindableState var route: Route? - @BindableState var ehCookiesState: CookiesState = .empty(.ehentai) - @BindableState var exCookiesState: CookiesState = .empty(.exhentai) + @BindingState var route: Route? + @BindingState var ehCookiesState: CookiesState = .empty(.ehentai) + @BindingState var exCookiesState: CookiesState = .empty(.exhentai) var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded var loginState = LoginState() diff --git a/EhPanda/View/Setting/DataFlow/AppearanceSettingStore.swift b/EhPanda/View/Setting/DataFlow/AppearanceSettingStore.swift index 68f47a50..9c41c843 100644 --- a/EhPanda/View/Setting/DataFlow/AppearanceSettingStore.swift +++ b/EhPanda/View/Setting/DataFlow/AppearanceSettingStore.swift @@ -12,7 +12,7 @@ struct AppearanceSettingState: Equatable { case appIcon } - @BindableState var route: Route? + @BindingState var route: Route? } enum AppearanceSettingAction: BindableAction { diff --git a/EhPanda/View/Setting/DataFlow/EhSettingStore.swift b/EhPanda/View/Setting/DataFlow/EhSettingStore.swift index fb221026..0e1216c4 100644 --- a/EhPanda/View/Setting/DataFlow/EhSettingStore.swift +++ b/EhPanda/View/Setting/DataFlow/EhSettingStore.swift @@ -17,10 +17,10 @@ struct EhSettingState: Equatable { let id = String(describing: EhSettingState.self) } - @BindableState var route: Route? - @BindableState var editingProfileName = "" - @BindableState var ehSetting: EhSetting? - @BindableState var ehProfile: EhProfile? + @BindingState var route: Route? + @BindingState var editingProfileName = "" + @BindingState var ehSetting: EhSetting? + @BindingState var ehProfile: EhProfile? var loadingState: LoadingState = .idle var submittingState: LoadingState = .idle diff --git a/EhPanda/View/Setting/DataFlow/GeneralSettingStore.swift b/EhPanda/View/Setting/DataFlow/GeneralSettingStore.swift index 25855c5a..ef7206ea 100644 --- a/EhPanda/View/Setting/DataFlow/GeneralSettingStore.swift +++ b/EhPanda/View/Setting/DataFlow/GeneralSettingStore.swift @@ -16,7 +16,7 @@ struct GeneralSettingState: Equatable { case removeCustomTranslations } - @BindableState var route: Route? + @BindingState var route: Route? var loadingState: LoadingState = .idle var diskImageCacheSize = "0 KB" diff --git a/EhPanda/View/Setting/DataFlow/LoginStore.swift b/EhPanda/View/Setting/DataFlow/LoginStore.swift index b2bd062e..b5928eed 100644 --- a/EhPanda/View/Setting/DataFlow/LoginStore.swift +++ b/EhPanda/View/Setting/DataFlow/LoginStore.swift @@ -20,10 +20,10 @@ struct LoginState: Equatable { let id = String(describing: LoginState.self) } - @BindableState var route: Route? - @BindableState var focusedField: FocusedField? - @BindableState var username = "" - @BindableState var password = "" + @BindingState var route: Route? + @BindingState var focusedField: FocusedField? + @BindingState var username = "" + @BindingState var password = "" var loginState: LoadingState = .idle var loginButtonDisabled: Bool { @@ -73,7 +73,7 @@ let loginReducer = Reducer { state, a case .loginDone(let result): state.route = nil - var effects = [Effect]() + var effects = [EffectTask]() if environment.cookiesClient.didLogin { state.loginState = .idle effects.append(environment.hapticClient.generateNotificationFeedback(.success).fireAndForget()) diff --git a/EhPanda/View/Setting/DataFlow/LogsStore.swift b/EhPanda/View/Setting/DataFlow/LogsStore.swift index 6e25fa1c..aeb41833 100644 --- a/EhPanda/View/Setting/DataFlow/LogsStore.swift +++ b/EhPanda/View/Setting/DataFlow/LogsStore.swift @@ -15,7 +15,7 @@ struct LogsState: Equatable { let id = String(describing: LogsState.self) } - @BindableState var route: Route? + @BindingState var route: Route? var loadingState: LoadingState = .idle var logs = [Log]() } diff --git a/EhPanda/View/Setting/DataFlow/SettingStore.swift b/EhPanda/View/Setting/DataFlow/SettingStore.swift index 431d36b8..aaab606e 100644 --- a/EhPanda/View/Setting/DataFlow/SettingStore.swift +++ b/EhPanda/View/Setting/DataFlow/SettingStore.swift @@ -21,11 +21,11 @@ struct SettingState: Equatable { } // AppEnvStorage - @BindableState var setting = Setting() + @BindingState var setting = Setting() var tagTranslator = TagTranslator() var user = User() - @BindableState var route: Route? + @BindingState var route: Route? var tagTranslatorLoadingState: LoadingState = .idle var accountSettingState = AccountSettingState() @@ -120,7 +120,7 @@ let settingReducer = Reducer.co ) case .binding(\.$setting.enablesTagsExtension): - var effects: [Effect] = [ + var effects: [EffectTask] = [ .init(value: .syncSetting) ] if state.setting.enablesTagsExtension { @@ -158,7 +158,7 @@ let settingReducer = Reducer.co return .init(value: .syncSetting) case .binding(\.$setting.enablesLandscape): - var effects: [Effect] = [ + var effects: [EffectTask] = [ .init(value: .syncSetting) ] if !state.setting.enablesLandscape && !environment.deviceClient.isPad() { @@ -235,7 +235,7 @@ let settingReducer = Reducer.co state.setting = appEnv.setting state.tagTranslator = appEnv.tagTranslator state.user = appEnv.user - var effects: [Effect] = [ + var effects: [EffectTask] = [ .init(value: .syncAppIconType), .init(value: .loadUserSettingsDone), .init(value: .syncUserInterfaceStyle), @@ -273,7 +273,7 @@ let settingReducer = Reducer.co return IgneousRequest().effect.map(SettingAction.fetchIgneousDone) case .fetchIgneousDone(let result): - var effects = [Effect]() + var effects = [EffectTask]() if case .success(let response) = result { effects.append(environment.cookiesClient.setCredentials(response: response).fireAndForget()) } @@ -350,7 +350,7 @@ let settingReducer = Reducer.co else { return .none } state.tagTranslatorLoadingState = .loading - var databaseEffect: Effect? + var databaseEffect: EffectTask? if state.tagTranslator.language != language { state.tagTranslator = TagTranslator(language: language) databaseEffect = .init(value: .syncTagTranslator) @@ -380,7 +380,7 @@ let settingReducer = Reducer.co return VerifyEhProfileRequest().effect.map(SettingAction.fetchEhProfileIndexDone) case .fetchEhProfileIndexDone(let result): - var effects = [Effect]() + var effects = [EffectTask]() if case .success(let (profileValue, profileNotFound)) = result { if let profileValue = profileValue { diff --git a/EhPanda/View/Support/FiltersStore.swift b/EhPanda/View/Support/FiltersStore.swift index d47ee16f..bfd8dfcb 100644 --- a/EhPanda/View/Support/FiltersStore.swift +++ b/EhPanda/View/Support/FiltersStore.swift @@ -16,13 +16,13 @@ struct FiltersState: Equatable { case upper } - @BindableState var route: Route? - @BindableState var filterRange: FilterRange = .search - @BindableState var focusedBound: FocusedBound? + @BindingState var route: Route? + @BindingState var filterRange: FilterRange = .search + @BindingState var focusedBound: FocusedBound? - @BindableState var searchFilter = Filter() - @BindableState var globalFilter = Filter() - @BindableState var watchedFilter = Filter() + @BindingState var searchFilter = Filter() + @BindingState var globalFilter = Filter() + @BindingState var watchedFilter = Filter() } enum FiltersAction: BindableAction { From b61bff71397a1a12be97622a7e289ba5fade164e Mon Sep 17 00:00:00 2001 From: Chihchy Date: Mon, 8 May 2023 23:47:56 +0800 Subject: [PATCH 05/29] Resolve some compiler warnings --- EhPanda/Models/Gallery/GalleryState.swift | 25 +++++++++++-------- EhPanda/Models/Persistent/AppEnv.swift | 17 +++++++------ EhPanda/View/Reading/ReadingView.swift | 9 ++++++- EhPanda/View/Setting/Support/LogsView.swift | 11 +++++--- .../Support/Components/ToolbarItems.swift | 4 +-- 5 files changed, 41 insertions(+), 25 deletions(-) diff --git a/EhPanda/Models/Gallery/GalleryState.swift b/EhPanda/Models/Gallery/GalleryState.swift index 9ca74759..f151b670 100644 --- a/EhPanda/Models/Gallery/GalleryState.swift +++ b/EhPanda/Models/Gallery/GalleryState.swift @@ -24,17 +24,20 @@ struct GalleryState: Codable { } extension GalleryState: CustomStringConvertible { var description: String { - let params = String(describing: [ - "gid": gid, - "tagsCount": tags.count, - "readingProgress": readingProgress, - "previewURLsCount": previewURLs.count, - "previewConfig": String(describing: previewConfig), - "commentsCount": comments.count, - "imageURLsCount": imageURLs.count, - "originalImageURLsCount": originalImageURLs.count, - "thumbnailURLsCount": thumbnailURLs.count - ]) + let params = String( + describing: [ + "gid": gid, + "tagsCount": tags.count, + "readingProgress": readingProgress, + "previewURLsCount": previewURLs.count, + "previewConfig": String(describing: previewConfig), + "commentsCount": comments.count, + "imageURLsCount": imageURLs.count, + "originalImageURLsCount": originalImageURLs.count, + "thumbnailURLsCount": thumbnailURLs.count + ] + as [String: Any] + ) return "GalleryState(\(params))" } } diff --git a/EhPanda/Models/Persistent/AppEnv.swift b/EhPanda/Models/Persistent/AppEnv.swift index e1cab1ef..9d101598 100644 --- a/EhPanda/Models/Persistent/AppEnv.swift +++ b/EhPanda/Models/Persistent/AppEnv.swift @@ -18,13 +18,16 @@ struct AppEnv: Codable { extension AppEnv: CustomStringConvertible { var description: String { - let params = String(describing: [ - "user": user, - "setting": setting, - "tagTranslator": tagTranslator, - "historyKeywordsCount": historyKeywords.count, - "quickSearchWordsCount": quickSearchWords.count - ]) + let params = String( + describing: [ + "user": user, + "setting": setting, + "tagTranslator": tagTranslator, + "historyKeywordsCount": historyKeywords.count, + "quickSearchWordsCount": quickSearchWords.count + ] + as [String: Any] + ) return "AppEnv(\(params))" } } diff --git a/EhPanda/View/Reading/ReadingView.swift b/EhPanda/View/Reading/ReadingView.swift index 116b40bc..ae124cb2 100644 --- a/EhPanda/View/Reading/ReadingView.swift +++ b/EhPanda/View/Reading/ReadingView.swift @@ -246,7 +246,14 @@ extension ReadingView { Logger.info("analyzeImageForLiveText image not found", context: ["index": index]) } case .failure(let error): - Logger.info("analyzeImageForLiveText failed", context: ["index": index, "error": error]) + Logger.info( + "analyzeImageForLiveText failed", + context: [ + "index": index, + "error": error + ] + as [String: Any] + ) } } } diff --git a/EhPanda/View/Setting/Support/LogsView.swift b/EhPanda/View/Setting/Support/LogsView.swift index 0ba62cf8..5de76b43 100644 --- a/EhPanda/View/Setting/Support/LogsView.swift +++ b/EhPanda/View/Setting/Support/LogsView.swift @@ -160,10 +160,13 @@ struct Log: Identifiable, Comparable { } extension Log: CustomStringConvertible { var description: String { - let params = String(describing: [ - "fileName": fileName, - "contentsCount": contents.count - ]) + let params = String( + describing: [ + "fileName": fileName, + "contentsCount": contents.count + ] + as [String: Any] + ) return "Log(\(params))" } } diff --git a/EhPanda/View/Support/Components/ToolbarItems.swift b/EhPanda/View/Support/Components/ToolbarItems.swift index 636880df..727b4966 100644 --- a/EhPanda/View/Support/Components/ToolbarItems.swift +++ b/EhPanda/View/Support/Components/ToolbarItems.swift @@ -136,7 +136,7 @@ struct FavoritesIndexMenu: View { } } } label: { - Image(systemSymbol: .dialMin) + Image(systemSymbol: .dialLow) .symbolRenderingMode(.hierarchical) } } @@ -164,7 +164,7 @@ struct ToplistsTypeMenu: View { } } } label: { - Image(systemSymbol: .dialMin) + Image(systemSymbol: .dialLow) .symbolRenderingMode(.hierarchical) } } From 87a2bae3ea045ffff4db5acbf2e1f10ab44234ed Mon Sep 17 00:00:00 2001 From: Chihchy Date: Mon, 8 May 2023 23:48:10 +0800 Subject: [PATCH 06/29] Prepare client APIs for new TCA reducers --- .../App/Tools/Clients/AppDelegateClient.swift | 22 ++++++++++++++++ .../Tools/Clients/AuthorizationClient.swift | 14 +++++++++++ .../App/Tools/Clients/ClipboardClient.swift | 14 +++++++++++ EhPanda/App/Tools/Clients/CookiesClient.swift | 14 +++++++++++ EhPanda/App/Tools/Clients/DFClient.swift | 21 ++++++++++++++++ .../App/Tools/Clients/DatabaseClient.swift | 14 +++++++++++ EhPanda/App/Tools/Clients/DeviceClient.swift | 25 +++++++++++++++++++ EhPanda/App/Tools/Clients/FileClient.swift | 14 +++++++++++ EhPanda/App/Tools/Clients/HapticClient.swift | 15 +++++++++++ EhPanda/App/Tools/Clients/ImageClient.swift | 24 ++++++++++++++++++ EhPanda/App/Tools/Clients/LibraryClient.swift | 14 +++++++++++ EhPanda/App/Tools/Clients/LoggerClient.swift | 14 +++++++++++ .../Tools/Clients/UIApplicationClient.swift | 14 +++++++++++ EhPanda/App/Tools/Clients/URLClient.swift | 25 ++++++++++++++++++- .../Tools/Clients/UserDefaultsClient.swift | 21 ++++++++++++++++ 15 files changed, 264 insertions(+), 1 deletion(-) diff --git a/EhPanda/App/Tools/Clients/AppDelegateClient.swift b/EhPanda/App/Tools/Clients/AppDelegateClient.swift index 39833e09..427ea27e 100644 --- a/EhPanda/App/Tools/Clients/AppDelegateClient.swift +++ b/EhPanda/App/Tools/Clients/AppDelegateClient.swift @@ -37,3 +37,25 @@ extension AppDelegateClient { setOrientationMask([.portrait, .portraitUpsideDown]) } } + +// MARK: API +enum AppDelegateClientKey: DependencyKey { + static let liveValue = AppDelegateClient.live + static let testValue = AppDelegateClient.noop + static let previewValue = AppDelegateClient.noop +} + +extension DependencyValues { + var appDelegateClient: AppDelegateClient { + get { self[AppDelegateClientKey.self] } + set { self[AppDelegateClientKey.self] = newValue } + } +} + +// MARK: Test +extension AppDelegateClient { + static let noop: Self = .init( + setOrientation: { _ in .none }, + setOrientationMask: { _ in .none } + ) +} diff --git a/EhPanda/App/Tools/Clients/AuthorizationClient.swift b/EhPanda/App/Tools/Clients/AuthorizationClient.swift index bc315977..39f32b21 100644 --- a/EhPanda/App/Tools/Clients/AuthorizationClient.swift +++ b/EhPanda/App/Tools/Clients/AuthorizationClient.swift @@ -40,6 +40,20 @@ extension AuthorizationClient { ) } +// MARK: API +enum AuthorizationClientKey: DependencyKey { + static let liveValue = AuthorizationClient.live + static let testValue = AuthorizationClient.noop + static let previewValue = AuthorizationClient.noop +} + +extension DependencyValues { + var authorizationClient: AuthorizationClient { + get { self[AuthorizationClientKey.self] } + set { self[AuthorizationClientKey.self] = newValue } + } +} + // MARK: Test #if DEBUG import XCTestDynamicOverlay diff --git a/EhPanda/App/Tools/Clients/ClipboardClient.swift b/EhPanda/App/Tools/Clients/ClipboardClient.swift index 2e952375..fdea8f18 100644 --- a/EhPanda/App/Tools/Clients/ClipboardClient.swift +++ b/EhPanda/App/Tools/Clients/ClipboardClient.swift @@ -49,6 +49,20 @@ extension ClipboardClient { ) } +// MARK: API +enum ClipboardClientKey: DependencyKey { + static let liveValue = ClipboardClient.live + static let testValue = ClipboardClient.noop + static let previewValue = ClipboardClient.noop +} + +extension DependencyValues { + var clipboardClient: ClipboardClient { + get { self[ClipboardClientKey.self] } + set { self[ClipboardClientKey.self] = newValue } + } +} + // MARK: Test #if DEBUG import XCTestDynamicOverlay diff --git a/EhPanda/App/Tools/Clients/CookiesClient.swift b/EhPanda/App/Tools/Clients/CookiesClient.swift index 34b21dd0..94d2e2dc 100644 --- a/EhPanda/App/Tools/Clients/CookiesClient.swift +++ b/EhPanda/App/Tools/Clients/CookiesClient.swift @@ -244,6 +244,20 @@ extension CookiesClient { } } +// MARK: API +enum CookiesClientKey: DependencyKey { + static let liveValue = CookiesClient.live + static let testValue = CookiesClient.noop + static let previewValue = CookiesClient.noop +} + +extension DependencyValues { + var cookiesClient: CookiesClient { + get { self[CookiesClientKey.self] } + set { self[CookiesClientKey.self] = newValue } + } +} + // MARK: Test #if DEBUG import XCTestDynamicOverlay diff --git a/EhPanda/App/Tools/Clients/DFClient.swift b/EhPanda/App/Tools/Clients/DFClient.swift index 3ab7c121..b43a4487 100644 --- a/EhPanda/App/Tools/Clients/DFClient.swift +++ b/EhPanda/App/Tools/Clients/DFClient.swift @@ -30,3 +30,24 @@ extension DFClient { } ) } + +// MARK: API +enum DFClientKey: DependencyKey { + static let liveValue = DFClient.live + static let testValue = DFClient.noop + static let previewValue = DFClient.noop +} + +extension DependencyValues { + var dfClient: DFClient { + get { self[DFClientKey.self] } + set { self[DFClientKey.self] = newValue } + } +} + +// MARK: Test +extension DFClient { + static let noop: Self = .init( + setActive: { _ in .none } + ) +} diff --git a/EhPanda/App/Tools/Clients/DatabaseClient.swift b/EhPanda/App/Tools/Clients/DatabaseClient.swift index 3af2c561..e7a3b4d2 100644 --- a/EhPanda/App/Tools/Clients/DatabaseClient.swift +++ b/EhPanda/App/Tools/Clients/DatabaseClient.swift @@ -535,6 +535,20 @@ extension DatabaseClient { } } +// MARK: API +enum DatabaseClientKey: DependencyKey { + static let liveValue = DatabaseClient.live + static let testValue = DatabaseClient.noop + static let previewValue = DatabaseClient.noop +} + +extension DependencyValues { + var databaseClient: DatabaseClient { + get { self[DatabaseClientKey.self] } + set { self[DatabaseClientKey.self] = newValue } + } +} + // MARK: Test #if DEBUG import XCTestDynamicOverlay diff --git a/EhPanda/App/Tools/Clients/DeviceClient.swift b/EhPanda/App/Tools/Clients/DeviceClient.swift index 710db876..0c437388 100644 --- a/EhPanda/App/Tools/Clients/DeviceClient.swift +++ b/EhPanda/App/Tools/Clients/DeviceClient.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Dependencies struct DeviceClient { let isPad: () -> Bool @@ -30,3 +31,27 @@ extension DeviceClient { } ) } + +// MARK: API +enum DeviceClientKey: DependencyKey { + static let liveValue = DeviceClient.live + static let testValue = DeviceClient.noop + static let previewValue = DeviceClient.noop +} + +extension DependencyValues { + var deviceClient: DeviceClient { + get { self[DeviceClientKey.self] } + set { self[DeviceClientKey.self] = newValue } + } +} + +// MARK: Test +extension DeviceClient { + static let noop: Self = .init( + isPad: { false }, + absWindowW: { .zero }, + absWindowH: { .zero }, + touchPoint: { .zero } + ) +} diff --git a/EhPanda/App/Tools/Clients/FileClient.swift b/EhPanda/App/Tools/Clients/FileClient.swift index 3c321b07..60478ebc 100644 --- a/EhPanda/App/Tools/Clients/FileClient.swift +++ b/EhPanda/App/Tools/Clients/FileClient.swift @@ -105,6 +105,20 @@ extension FileClient { } } +// MARK: API +enum FileClientKey: DependencyKey { + static let liveValue = FileClient.live + static let testValue = FileClient.noop + static let previewValue = FileClient.noop +} + +extension DependencyValues { + var fileClient: FileClient { + get { self[FileClientKey.self] } + set { self[FileClientKey.self] = newValue } + } +} + // MARK: Test #if DEBUG import XCTestDynamicOverlay diff --git a/EhPanda/App/Tools/Clients/HapticClient.swift b/EhPanda/App/Tools/Clients/HapticClient.swift index 3f5d00fc..960ec9d5 100644 --- a/EhPanda/App/Tools/Clients/HapticClient.swift +++ b/EhPanda/App/Tools/Clients/HapticClient.swift @@ -27,6 +27,21 @@ extension HapticClient { } ) } + +// MARK: API +enum HapticClientKey: DependencyKey { + static let liveValue = HapticClient.live + static let testValue = HapticClient.noop + static let previewValue = HapticClient.noop +} + +extension DependencyValues { + var hapticClient: HapticClient { + get { self[HapticClientKey.self] } + set { self[HapticClientKey.self] = newValue } + } +} + // MARK: Test #if DEBUG extension HapticClient { diff --git a/EhPanda/App/Tools/Clients/ImageClient.swift b/EhPanda/App/Tools/Clients/ImageClient.swift index c11ca8a2..53747340 100644 --- a/EhPanda/App/Tools/Clients/ImageClient.swift +++ b/EhPanda/App/Tools/Clients/ImageClient.swift @@ -103,3 +103,27 @@ private final class ImageSaver: NSObject { completion(error == nil) } } + +// MARK: API +enum ImageClientKey: DependencyKey { + static let liveValue = ImageClient.live + static let testValue = ImageClient.noop + static let previewValue = ImageClient.noop +} + +extension DependencyValues { + var imageClient: ImageClient { + get { self[ImageClientKey.self] } + set { self[ImageClientKey.self] = newValue } + } +} + +// MARK: Test +extension ImageClient { + static let noop: Self = .init( + prefetchImages: { _ in .none }, + saveImageToPhotoLibrary: { _, _ in .none }, + downloadImage: { _ in .none }, + retrieveImage: { _ in .none } + ) +} diff --git a/EhPanda/App/Tools/Clients/LibraryClient.swift b/EhPanda/App/Tools/Clients/LibraryClient.swift index d6cca102..a4142815 100644 --- a/EhPanda/App/Tools/Clients/LibraryClient.swift +++ b/EhPanda/App/Tools/Clients/LibraryClient.swift @@ -88,6 +88,20 @@ extension LibraryClient { ) } +// MARK: API +enum LibraryClientKey: DependencyKey { + static let liveValue = LibraryClient.live + static let testValue = LibraryClient.noop + static let previewValue = LibraryClient.noop +} + +extension DependencyValues { + var libraryClient: LibraryClient { + get { self[LibraryClientKey.self] } + set { self[LibraryClientKey.self] = newValue } + } +} + // MARK: Test #if DEBUG import XCTestDynamicOverlay diff --git a/EhPanda/App/Tools/Clients/LoggerClient.swift b/EhPanda/App/Tools/Clients/LoggerClient.swift index c230b9db..f4eb8184 100644 --- a/EhPanda/App/Tools/Clients/LoggerClient.swift +++ b/EhPanda/App/Tools/Clients/LoggerClient.swift @@ -27,6 +27,20 @@ extension LoggerClient { ) } +// MARK: API +enum LoggerClientKey: DependencyKey { + static let liveValue = LoggerClient.live + static let testValue = LoggerClient.noop + static let previewValue = LoggerClient.noop +} + +extension DependencyValues { + var loggerClient: LoggerClient { + get { self[LoggerClientKey.self] } + set { self[LoggerClientKey.self] = newValue } + } +} + // MARK: Test #if DEBUG import XCTestDynamicOverlay diff --git a/EhPanda/App/Tools/Clients/UIApplicationClient.swift b/EhPanda/App/Tools/Clients/UIApplicationClient.swift index 97603507..153d8f8e 100644 --- a/EhPanda/App/Tools/Clients/UIApplicationClient.swift +++ b/EhPanda/App/Tools/Clients/UIApplicationClient.swift @@ -67,6 +67,20 @@ extension UIApplicationClient { } } +// MARK: API +enum UIApplicationClientKey: DependencyKey { + static let liveValue = UIApplicationClient.live + static let testValue = UIApplicationClient.noop + static let previewValue = UIApplicationClient.noop +} + +extension DependencyValues { + var uiApplicationClient: UIApplicationClient { + get { self[UIApplicationClientKey.self] } + set { self[UIApplicationClientKey.self] = newValue } + } +} + // MARK: Test // swiftlint:disable line_length #if DEBUG diff --git a/EhPanda/App/Tools/Clients/URLClient.swift b/EhPanda/App/Tools/Clients/URLClient.swift index d6487e14..935a1253 100644 --- a/EhPanda/App/Tools/Clients/URLClient.swift +++ b/EhPanda/App/Tools/Clients/URLClient.swift @@ -6,7 +6,7 @@ // import SwiftUI -import ComposableArchitecture +import Dependencies struct URLClient { let checkIfHandleable: (URL) -> Bool @@ -67,3 +67,26 @@ extension URLClient { return (isGalleryImageURL, pageIndex, commentID) } } + +// MARK: API +enum URLClientKey: DependencyKey { + static let liveValue = URLClient.live + static let testValue = URLClient.noop + static let previewValue = URLClient.noop +} + +extension DependencyValues { + var urlClient: URLClient { + get { self[URLClientKey.self] } + set { self[URLClientKey.self] = newValue } + } +} + +// MARK: Test +extension URLClient { + static let noop: Self = .init( + checkIfHandleable: { _ in false }, + checkIfMPVURL: { _ in false }, + parseGalleryID: { _ in .init() } + ) +} diff --git a/EhPanda/App/Tools/Clients/UserDefaultsClient.swift b/EhPanda/App/Tools/Clients/UserDefaultsClient.swift index 8b0b3937..45847af6 100644 --- a/EhPanda/App/Tools/Clients/UserDefaultsClient.swift +++ b/EhPanda/App/Tools/Clients/UserDefaultsClient.swift @@ -25,3 +25,24 @@ extension UserDefaultsClient { UserDefaultsUtil.value(forKey: key) } } + +// MARK: API +enum UserDefaultsClientKey: DependencyKey { + static let liveValue = UserDefaultsClient.live + static let testValue = UserDefaultsClient.noop + static let previewValue = UserDefaultsClient.noop +} + +extension DependencyValues { + var userDefaultsClient: UserDefaultsClient { + get { self[UserDefaultsClientKey.self] } + set { self[UserDefaultsClientKey.self] = newValue } + } +} + +// MARK: Test +extension UserDefaultsClient { + static let noop: Self = .init( + setValue: { _, _ in .none } + ) +} From 5c3a222d8d0ced8c9f39a3037e7377be9774e7c4 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Mon, 8 May 2023 23:48:24 +0800 Subject: [PATCH 07/29] Refactor AccountSettingReducer --- EhPanda.xcodeproj/project.pbxproj | 18 +- .../Tools/Extensions/Reducer_Extension.swift | 29 +++ .../AccountSettingReducer.swift | 161 ++++++++++++++++ .../AccountSettingView.swift | 32 ++-- .../DataFlow/AccountSettingStore.swift | 177 ------------------ .../View/Setting/DataFlow/SettingStore.swift | 29 +-- 6 files changed, 231 insertions(+), 215 deletions(-) create mode 100644 EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift rename EhPanda/View/Setting/{ => AccountSetting}/AccountSettingView.swift (89%) delete mode 100644 EhPanda/View/Setting/DataFlow/AccountSettingStore.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 69e84c4d..78f0ffa6 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - AB0929B6277F043D00F107CA /* AccountSettingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929B5277F043D00F107CA /* AccountSettingStore.swift */; }; + AB0929B6277F043D00F107CA /* AccountSettingReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929B5277F043D00F107CA /* AccountSettingReducer.swift */; }; AB0929BE2780032400F107CA /* EhSettingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929BD2780032400F107CA /* EhSettingStore.swift */; }; AB0929C027805A8200F107CA /* LoginStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929BF27805A8200F107CA /* LoginStore.swift */; }; AB0929C6278160AE00F107CA /* LibraryClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929C5278160AE00F107CA /* LibraryClient.swift */; }; @@ -309,7 +309,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - AB0929B5277F043D00F107CA /* AccountSettingStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingStore.swift; sourceTree = ""; }; + AB0929B5277F043D00F107CA /* AccountSettingReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingReducer.swift; sourceTree = ""; }; AB0929BD2780032400F107CA /* EhSettingStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EhSettingStore.swift; sourceTree = ""; }; AB0929BF27805A8200F107CA /* LoginStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginStore.swift; sourceTree = ""; }; AB0929C5278160AE00F107CA /* LibraryClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryClient.swift; sourceTree = ""; }; @@ -634,7 +634,6 @@ isa = PBXGroup; children = ( ABD49D69277EEF73003D1A07 /* SettingStore.swift */, - AB0929B5277F043D00F107CA /* AccountSettingStore.swift */, AB0929D52782A65F00F107CA /* GeneralSettingStore.swift */, AB86AC092782FAFA00E61E6A /* AppearanceSettingStore.swift */, AB0929BD2780032400F107CA /* EhSettingStore.swift */, @@ -1369,8 +1368,8 @@ ABF45AD725F3313D00ECB568 /* Setting */ = { isa = PBXGroup; children = ( + EA2B9B042A0A89C900E7BA07 /* AccountSetting */, ABF45ADD25F3313D00ECB568 /* SettingView.swift */, - ABF45AD925F3313D00ECB568 /* AccountSettingView.swift */, ABF45AD825F3313D00ECB568 /* GeneralSettingView.swift */, ABF45ADC25F3313D00ECB568 /* AppearanceSettingView.swift */, ABF45ADB25F3313D00ECB568 /* ReadingSettingView.swift */, @@ -1382,6 +1381,15 @@ path = Setting; sourceTree = ""; }; + EA2B9B042A0A89C900E7BA07 /* AccountSetting */ = { + isa = PBXGroup; + children = ( + ABF45AD925F3313D00ECB568 /* AccountSettingView.swift */, + AB0929B5277F043D00F107CA /* AccountSettingReducer.swift */, + ); + path = AccountSetting; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1830,7 +1838,7 @@ ABC732C527B9024500D47DA9 /* LiveText.swift in Sources */, ABF45AE825F3313D00ECB568 /* LinkedText.swift in Sources */, ABC732C727B90F0900D47DA9 /* LiveTextView.swift in Sources */, - AB0929B6277F043D00F107CA /* AccountSettingStore.swift in Sources */, + AB0929B6277F043D00F107CA /* AccountSettingReducer.swift in Sources */, ABD49D67277EAC90003D1A07 /* URLUtil.swift in Sources */, ABBB2638278FBD2F007B6149 /* SwiftUINavigation_Extension.swift in Sources */, AB10117E26986B7D00C2C1A9 /* GalleryStateMO+CoreDataProperties.swift in Sources */, diff --git a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift index 0058ff88..72fd2920 100644 --- a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift +++ b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift @@ -47,3 +47,32 @@ extension Reducer { } } } + +extension ReducerProtocol { + func haptics( + unwrapping enum: @escaping (State) -> Enum?, + case casePath: CasePath, + hapticsClient: HapticClient, + style: UIImpactFeedbackGenerator.FeedbackStyle = .light + ) -> some ReducerProtocol { + onBecomeNonNil(unwrapping: `enum`, case: casePath) { _, _ in + .fireAndForget({ hapticsClient.generateFeedback(style) }) + } + } + + func onBecomeNonNil( + unwrapping enum: @escaping (State) -> Enum?, + case casePath: CasePath, + perform additionalEffects: @escaping (inout State, Action) -> EffectTask + ) -> some ReducerProtocol { + Reduce { state, action in + let previousCase = Binding.constant(`enum`(state)).case(casePath).wrappedValue + let effects = reduce(into: &state, action: action) + let currentCase = Binding.constant(`enum`(state)).case(casePath).wrappedValue + + return previousCase == nil && currentCase != nil + ? .merge(effects, additionalEffects(&state, action)) + : effects + } + } +} diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift new file mode 100644 index 00000000..7511f7f2 --- /dev/null +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift @@ -0,0 +1,161 @@ +// +// AccountSettingReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/12/31. +// + +import Foundation +import TTProgressHUD +import ComposableArchitecture + +struct AccountSettingReducer: ReducerProtocol { + enum Route: Equatable { + case hud + case login + case logout + case ehSetting + case webView(URL) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var ehCookiesState: CookiesState = .empty(.ehentai) + @BindingState var exCookiesState: CookiesState = .empty(.exhentai) + var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded + + var loginState = LoginState() + var ehSettingState = EhSettingState() + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case setNavigation(Route?) + case onLogoutConfirmButtonTapped + case clearSubStates + + case loadCookies + case copyCookies(GalleryHost) + + case login(LoginAction) + case ehSetting(EhSettingAction) + } + + @Dependency(\.clipboardClient) private var clipboardClient + @Dependency(\.cookiesClient) private var cookiesClient + @Dependency(\.hapticClient) private var hapticClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding(\.$ehCookiesState): + return cookiesClient.setCookies(state: state.ehCookiesState).fireAndForget() + + case .binding(\.$exCookiesState): + return cookiesClient.setCookies(state: state.exCookiesState).fireAndForget() + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .onLogoutConfirmButtonTapped: + return .init(value: .loadCookies) + + case .clearSubStates: + state.loginState = .init() + state.ehSettingState = .init() + return .merge( + .init(value: .login(.teardown)), + .init(value: .ehSetting(.teardown)) + ) + + case .loadCookies: + state.ehCookiesState = cookiesClient.loadCookiesState(host: .ehentai) + state.exCookiesState = cookiesClient.loadCookiesState(host: .exhentai) + return .none + + case .copyCookies(let host): + let cookiesDescription = cookiesClient.getCookiesDescription(host: host) + return .merge( + .init(value: .setNavigation(.hud)), + clipboardClient.saveText(cookiesDescription).fireAndForget(), + hapticClient.generateNotificationFeedback(.success).fireAndForget() + ) + + case .login(.loginDone): + return cookiesClient.didLogin ? .init(value: .setNavigation(nil)) : .none + + case .login: + return .none + + case .ehSetting: + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.webView, + hapticsClient: hapticClient + ) + +// // TODO: Child reducers +// Scope(state: \.loginState, action: /Action.login, child: { loginReducer }) +// Scope(state: \.ehSettingState, action: /Action.ehSetting, child: { ehSettingReducer }) + + BindingReducer() + } +} + +// MARK: Models +struct CookieValue: Equatable { + static let empty: Self = .init( + rawValue: .init(), localizedString: .init() + ) + + let rawValue: String + let localizedString: String + + var isInvalid: Bool { + !localizedString.isEmpty && !rawValue.isEmpty + } + var placeholder: String { + localizedString.isEmpty ? rawValue : localizedString + } +} + +struct CookiesState: Equatable { + static func empty(_ host: GalleryHost) -> Self { + .init( + host: host, + igneous: .empty, + memberID: .empty, + passHash: .empty + ) + } + var allCases: [CookieState] {[ + igneous, memberID, passHash + ]} + + let host: GalleryHost + var igneous: CookieState + var memberID: CookieState + var passHash: CookieState +} + +struct CookieState: Equatable { + static let empty: Self = .init( + key: "", value: .init( + rawValue: "", localizedString: "" + ) + ) + + let key: String + var value: CookieValue + var editingText = "" +} diff --git a/EhPanda/View/Setting/AccountSettingView.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift similarity index 89% rename from EhPanda/View/Setting/AccountSettingView.swift rename to EhPanda/View/Setting/AccountSetting/AccountSettingView.swift index 0886ca43..7bc49d19 100644 --- a/EhPanda/View/Setting/AccountSettingView.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift @@ -9,15 +9,15 @@ import SwiftUI import ComposableArchitecture struct AccountSettingView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf @Binding private var galleryHost: GalleryHost @Binding private var showsNewDawnGreeting: Bool private let bypassesSNIFiltering: Bool private let blurRadius: Double init( - store: Store, + store: StoreOf, galleryHost: Binding, showsNewDawnGreeting: Binding, bypassesSNIFiltering: Bool, blurRadius: Double ) { @@ -63,9 +63,9 @@ struct AccountSettingView: View { .progressHUD( config: viewStore.hudConfig, unwrapping: viewStore.binding(\.$route), - case: /AccountSettingState.Route.hud + case: /AccountSettingReducer.Route.hud ) - .sheet(unwrapping: viewStore.binding(\.$route), case: /AccountSettingState.Route.webView) { route in + .sheet(unwrapping: viewStore.binding(\.$route), case: /AccountSettingReducer.Route.webView) { route in WebView(url: route.wrappedValue) .autoBlur(radius: blurRadius) } @@ -78,15 +78,15 @@ struct AccountSettingView: View { // MARK: NavigationLinks private extension AccountSettingView { @ViewBuilder var navigationLinks: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /AccountSettingState.Route.login) { _ in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /AccountSettingReducer.Route.login) { _ in LoginView( - store: store.scope(state: \.loginState, action: AccountSettingAction.login), + store: store.scope(state: \.loginState, action: AccountSettingReducer.Action.login), bypassesSNIFiltering: bypassesSNIFiltering, blurRadius: blurRadius ) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /AccountSettingState.Route.ehSetting) { _ in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /AccountSettingReducer.Route.ehSetting) { _ in EhSettingView( - store: store.scope(state: \.ehSettingState, action: AccountSettingAction.ehSetting), + store: store.scope(state: \.ehSettingState, action: AccountSettingReducer.Action.ehSetting), bypassesSNIFiltering: bypassesSNIFiltering, blurRadius: blurRadius ) } @@ -95,7 +95,7 @@ private extension AccountSettingView { // MARK: AccountSection private struct AccountSection: View { - @Binding private var route: AccountSettingState.Route? + @Binding private var route: AccountSettingReducer.Route? @Binding private var showsNewDawnGreeting: Bool private let bypassesSNIFiltering: Bool private let loginAction: () -> Void @@ -105,7 +105,7 @@ private struct AccountSection: View { private let manageTagsAction: () -> Void init( - route: Binding, + route: Binding, showsNewDawnGreeting: Binding, bypassesSNIFiltering: Bool, loginAction: @escaping () -> Void, logoutAction: @escaping () -> Void, logoutDialogAction: @escaping () -> Void, @@ -132,7 +132,7 @@ private struct AccountSection: View { ) .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.logout, - unwrapping: $route, case: /AccountSettingState.Route.logout + unwrapping: $route, case: /AccountSettingReducer.Route.logout ) { Button( L10n.Localizable.ConfirmationDialog.Button.logout, @@ -224,13 +224,7 @@ struct AccountSettingView_Previews: PreviewProvider { AccountSettingView( store: .init( initialState: .init(), - reducer: accountSettingReducer, - environment: AccountSettingEnvironment( - hapticClient: .live, - cookiesClient: .live, - clipboardClient: .live, - uiApplicationClient: .live - ) + reducer: AccountSettingReducer() ), galleryHost: .constant(.ehentai), showsNewDawnGreeting: .constant(false), diff --git a/EhPanda/View/Setting/DataFlow/AccountSettingStore.swift b/EhPanda/View/Setting/DataFlow/AccountSettingStore.swift deleted file mode 100644 index e48034ee..00000000 --- a/EhPanda/View/Setting/DataFlow/AccountSettingStore.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// AccountSettingStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 3/12/31. -// - -import Foundation -import TTProgressHUD -import ComposableArchitecture - -struct CookieValue: Equatable { - static let empty: Self = .init( - rawValue: .init(), localizedString: .init() - ) - - let rawValue: String - let localizedString: String - - var isInvalid: Bool { - !localizedString.isEmpty && !rawValue.isEmpty - } - var placeholder: String { - localizedString.isEmpty ? rawValue : localizedString - } -} - -struct CookiesState: Equatable { - static func empty(_ host: GalleryHost) -> Self { - .init( - host: host, - igneous: .empty, - memberID: .empty, - passHash: .empty - ) - } - var allCases: [CookieState] {[ - igneous, memberID, passHash - ]} - - let host: GalleryHost - var igneous: CookieState - var memberID: CookieState - var passHash: CookieState -} - -struct CookieState: Equatable { - static let empty: Self = .init( - key: "", value: .init( - rawValue: "", localizedString: "" - ) - ) - - let key: String - var value: CookieValue - var editingText = "" -} - -struct AccountSettingState: Equatable { - enum Route: Equatable { - case hud - case login - case logout - case ehSetting - case webView(URL) - } - - @BindingState var route: Route? - @BindingState var ehCookiesState: CookiesState = .empty(.ehentai) - @BindingState var exCookiesState: CookiesState = .empty(.exhentai) - var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded - - var loginState = LoginState() - var ehSettingState = EhSettingState() -} - -enum AccountSettingAction: BindableAction, Equatable { - case binding(BindingAction) - case setNavigation(AccountSettingState.Route?) - case onLogoutConfirmButtonTapped - case clearSubStates - - case loadCookies - case copyCookies(GalleryHost) - - case login(LoginAction) - case ehSetting(EhSettingAction) -} - -struct AccountSettingEnvironment { - let hapticClient: HapticClient - let cookiesClient: CookiesClient - let clipboardClient: ClipboardClient - let uiApplicationClient: UIApplicationClient -} - -let accountSettingReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding(\.$ehCookiesState): - return environment.cookiesClient.setCookies(state: state.ehCookiesState).fireAndForget() - - case .binding(\.$exCookiesState): - return environment.cookiesClient.setCookies(state: state.exCookiesState).fireAndForget() - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .onLogoutConfirmButtonTapped: - return .init(value: .loadCookies) - - case .clearSubStates: - state.loginState = .init() - state.ehSettingState = .init() - return .merge( - .init(value: .login(.teardown)), - .init(value: .ehSetting(.teardown)) - ) - - case .loadCookies: - state.ehCookiesState = environment.cookiesClient.loadCookiesState(host: .ehentai) - state.exCookiesState = environment.cookiesClient.loadCookiesState(host: .exhentai) - return .none - - case .copyCookies(let host): - let cookiesDescription = environment.cookiesClient.getCookiesDescription(host: host) - return .merge( - .init(value: .setNavigation(.hud)), - environment.clipboardClient.saveText(cookiesDescription).fireAndForget(), - environment.hapticClient.generateNotificationFeedback(.success).fireAndForget() - ) - - case .login(.loginDone): - return environment.cookiesClient.didLogin ? .init(value: .setNavigation(nil)) : .none - - case .login: - return .none - - case .ehSetting: - return .none - } - } - .haptics( - unwrapping: \.route, - case: /AccountSettingState.Route.webView, - hapticClient: \.hapticClient - ) - .binding(), - loginReducer.pullback( - state: \.loginState, - action: /AccountSettingAction.login, - environment: { - .init( - hapticClient: $0.hapticClient, - cookiesClient: $0.cookiesClient - ) - } - ), - ehSettingReducer.pullback( - state: \.ehSettingState, - action: /AccountSettingAction.ehSetting, - environment: { - .init( - hapticClient: $0.hapticClient, - cookiesClient: $0.cookiesClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ) -) diff --git a/EhPanda/View/Setting/DataFlow/SettingStore.swift b/EhPanda/View/Setting/DataFlow/SettingStore.swift index aaab606e..58ff4225 100644 --- a/EhPanda/View/Setting/DataFlow/SettingStore.swift +++ b/EhPanda/View/Setting/DataFlow/SettingStore.swift @@ -28,7 +28,7 @@ struct SettingState: Equatable { @BindingState var route: Route? var tagTranslatorLoadingState: LoadingState = .idle - var accountSettingState = AccountSettingState() + var accountSettingState = AccountSettingReducer.State() var generalSettingState = GeneralSettingState() var appearanceSettingState = AppearanceSettingState() @@ -88,7 +88,7 @@ enum SettingAction: BindableAction { case fetchFavoriteCategories case fetchFavoriteCategoriesDone(Result<[Int: String], AppError>) - case account(AccountSettingAction) + case account(AccountSettingReducer.Action) case general(GeneralSettingAction) case appearance(AppearanceSettingAction) } @@ -454,18 +454,19 @@ let settingReducer = Reducer.co } } .binding(), - accountSettingReducer.pullback( - state: \.accountSettingState, - action: /SettingAction.account, - environment: { - .init( - hapticClient: $0.hapticClient, - cookiesClient: $0.cookiesClient, - clipboardClient: $0.clipboardClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ), +// // TODO: Parent reducer +// accountSettingReducer.pullback( +// state: \.accountSettingState, +// action: /SettingAction.account, +// environment: { +// .init( +// hapticClient: $0.hapticClient, +// cookiesClient: $0.cookiesClient, +// clipboardClient: $0.clipboardClient, +// uiApplicationClient: $0.uiApplicationClient +// ) +// } +// ), generalSettingReducer.pullback( state: \.generalSettingState, action: /SettingAction.general, From ee745b3dbe32717ff53665cad0c7d7a98d434530 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Tue, 9 May 2023 22:42:30 +0800 Subject: [PATCH 08/29] Rename haptic to haptics --- EhPanda.xcodeproj/project.pbxproj | 16 ++--- EhPanda/App/Tools/Clients/HapticClient.swift | 59 ------------------- EhPanda/App/Tools/Clients/HapticsClient.swift | 51 ++++++++++++++++ .../Tools/Extensions/Reducer_Extension.swift | 6 +- .../{HapticUtil.swift => HapticsUtil.swift} | 4 +- EhPanda/DataFlow/AppDelegateStore.swift | 2 +- EhPanda/DataFlow/AppRouteStore.swift | 8 +-- EhPanda/DataFlow/AppStore.swift | 16 ++--- EhPanda/View/Detail/ArchivesView.swift | 4 +- EhPanda/View/Detail/CommentsView.swift | 2 +- .../View/Detail/DataFlow/ArchivesStore.swift | 4 +- .../View/Detail/DataFlow/CommentsStore.swift | 4 +- .../Detail/DataFlow/DetailSearchStore.swift | 6 +- .../View/Detail/DataFlow/DetailStore.swift | 40 ++++++------- .../Detail/DataFlow/GalleryInfosStore.swift | 4 +- .../View/Detail/DataFlow/PreviewsStore.swift | 6 +- .../View/Detail/DataFlow/TorrentsStore.swift | 6 +- EhPanda/View/Detail/DetailSearchView.swift | 2 +- EhPanda/View/Detail/DetailView.swift | 2 +- EhPanda/View/Detail/GalleryInfosView.swift | 2 +- EhPanda/View/Detail/PreviewsView.swift | 2 +- EhPanda/View/Detail/TorrentsView.swift | 2 +- EhPanda/View/Favorites/FavoritesStore.swift | 6 +- EhPanda/View/Favorites/FavoritesView.swift | 2 +- .../View/Home/DataFlow/FrontpageStore.swift | 6 +- EhPanda/View/Home/DataFlow/HistoryStore.swift | 4 +- EhPanda/View/Home/DataFlow/HomeStore.swift | 14 ++--- EhPanda/View/Home/DataFlow/PopularStore.swift | 6 +- .../View/Home/DataFlow/ToplistsStore.swift | 8 +-- EhPanda/View/Home/DataFlow/WatchedStore.swift | 8 +-- EhPanda/View/Home/FrontpageView.swift | 2 +- EhPanda/View/Home/HistoryView.swift | 2 +- EhPanda/View/Home/HomeView.swift | 2 +- EhPanda/View/Home/PopularView.swift | 2 +- EhPanda/View/Home/ToplistsView.swift | 2 +- EhPanda/View/Home/WatchedView.swift | 2 +- EhPanda/View/Reading/ReadingStore.swift | 10 ++-- EhPanda/View/Reading/ReadingView.swift | 2 +- EhPanda/View/Search/SearchRootStore.swift | 10 ++-- EhPanda/View/Search/SearchRootView.swift | 2 +- EhPanda/View/Search/SearchStore.swift | 8 +-- EhPanda/View/Search/SearchView.swift | 2 +- .../AccountSettingReducer.swift | 10 ++-- .../Setting/DataFlow/EhSettingStore.swift | 4 +- .../View/Setting/DataFlow/SettingStore.swift | 6 +- EhPanda/View/Setting/SettingView.swift | 2 +- .../View/Setting/Support/EhSettingView.swift | 6 +- .../Support/Components/CategoryView.swift | 2 +- .../View/Support/Components/SubSection.swift | 2 +- EhPanda/View/TabBar/TabBarView.swift | 2 +- .../Setting/AccountSettingStoreTests.swift | 6 +- 51 files changed, 190 insertions(+), 198 deletions(-) delete mode 100644 EhPanda/App/Tools/Clients/HapticClient.swift create mode 100644 EhPanda/App/Tools/Clients/HapticsClient.swift rename EhPanda/App/Tools/Utilities/{HapticUtil.swift => HapticsUtil.swift} (97%) diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 78f0ffa6..427a1b74 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -13,7 +13,7 @@ AB0929C6278160AE00F107CA /* LibraryClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929C5278160AE00F107CA /* LibraryClient.swift */; }; AB0929C82781938A00F107CA /* DFClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929C72781938A00F107CA /* DFClient.swift */; }; AB0929CA278196ED00F107CA /* CookiesClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929C9278196ED00F107CA /* CookiesClient.swift */; }; - AB0929CC2781A0B000F107CA /* HapticClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929CB2781A0B000F107CA /* HapticClient.swift */; }; + AB0929CC2781A0B000F107CA /* HapticsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929CB2781A0B000F107CA /* HapticsClient.swift */; }; AB0929CE2781AADA00F107CA /* DatabaseClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929CD2781AADA00F107CA /* DatabaseClient.swift */; }; AB0929D02781E1CC00F107CA /* UIApplicationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929CF2781E1CC00F107CA /* UIApplicationClient.swift */; }; AB0929D22781E7D500F107CA /* LoggerClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929D12781E7D500F107CA /* LoggerClient.swift */; }; @@ -152,7 +152,7 @@ AB7BF2CC27A96A3C001865A3 /* GalleryTorrent.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2CB27A96A3C001865A3 /* GalleryTorrent.swift */; }; AB7BF2CE27AA3E58001865A3 /* AppUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2CD27AA3E58001865A3 /* AppUtil.swift */; }; AB7BF2D027AA3E75001865A3 /* DeviceUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2CF27AA3E75001865A3 /* DeviceUtil.swift */; }; - AB7BF2D227AA3EDC001865A3 /* HapticUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D127AA3EDC001865A3 /* HapticUtil.swift */; }; + AB7BF2D227AA3EDC001865A3 /* HapticsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D127AA3EDC001865A3 /* HapticsUtil.swift */; }; AB7BF2D427AA3F12001865A3 /* CookiesUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D327AA3F12001865A3 /* CookiesUtil.swift */; }; AB7BF2D627AA3F4C001865A3 /* FileUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D527AA3F4C001865A3 /* FileUtil.swift */; }; AB7BF2D827AA3F61001865A3 /* UserDefaultsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D727AA3F61001865A3 /* UserDefaultsUtil.swift */; }; @@ -315,7 +315,7 @@ AB0929C5278160AE00F107CA /* LibraryClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryClient.swift; sourceTree = ""; }; AB0929C72781938A00F107CA /* DFClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DFClient.swift; sourceTree = ""; }; AB0929C9278196ED00F107CA /* CookiesClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiesClient.swift; sourceTree = ""; }; - AB0929CB2781A0B000F107CA /* HapticClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticClient.swift; sourceTree = ""; }; + AB0929CB2781A0B000F107CA /* HapticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticsClient.swift; sourceTree = ""; }; AB0929CD2781AADA00F107CA /* DatabaseClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseClient.swift; sourceTree = ""; }; AB0929CF2781E1CC00F107CA /* UIApplicationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationClient.swift; sourceTree = ""; }; AB0929D12781E7D500F107CA /* LoggerClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerClient.swift; sourceTree = ""; }; @@ -452,7 +452,7 @@ AB7BF2CB27A96A3C001865A3 /* GalleryTorrent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryTorrent.swift; sourceTree = ""; }; AB7BF2CD27AA3E58001865A3 /* AppUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUtil.swift; sourceTree = ""; }; AB7BF2CF27AA3E75001865A3 /* DeviceUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceUtil.swift; sourceTree = ""; }; - AB7BF2D127AA3EDC001865A3 /* HapticUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticUtil.swift; sourceTree = ""; }; + AB7BF2D127AA3EDC001865A3 /* HapticsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticsUtil.swift; sourceTree = ""; }; AB7BF2D327AA3F12001865A3 /* CookiesUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiesUtil.swift; sourceTree = ""; }; AB7BF2D527AA3F4C001865A3 /* FileUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtil.swift; sourceTree = ""; }; AB7BF2D727AA3F61001865A3 /* UserDefaultsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsUtil.swift; sourceTree = ""; }; @@ -652,7 +652,7 @@ ABBB2676279CDBB0007B6149 /* ImageClient.swift */, AB706F8D278A5DCF0025A48A /* DeviceClient.swift */, AB0929D12781E7D500F107CA /* LoggerClient.swift */, - AB0929CB2781A0B000F107CA /* HapticClient.swift */, + AB0929CB2781A0B000F107CA /* HapticsClient.swift */, AB0929C5278160AE00F107CA /* LibraryClient.swift */, AB0929C9278196ED00F107CA /* CookiesClient.swift */, AB0929CD2781AADA00F107CA /* DatabaseClient.swift */, @@ -1250,7 +1250,7 @@ AB7BF2CD27AA3E58001865A3 /* AppUtil.swift */, AB7BF2D527AA3F4C001865A3 /* FileUtil.swift */, AB7BF2CF27AA3E75001865A3 /* DeviceUtil.swift */, - AB7BF2D127AA3EDC001865A3 /* HapticUtil.swift */, + AB7BF2D127AA3EDC001865A3 /* HapticsUtil.swift */, AB7BF2D327AA3F12001865A3 /* CookiesUtil.swift */, AB0CFBD427C24B3B004BD372 /* MarkdownUtil.swift */, AB7BF2D727AA3F61001865A3 /* UserDefaultsUtil.swift */, @@ -1688,7 +1688,7 @@ ABBC332826BE31AE0084A331 /* EhSettingView.swift in Sources */, AB7BF2C827A968F7001865A3 /* GalleryComment.swift in Sources */, AB706F97278A77E20025A48A /* HistoryStore.swift in Sources */, - AB0929CC2781A0B000F107CA /* HapticClient.swift in Sources */, + AB0929CC2781A0B000F107CA /* HapticsClient.swift in Sources */, ABD4032626B78E5A00001B8C /* GalleryThumbnailCell.swift in Sources */, AB0CFBCD27C1CC67004BD372 /* EhTagTranslationDatabaseModel.swift in Sources */, AB358319269D9996009466A5 /* DomainResolver.swift in Sources */, @@ -1716,7 +1716,7 @@ AB7BF2D427AA3F12001865A3 /* CookiesUtil.swift in Sources */, AB7BF30A27ABDFF1001865A3 /* CoreDataMigrationStep.swift in Sources */, AB69CB8226B3DAF400699359 /* ControlPanel.swift in Sources */, - AB7BF2D227AA3EDC001865A3 /* HapticUtil.swift in Sources */, + AB7BF2D227AA3EDC001865A3 /* HapticsUtil.swift in Sources */, ABD49D5A277C5356003D1A07 /* FavoritesStore.swift in Sources */, AB1EF25427AFA19200F507D6 /* Heap.swift in Sources */, AB7BF2C227A96760001865A3 /* GalleryDetail.swift in Sources */, diff --git a/EhPanda/App/Tools/Clients/HapticClient.swift b/EhPanda/App/Tools/Clients/HapticClient.swift deleted file mode 100644 index 960ec9d5..00000000 --- a/EhPanda/App/Tools/Clients/HapticClient.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// HapticClient.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/02. -// - -import SwiftUI -import ComposableArchitecture - -struct HapticClient { - let generateFeedback: (UIImpactFeedbackGenerator.FeedbackStyle) -> EffectTask - let generateNotificationFeedback: (UINotificationFeedbackGenerator.FeedbackType) -> EffectTask -} - -extension HapticClient { - static let live: Self = .init( - generateFeedback: { style in - .fireAndForget { - HapticUtil.generateFeedback(style: style) - } - }, - generateNotificationFeedback: { style in - .fireAndForget { - HapticUtil.generateNotificationFeedback(style: style) - } - } - ) -} - -// MARK: API -enum HapticClientKey: DependencyKey { - static let liveValue = HapticClient.live - static let testValue = HapticClient.noop - static let previewValue = HapticClient.noop -} - -extension DependencyValues { - var hapticClient: HapticClient { - get { self[HapticClientKey.self] } - set { self[HapticClientKey.self] = newValue } - } -} - -// MARK: Test -#if DEBUG -extension HapticClient { - static let failing: Self = .init( - generateFeedback: { .failing("\(Self.self).generateFeedback(\($0)) is unimplemented") }, - generateNotificationFeedback: { .failing("\(Self.self).generateNotificationFeedback(\($0)) is unimplemented") } - ) -} -#endif -extension HapticClient { - static let noop: Self = .init( - generateFeedback: { _ in .none }, - generateNotificationFeedback: { _ in .none } - ) -} diff --git a/EhPanda/App/Tools/Clients/HapticsClient.swift b/EhPanda/App/Tools/Clients/HapticsClient.swift new file mode 100644 index 00000000..6cb939d5 --- /dev/null +++ b/EhPanda/App/Tools/Clients/HapticsClient.swift @@ -0,0 +1,51 @@ +// +// HapticsClient.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/02. +// + +import SwiftUI +import ComposableArchitecture + +struct HapticsClient { + let generateFeedback: (UIImpactFeedbackGenerator.FeedbackStyle) -> EffectTask + let generateNotificationFeedback: (UINotificationFeedbackGenerator.FeedbackType) -> EffectTask +} + +extension HapticsClient { + static let live: Self = .init( + generateFeedback: { style in + .fireAndForget { + HapticsUtil.generateFeedback(style: style) + } + }, + generateNotificationFeedback: { style in + .fireAndForget { + HapticsUtil.generateNotificationFeedback(style: style) + } + } + ) +} + +// MARK: API +enum HapticsClientKey: DependencyKey { + static let liveValue = HapticsClient.live + static let testValue = HapticsClient.noop + static let previewValue = HapticsClient.noop +} + +extension DependencyValues { + var hapticsClient: HapticsClient { + get { self[HapticsClientKey.self] } + set { self[HapticsClientKey.self] = newValue } + } +} + +// MARK: Test +extension HapticsClient { + static let noop: Self = .init( + generateFeedback: { _ in .none }, + generateNotificationFeedback: { _ in .none } + ) +} diff --git a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift index 72fd2920..732d915a 100644 --- a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift +++ b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift @@ -39,11 +39,11 @@ extension Reducer { func haptics( unwrapping enum: @escaping (State) -> Enum?, case casePath: CasePath, - hapticClient: @escaping (Environment) -> HapticClient, + hapticsClient: @escaping (Environment) -> HapticsClient, style: UIImpactFeedbackGenerator.FeedbackStyle = .light ) -> Self { onBecomeNonNil(unwrapping: `enum`, case: casePath) { - hapticClient($2).generateFeedback(style).fireAndForget() + hapticsClient($2).generateFeedback(style).fireAndForget() } } } @@ -52,7 +52,7 @@ extension ReducerProtocol { func haptics( unwrapping enum: @escaping (State) -> Enum?, case casePath: CasePath, - hapticsClient: HapticClient, + hapticsClient: HapticsClient, style: UIImpactFeedbackGenerator.FeedbackStyle = .light ) -> some ReducerProtocol { onBecomeNonNil(unwrapping: `enum`, case: casePath) { _, _ in diff --git a/EhPanda/App/Tools/Utilities/HapticUtil.swift b/EhPanda/App/Tools/Utilities/HapticsUtil.swift similarity index 97% rename from EhPanda/App/Tools/Utilities/HapticUtil.swift rename to EhPanda/App/Tools/Utilities/HapticsUtil.swift index 3cf825eb..e436b136 100644 --- a/EhPanda/App/Tools/Utilities/HapticUtil.swift +++ b/EhPanda/App/Tools/Utilities/HapticsUtil.swift @@ -1,5 +1,5 @@ // -// HapticUtil.swift +// HapticsUtil.swift // EhPanda // // Created by 荒木辰造 on R 4/02/02. @@ -8,7 +8,7 @@ import SwiftUI import AudioToolbox -struct HapticUtil { +struct HapticsUtil { static func generateFeedback(style: UIImpactFeedbackGenerator.FeedbackStyle) { guard !isLegacyTapticEngine else { generateLegacyFeedback() diff --git a/EhPanda/DataFlow/AppDelegateStore.swift b/EhPanda/DataFlow/AppDelegateStore.swift index d0f4e2bd..1a756170 100644 --- a/EhPanda/DataFlow/AppDelegateStore.swift +++ b/EhPanda/DataFlow/AppDelegateStore.swift @@ -21,7 +21,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { imageClient: .live, deviceClient: .live, loggerClient: .live, - hapticClient: .live, + hapticsClient: .live, libraryClient: .live, cookiesClient: .live, databaseClient: .live, diff --git a/EhPanda/DataFlow/AppRouteStore.swift b/EhPanda/DataFlow/AppRouteStore.swift index 38ea7ef2..ccaeace3 100644 --- a/EhPanda/DataFlow/AppRouteStore.swift +++ b/EhPanda/DataFlow/AppRouteStore.swift @@ -53,7 +53,7 @@ struct AppRouteEnvironment { let imageClient: ImageClient let deviceClient: DeviceClient let loggerClient: LoggerClient - let hapticClient: HapticClient + let hapticsClient: HapticsClient let libraryClient: LibraryClient let cookiesClient: CookiesClient let databaseClient: DatabaseClient @@ -174,12 +174,12 @@ let appRouteReducer = Reducer { state, actio case .tabBar(.setTabBarItemType(let type)): var effects = [EffectTask]() - let hapticEffect: EffectTask = environment.hapticClient + let hapticEffect: EffectTask = environment.hapticsClient .generateFeedback(.soft).fireAndForget() if type == state.tabBarState.tabBarItemType { switch type { @@ -157,7 +157,7 @@ let appReducerCore = Reducer { state, actio case .home(.watched(.onNotLoginViewButtonTapped)), .favorites(.onNotLoginViewButtonTapped): var effects: [EffectTask] = [ - environment.hapticClient.generateFeedback(.soft).fireAndForget(), + environment.hapticsClient.generateFeedback(.soft).fireAndForget(), .init(value: .tabBar(.setTabBarItemType(.setting))) ] effects.append(.init(value: .setting(.setNavigation(.account)))) @@ -217,7 +217,7 @@ let appReducer = Reducer.combine( imageClient: $0.imageClient, deviceClient: $0.deviceClient, loggerClient: $0.loggerClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, libraryClient: $0.libraryClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, @@ -268,7 +268,7 @@ let appReducer = Reducer.combine( fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, libraryClient: $0.libraryClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, @@ -287,7 +287,7 @@ let appReducer = Reducer.combine( fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, @@ -306,7 +306,7 @@ let appReducer = Reducer.combine( fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, @@ -324,7 +324,7 @@ let appReducer = Reducer.combine( fileClient: $0.fileClient, deviceClient: $0.deviceClient, loggerClient: $0.loggerClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, libraryClient: $0.libraryClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, diff --git a/EhPanda/View/Detail/ArchivesView.swift b/EhPanda/View/Detail/ArchivesView.swift index 3ded93ac..9d277697 100644 --- a/EhPanda/View/Detail/ArchivesView.swift +++ b/EhPanda/View/Detail/ArchivesView.swift @@ -98,7 +98,7 @@ private struct HathArchivesView: View { Button { if archive.isValid { selection = archive - HapticUtil.generateFeedback(style: .soft) + HapticsUtil.generateFeedback(style: .soft) } } label: { HathArchiveGrid(isSelected: selection == archive, archive: archive) @@ -225,7 +225,7 @@ struct ArchivesView_Previews: PreviewProvider { initialState: .init(), reducer: archivesReducer, environment: ArchivesEnvironment( - hapticClient: .live, + hapticsClient: .live, cookiesClient: .live, databaseClient: .live ) diff --git a/EhPanda/View/Detail/CommentsView.swift b/EhPanda/View/Detail/CommentsView.swift index e437e1e1..89c4b303 100644 --- a/EhPanda/View/Detail/CommentsView.swift +++ b/EhPanda/View/Detail/CommentsView.swift @@ -280,7 +280,7 @@ struct CommentsView_Previews: PreviewProvider { fileClient: .live, imageClient: .live, deviceClient: .live, - hapticClient: .live, + hapticsClient: .live, cookiesClient: .live, databaseClient: .live, clipboardClient: .live, diff --git a/EhPanda/View/Detail/DataFlow/ArchivesStore.swift b/EhPanda/View/Detail/DataFlow/ArchivesStore.swift index 0933e5d4..7f1514de 100644 --- a/EhPanda/View/Detail/DataFlow/ArchivesStore.swift +++ b/EhPanda/View/Detail/DataFlow/ArchivesStore.swift @@ -44,7 +44,7 @@ enum ArchivesAction: BindableAction { } struct ArchivesEnvironment { - let hapticClient: HapticClient + let hapticsClient: HapticsClient let cookiesClient: CookiesClient let databaseClient: DatabaseClient } @@ -133,7 +133,7 @@ let archivesReducer = Reducer.recurs case .toggleShowFullTitle: state.showsFullTitle.toggle() - return environment.hapticClient.generateFeedback(.soft).fireAndForget() + return environment.hapticsClient.generateFeedback(.soft).fireAndForget() case .toggleShowUserRating: state.showsUserRating.toggle() - return environment.hapticClient.generateFeedback(.soft).fireAndForget() + return environment.hapticsClient.generateFeedback(.soft).fireAndForget() case .setCommentContent(let content): state.commentContent = content @@ -208,7 +208,7 @@ let detailReducer = Reducer.recurs state.updateRating(value: value) return .merge( .init(value: .rateGallery), - environment.hapticClient.generateFeedback(.soft).fireAndForget(), + environment.hapticsClient.generateFeedback(.soft).fireAndForget(), .init(value: .confirmRatingDone).delay(for: 1, scheduler: DispatchQueue.main).eraseToEffect() ) @@ -340,10 +340,10 @@ let detailReducer = Reducer.recurs if case .success = result { return .merge( .init(value: .fetchGalleryDetail), - environment.hapticClient.generateNotificationFeedback(.success).fireAndForget() + environment.hapticsClient.generateNotificationFeedback(.success).fireAndForget() ) } - return environment.hapticClient.generateNotificationFeedback(.error).fireAndForget() + return environment.hapticsClient.generateNotificationFeedback(.error).fireAndForget() case .reading(.onPerformDismiss): return .init(value: .setNavigation(nil)) @@ -386,38 +386,38 @@ let detailReducer = Reducer.recurs .haptics( unwrapping: \.route, case: /DetailState.Route.detailSearch, - hapticClient: \.hapticClient, + hapticsClient: \.hapticsClient, style: .soft ) .haptics( unwrapping: \.route, case: /DetailState.Route.postComment, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .haptics( unwrapping: \.route, case: /DetailState.Route.tagDetail, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .haptics( unwrapping: \.route, case: /DetailState.Route.torrents, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .haptics( unwrapping: \.route, case: /DetailState.Route.archives, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .haptics( unwrapping: \.route, case: /DetailState.Route.reading, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .haptics( unwrapping: \.route, case: /DetailState.Route.share, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .binding(), readingReducer.pullback( @@ -428,7 +428,7 @@ let detailReducer = Reducer.recurs urlClient: $0.urlClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, @@ -441,7 +441,7 @@ let detailReducer = Reducer.recurs action: /DetailAction.archives, environment: { .init( - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient ) @@ -453,7 +453,7 @@ let detailReducer = Reducer.recurs environment: { .init( fileClient: $0.fileClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, clipboardClient: $0.clipboardClient ) } @@ -466,7 +466,7 @@ let detailReducer = Reducer.recurs urlClient: $0.urlClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, @@ -483,7 +483,7 @@ let detailReducer = Reducer.recurs fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, @@ -501,7 +501,7 @@ let detailReducer = Reducer.recurs fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, @@ -515,7 +515,7 @@ let detailReducer = Reducer.recurs action: /DetailAction.galleryInfos, environment: { .init( - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, clipboardClient: $0.clipboardClient ) } diff --git a/EhPanda/View/Detail/DataFlow/GalleryInfosStore.swift b/EhPanda/View/Detail/DataFlow/GalleryInfosStore.swift index bd0022ff..9f75496a 100644 --- a/EhPanda/View/Detail/DataFlow/GalleryInfosStore.swift +++ b/EhPanda/View/Detail/DataFlow/GalleryInfosStore.swift @@ -23,7 +23,7 @@ enum GalleryInfosAction: BindableAction { } struct GalleryInfosEnvironment { - let hapticClient: HapticClient + let hapticsClient: HapticsClient let clipboardClient: ClipboardClient } @@ -37,7 +37,7 @@ let galleryInfosReducer = Reducer.co fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, diff --git a/EhPanda/View/Home/DataFlow/HomeStore.swift b/EhPanda/View/Home/DataFlow/HomeStore.swift index c42f41ad..925ba623 100644 --- a/EhPanda/View/Home/DataFlow/HomeStore.swift +++ b/EhPanda/View/Home/DataFlow/HomeStore.swift @@ -93,7 +93,7 @@ struct HomeEnvironment { let fileClient: FileClient let imageClient: ImageClient let deviceClient: DeviceClient - let hapticClient: HapticClient + let hapticsClient: HapticsClient let libraryClient: LibraryClient let cookiesClient: CookiesClient let databaseClient: DatabaseClient @@ -265,7 +265,7 @@ let homeReducer = Reducer.combine( fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, @@ -283,7 +283,7 @@ let homeReducer = Reducer.combine( fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, @@ -301,7 +301,7 @@ let homeReducer = Reducer.combine( fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, @@ -319,7 +319,7 @@ let homeReducer = Reducer.combine( fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, @@ -337,7 +337,7 @@ let homeReducer = Reducer.combine( fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, @@ -355,7 +355,7 @@ let homeReducer = Reducer.combine( fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, diff --git a/EhPanda/View/Home/DataFlow/PopularStore.swift b/EhPanda/View/Home/DataFlow/PopularStore.swift index b9a72108..1e529e55 100644 --- a/EhPanda/View/Home/DataFlow/PopularStore.swift +++ b/EhPanda/View/Home/DataFlow/PopularStore.swift @@ -52,7 +52,7 @@ struct PopularEnvironment { let fileClient: FileClient let imageClient: ImageClient let deviceClient: DeviceClient - let hapticClient: HapticClient + let hapticsClient: HapticsClient let cookiesClient: CookiesClient let databaseClient: DatabaseClient let clipboardClient: ClipboardClient @@ -113,7 +113,7 @@ let popularReducer = Reducer.co .haptics( unwrapping: \.route, case: /PopularState.Route.filters, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .binding(), filtersReducer.pullback( @@ -134,7 +134,7 @@ let popularReducer = Reducer.co fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, diff --git a/EhPanda/View/Home/DataFlow/ToplistsStore.swift b/EhPanda/View/Home/DataFlow/ToplistsStore.swift index 961c93bb..4676d465 100644 --- a/EhPanda/View/Home/DataFlow/ToplistsStore.swift +++ b/EhPanda/View/Home/DataFlow/ToplistsStore.swift @@ -85,7 +85,7 @@ struct ToplistsEnvironment { let fileClient: FileClient let imageClient: ImageClient let deviceClient: DeviceClient - let hapticClient: HapticClient + let hapticsClient: HapticsClient let cookiesClient: CookiesClient let databaseClient: DatabaseClient let clipboardClient: ClipboardClient @@ -125,13 +125,13 @@ let toplistsReducer = Reducer 0, index <= pageNumber.maximum + 1 else { - return environment.hapticClient.generateNotificationFeedback(.error).fireAndForget() + return environment.hapticsClient.generateNotificationFeedback(.error).fireAndForget() } return .init(value: .fetchGalleries(index - 1)) case .presentJumpPageAlert: state.jumpPageAlertPresented = true - return environment.hapticClient.generateFeedback(.light).fireAndForget() + return environment.hapticsClient.generateFeedback(.light).fireAndForget() case .setJumpPageAlertFocused(let isFocused): state.jumpPageAlertFocused = isFocused @@ -217,7 +217,7 @@ let toplistsReducer = Reducer.co .haptics( unwrapping: \.route, case: /WatchedState.Route.quickSearch, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .haptics( unwrapping: \.route, case: /WatchedState.Route.filters, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .binding(), filtersReducer.pullback( @@ -210,7 +210,7 @@ let watchedReducer = Reducer.co fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, diff --git a/EhPanda/View/Home/FrontpageView.swift b/EhPanda/View/Home/FrontpageView.swift index dc166a85..fa4b7041 100644 --- a/EhPanda/View/Home/FrontpageView.swift +++ b/EhPanda/View/Home/FrontpageView.swift @@ -106,7 +106,7 @@ struct FrontpageView_Previews: PreviewProvider { fileClient: .live, imageClient: .live, deviceClient: .live, - hapticClient: .live, + hapticsClient: .live, cookiesClient: .live, databaseClient: .live, clipboardClient: .live, diff --git a/EhPanda/View/Home/HistoryView.swift b/EhPanda/View/Home/HistoryView.swift index 58d7523a..8a4c0f7a 100644 --- a/EhPanda/View/Home/HistoryView.swift +++ b/EhPanda/View/Home/HistoryView.swift @@ -112,7 +112,7 @@ struct HistoryView_Previews: PreviewProvider { fileClient: .live, imageClient: .live, deviceClient: .live, - hapticClient: .live, + hapticsClient: .live, cookiesClient: .live, databaseClient: .live, clipboardClient: .live, diff --git a/EhPanda/View/Home/HomeView.swift b/EhPanda/View/Home/HomeView.swift index 42e5f41d..11b2870a 100644 --- a/EhPanda/View/Home/HomeView.swift +++ b/EhPanda/View/Home/HomeView.swift @@ -527,7 +527,7 @@ struct HomeView_Previews: PreviewProvider { fileClient: .live, imageClient: .live, deviceClient: .live, - hapticClient: .live, + hapticsClient: .live, libraryClient: .live, cookiesClient: .live, databaseClient: .live, diff --git a/EhPanda/View/Home/PopularView.swift b/EhPanda/View/Home/PopularView.swift index 566da7c3..ead30687 100644 --- a/EhPanda/View/Home/PopularView.swift +++ b/EhPanda/View/Home/PopularView.swift @@ -103,7 +103,7 @@ struct PopularView_Previews: PreviewProvider { fileClient: .live, imageClient: .live, deviceClient: .live, - hapticClient: .live, + hapticsClient: .live, cookiesClient: .live, databaseClient: .live, clipboardClient: .live, diff --git a/EhPanda/View/Home/ToplistsView.swift b/EhPanda/View/Home/ToplistsView.swift index 6800f36e..7203995b 100644 --- a/EhPanda/View/Home/ToplistsView.swift +++ b/EhPanda/View/Home/ToplistsView.swift @@ -161,7 +161,7 @@ struct ToplistsView_Previews: PreviewProvider { fileClient: .live, imageClient: .live, deviceClient: .live, - hapticClient: .live, + hapticsClient: .live, cookiesClient: .live, databaseClient: .live, clipboardClient: .live, diff --git a/EhPanda/View/Home/WatchedView.swift b/EhPanda/View/Home/WatchedView.swift index d486de2e..cfe0b44c 100644 --- a/EhPanda/View/Home/WatchedView.swift +++ b/EhPanda/View/Home/WatchedView.swift @@ -134,7 +134,7 @@ struct WatchedView_Previews: PreviewProvider { fileClient: .live, imageClient: .live, deviceClient: .live, - hapticClient: .live, + hapticsClient: .live, cookiesClient: .live, databaseClient: .live, clipboardClient: .live, diff --git a/EhPanda/View/Reading/ReadingStore.swift b/EhPanda/View/Reading/ReadingStore.swift index 6a67760c..f2f2a4e9 100644 --- a/EhPanda/View/Reading/ReadingStore.swift +++ b/EhPanda/View/Reading/ReadingStore.swift @@ -168,7 +168,7 @@ struct ReadingEnvironment { let urlClient: URLClient let imageClient: ImageClient let deviceClient: DeviceClient - let hapticClient: HapticClient + let hapticsClient: HapticsClient let cookiesClient: CookiesClient let databaseClient: DatabaseClient let clipboardClient: ClipboardClient @@ -178,7 +178,7 @@ struct ReadingEnvironment { let readingReducer = Reducer { state, action, environment in switch action { case .binding(\.$showsSliderPreview): - return environment.hapticClient.generateFeedback(.soft).fireAndForget() + return environment.hapticsClient.generateFeedback(.soft).fireAndForget() case .binding: return .none @@ -202,7 +202,7 @@ let readingReducer = Reducer { return .merge(effects) case .onPerformDismiss: - return environment.hapticClient.generateFeedback(.light).fireAndForget() + return environment.hapticsClient.generateFeedback(.light).fireAndForget() case .onAppear(let gid, let enablesLandscape): var effects: [EffectTask] = [ @@ -577,11 +577,11 @@ let readingReducer = Reducer { .haptics( unwrapping: \.route, case: /ReadingState.Route.readingSetting, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .haptics( unwrapping: \.route, case: /ReadingState.Route.share, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .binding() diff --git a/EhPanda/View/Reading/ReadingView.swift b/EhPanda/View/Reading/ReadingView.swift index ae124cb2..f11f16d2 100644 --- a/EhPanda/View/Reading/ReadingView.swift +++ b/EhPanda/View/Reading/ReadingView.swift @@ -605,7 +605,7 @@ struct ReadingView_Previews: PreviewProvider { urlClient: .live, imageClient: .live, deviceClient: .live, - hapticClient: .live, + hapticsClient: .live, cookiesClient: .live, databaseClient: .live, clipboardClient: .live, diff --git a/EhPanda/View/Search/SearchRootStore.swift b/EhPanda/View/Search/SearchRootStore.swift index 3d65e8d1..646d4b68 100644 --- a/EhPanda/View/Search/SearchRootStore.swift +++ b/EhPanda/View/Search/SearchRootStore.swift @@ -86,7 +86,7 @@ struct SearchRootEnvironment { let fileClient: FileClient let imageClient: ImageClient let deviceClient: DeviceClient - let hapticClient: HapticClient + let hapticsClient: HapticsClient let cookiesClient: CookiesClient let databaseClient: DatabaseClient let clipboardClient: ClipboardClient @@ -183,12 +183,12 @@ let searchRootReducer = Reducer.combin .haptics( unwrapping: \.route, case: /SearchState.Route.quickSearch, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .haptics( unwrapping: \.route, case: /SearchState.Route.filters, - hapticClient: \.hapticClient + hapticsClient: \.hapticsClient ) .binding(), filtersReducer.pullback( @@ -215,7 +215,7 @@ let searchReducer = Reducer.combin fileClient: $0.fileClient, imageClient: $0.imageClient, deviceClient: $0.deviceClient, - hapticClient: $0.hapticClient, + hapticsClient: $0.hapticsClient, cookiesClient: $0.cookiesClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, diff --git a/EhPanda/View/Search/SearchView.swift b/EhPanda/View/Search/SearchView.swift index ceca4976..b6ff2292 100644 --- a/EhPanda/View/Search/SearchView.swift +++ b/EhPanda/View/Search/SearchView.swift @@ -129,7 +129,7 @@ struct SearchView_Previews: PreviewProvider { fileClient: .live, imageClient: .live, deviceClient: .live, - hapticClient: .live, + hapticsClient: .live, cookiesClient: .live, databaseClient: .live, clipboardClient: .live, diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift index 7511f7f2..2635ab4e 100644 --- a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift @@ -24,7 +24,7 @@ struct AccountSettingReducer: ReducerProtocol { @BindingState var exCookiesState: CookiesState = .empty(.exhentai) var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded - var loginState = LoginState() + var loginState = LoginReducer.State() var ehSettingState = EhSettingState() } @@ -37,13 +37,13 @@ struct AccountSettingReducer: ReducerProtocol { case loadCookies case copyCookies(GalleryHost) - case login(LoginAction) + case login(LoginReducer.Action) case ehSetting(EhSettingAction) } @Dependency(\.clipboardClient) private var clipboardClient @Dependency(\.cookiesClient) private var cookiesClient - @Dependency(\.hapticClient) private var hapticClient + @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { Reduce { state, action in @@ -85,7 +85,7 @@ struct AccountSettingReducer: ReducerProtocol { return .merge( .init(value: .setNavigation(.hud)), clipboardClient.saveText(cookiesDescription).fireAndForget(), - hapticClient.generateNotificationFeedback(.success).fireAndForget() + hapticsClient.generateNotificationFeedback(.success).fireAndForget() ) case .login(.loginDone): @@ -101,7 +101,7 @@ struct AccountSettingReducer: ReducerProtocol { .haptics( unwrapping: \.route, case: /Route.webView, - hapticsClient: hapticClient + hapticsClient: hapticsClient ) // // TODO: Child reducers diff --git a/EhPanda/View/Setting/DataFlow/EhSettingStore.swift b/EhPanda/View/Setting/DataFlow/EhSettingStore.swift index 0e1216c4..5e759d52 100644 --- a/EhPanda/View/Setting/DataFlow/EhSettingStore.swift +++ b/EhPanda/View/Setting/DataFlow/EhSettingStore.swift @@ -49,7 +49,7 @@ enum EhSettingAction: BindableAction, Equatable { } struct EhSettingEnvironment { - let hapticClient: HapticClient + let hapticsClient: HapticsClient let cookiesClient: CookiesClient let uiApplicationClient: UIApplicationClient } @@ -133,6 +133,6 @@ let ehSettingReducer = Reducer.co case .binding(\.$setting.bypassesSNIFiltering): return .merge( .init(value: .syncSetting), - environment.hapticClient.generateFeedback(.soft).fireAndForget(), + environment.hapticsClient.generateFeedback(.soft).fireAndForget(), environment.dfClient.setActive(state.setting.bypassesSNIFiltering).fireAndForget() ) @@ -460,7 +460,7 @@ let settingReducer = Reducer.co // action: /SettingAction.account, // environment: { // .init( -// hapticClient: $0.hapticClient, +// hapticsClient: $0.hapticsClient, // cookiesClient: $0.cookiesClient, // clipboardClient: $0.clipboardClient, // uiApplicationClient: $0.uiApplicationClient diff --git a/EhPanda/View/Setting/SettingView.swift b/EhPanda/View/Setting/SettingView.swift index afeda7bf..0bd5665d 100644 --- a/EhPanda/View/Setting/SettingView.swift +++ b/EhPanda/View/Setting/SettingView.swift @@ -191,7 +191,7 @@ struct SettingView_Previews: PreviewProvider { fileClient: .live, deviceClient: .live, loggerClient: .live, - hapticClient: .live, + hapticsClient: .live, libraryClient: .live, cookiesClient: .live, databaseClient: .live, diff --git a/EhPanda/View/Setting/Support/EhSettingView.swift b/EhPanda/View/Setting/Support/EhSettingView.swift index 7a17afce..d35d7408 100644 --- a/EhPanda/View/Setting/Support/EhSettingView.swift +++ b/EhPanda/View/Setting/Support/EhSettingView.swift @@ -536,7 +536,7 @@ private struct ExcludeView: View { .opacity(isExcluded.wrappedValue ? 1 : 0) } .onTapGesture { - HapticUtil.generateFeedback(style: .soft) + HapticsUtil.generateFeedback(style: .soft) withAnimation { isExcluded.wrappedValue.toggle() } } } @@ -714,7 +714,7 @@ private struct ExcludeToggle: View { } .onTapGesture { withAnimation { isOn.toggle() } - HapticUtil.generateFeedback(style: .soft) + HapticsUtil.generateFeedback(style: .soft) } } } @@ -1089,7 +1089,7 @@ struct EhSettingView_Previews: PreviewProvider { initialState: .init(ehSetting: .empty, ehProfile: .empty, loadingState: .idle), reducer: ehSettingReducer, environment: EhSettingEnvironment( - hapticClient: .live, + hapticsClient: .live, cookiesClient: .live, uiApplicationClient: .live ) diff --git a/EhPanda/View/Support/Components/CategoryView.swift b/EhPanda/View/Support/Components/CategoryView.swift index 33a77ea4..1c4df40a 100644 --- a/EhPanda/View/Support/Components/CategoryView.swift +++ b/EhPanda/View/Support/Components/CategoryView.swift @@ -84,7 +84,7 @@ private struct CategoryCell: View { } .onTapGesture { isFiltered.toggle() - HapticUtil.generateFeedback(style: .soft) + HapticsUtil.generateFeedback(style: .soft) } .cornerRadius(5) } diff --git a/EhPanda/View/Support/Components/SubSection.swift b/EhPanda/View/Support/Components/SubSection.swift index 9b63deaa..674046f9 100644 --- a/EhPanda/View/Support/Components/SubSection.swift +++ b/EhPanda/View/Support/Components/SubSection.swift @@ -37,7 +37,7 @@ struct SubSection: View { HStack { Button { reloadAction?() - HapticUtil.generateFeedback(style: .soft) + HapticsUtil.generateFeedback(style: .soft) } label: { HStack(spacing: 10) { Text(title).font(.title3.bold()) diff --git a/EhPanda/View/TabBar/TabBarView.swift b/EhPanda/View/TabBar/TabBarView.swift index 82cfff25..32cbdaef 100644 --- a/EhPanda/View/TabBar/TabBarView.swift +++ b/EhPanda/View/TabBar/TabBarView.swift @@ -163,7 +163,7 @@ struct TabBarView_Previews: PreviewProvider { imageClient: .live, deviceClient: .live, loggerClient: .live, - hapticClient: .live, + hapticsClient: .live, libraryClient: .live, cookiesClient: .live, databaseClient: .live, diff --git a/EhPandaTests/Tests/Store/Setting/AccountSettingStoreTests.swift b/EhPandaTests/Tests/Store/Setting/AccountSettingStoreTests.swift index 6e9fb34c..07aca46c 100644 --- a/EhPandaTests/Tests/Store/Setting/AccountSettingStoreTests.swift +++ b/EhPandaTests/Tests/Store/Setting/AccountSettingStoreTests.swift @@ -12,7 +12,7 @@ import ComposableArchitecture class AccountSettingStoreTests: XCTestCase { private var noopEnvironment: AccountSettingEnvironment { .init( - hapticClient: .noop, + hapticsClient: .noop, cookiesClient: .noop, clipboardClient: .noop, uiApplicationClient: .noop @@ -68,7 +68,7 @@ class AccountSettingStoreTests: XCTestCase { ), reducer: accountSettingReducer, environment: AccountSettingEnvironment( - hapticClient: .noop, + hapticsClient: .noop, cookiesClient: .live, clipboardClient: .noop, uiApplicationClient: .noop @@ -134,7 +134,7 @@ class AccountSettingStoreTests: XCTestCase { ), reducer: accountSettingReducer, environment: AccountSettingEnvironment( - hapticClient: .noop, + hapticsClient: .noop, cookiesClient: .live, clipboardClient: .noop, uiApplicationClient: .noop From e7742b44957efb135a4ea51f69345c71f9597285 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Tue, 9 May 2023 22:52:46 +0800 Subject: [PATCH 09/29] Rename cookies to cookie --- EhPanda.xcodeproj/project.pbxproj | 16 +++++----- ...CookiesClient.swift => CookieClient.swift} | 32 +++++++++---------- .../{CookiesUtil.swift => CookieUtil.swift} | 10 +++--- EhPanda/DataFlow/AppDelegateStore.swift | 10 +++--- EhPanda/DataFlow/AppRouteStore.swift | 4 +-- EhPanda/DataFlow/AppStore.swift | 18 +++++------ EhPanda/View/Detail/ArchivesView.swift | 2 +- EhPanda/View/Detail/CommentsView.swift | 4 +-- .../View/Detail/DataFlow/ArchivesStore.swift | 4 +-- .../View/Detail/DataFlow/CommentsStore.swift | 4 +-- .../Detail/DataFlow/DetailSearchStore.swift | 2 +- .../View/Detail/DataFlow/DetailStore.swift | 16 +++++----- .../View/Detail/DataFlow/PreviewsStore.swift | 4 +-- EhPanda/View/Detail/DetailSearchView.swift | 2 +- EhPanda/View/Detail/DetailView.swift | 12 +++---- EhPanda/View/Detail/PreviewsView.swift | 2 +- EhPanda/View/Favorites/FavoritesStore.swift | 4 +-- EhPanda/View/Favorites/FavoritesView.swift | 6 ++-- .../View/Home/DataFlow/FrontpageStore.swift | 4 +-- EhPanda/View/Home/DataFlow/HistoryStore.swift | 4 +-- EhPanda/View/Home/DataFlow/HomeStore.swift | 14 ++++---- EhPanda/View/Home/DataFlow/PopularStore.swift | 4 +-- .../View/Home/DataFlow/ToplistsStore.swift | 4 +-- EhPanda/View/Home/DataFlow/WatchedStore.swift | 4 +-- EhPanda/View/Home/FrontpageView.swift | 2 +- EhPanda/View/Home/HistoryView.swift | 2 +- EhPanda/View/Home/HomeView.swift | 2 +- EhPanda/View/Home/PopularView.swift | 2 +- EhPanda/View/Home/ToplistsView.swift | 2 +- EhPanda/View/Home/WatchedView.swift | 6 ++-- EhPanda/View/Reading/ReadingStore.swift | 4 +-- EhPanda/View/Reading/ReadingView.swift | 2 +- EhPanda/View/Search/SearchRootStore.swift | 6 ++-- EhPanda/View/Search/SearchRootView.swift | 2 +- EhPanda/View/Search/SearchStore.swift | 4 +-- EhPanda/View/Search/SearchView.swift | 2 +- .../AccountSettingReducer.swift | 14 ++++---- .../AccountSetting/AccountSettingView.swift | 2 +- .../Setting/DataFlow/EhSettingStore.swift | 4 +-- .../View/Setting/DataFlow/SettingStore.swift | 32 +++++++++---------- EhPanda/View/Setting/SettingView.swift | 2 +- .../View/Setting/Support/EhSettingView.swift | 2 +- EhPanda/View/TabBar/TabBarView.swift | 2 +- .../Setting/AccountSettingStoreTests.swift | 12 +++---- 44 files changed, 146 insertions(+), 146 deletions(-) rename EhPanda/App/Tools/Clients/{CookiesClient.swift => CookieClient.swift} (95%) rename EhPanda/App/Tools/Utilities/{CookiesUtil.swift => CookieUtil.swift} (83%) diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 427a1b74..e1304e4e 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -12,7 +12,7 @@ AB0929C027805A8200F107CA /* LoginStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929BF27805A8200F107CA /* LoginStore.swift */; }; AB0929C6278160AE00F107CA /* LibraryClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929C5278160AE00F107CA /* LibraryClient.swift */; }; AB0929C82781938A00F107CA /* DFClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929C72781938A00F107CA /* DFClient.swift */; }; - AB0929CA278196ED00F107CA /* CookiesClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929C9278196ED00F107CA /* CookiesClient.swift */; }; + AB0929CA278196ED00F107CA /* CookieClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929C9278196ED00F107CA /* CookieClient.swift */; }; AB0929CC2781A0B000F107CA /* HapticsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929CB2781A0B000F107CA /* HapticsClient.swift */; }; AB0929CE2781AADA00F107CA /* DatabaseClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929CD2781AADA00F107CA /* DatabaseClient.swift */; }; AB0929D02781E1CC00F107CA /* UIApplicationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929CF2781E1CC00F107CA /* UIApplicationClient.swift */; }; @@ -153,7 +153,7 @@ AB7BF2CE27AA3E58001865A3 /* AppUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2CD27AA3E58001865A3 /* AppUtil.swift */; }; AB7BF2D027AA3E75001865A3 /* DeviceUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2CF27AA3E75001865A3 /* DeviceUtil.swift */; }; AB7BF2D227AA3EDC001865A3 /* HapticsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D127AA3EDC001865A3 /* HapticsUtil.swift */; }; - AB7BF2D427AA3F12001865A3 /* CookiesUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D327AA3F12001865A3 /* CookiesUtil.swift */; }; + AB7BF2D427AA3F12001865A3 /* CookieUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D327AA3F12001865A3 /* CookieUtil.swift */; }; AB7BF2D627AA3F4C001865A3 /* FileUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D527AA3F4C001865A3 /* FileUtil.swift */; }; AB7BF2D827AA3F61001865A3 /* UserDefaultsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D727AA3F61001865A3 /* UserDefaultsUtil.swift */; }; AB7BF2DA27AA78CF001865A3 /* Reducer_Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D927AA78CF001865A3 /* Reducer_Extension.swift */; }; @@ -314,7 +314,7 @@ AB0929BF27805A8200F107CA /* LoginStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginStore.swift; sourceTree = ""; }; AB0929C5278160AE00F107CA /* LibraryClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryClient.swift; sourceTree = ""; }; AB0929C72781938A00F107CA /* DFClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DFClient.swift; sourceTree = ""; }; - AB0929C9278196ED00F107CA /* CookiesClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiesClient.swift; sourceTree = ""; }; + AB0929C9278196ED00F107CA /* CookieClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieClient.swift; sourceTree = ""; }; AB0929CB2781A0B000F107CA /* HapticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticsClient.swift; sourceTree = ""; }; AB0929CD2781AADA00F107CA /* DatabaseClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseClient.swift; sourceTree = ""; }; AB0929CF2781E1CC00F107CA /* UIApplicationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationClient.swift; sourceTree = ""; }; @@ -453,7 +453,7 @@ AB7BF2CD27AA3E58001865A3 /* AppUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUtil.swift; sourceTree = ""; }; AB7BF2CF27AA3E75001865A3 /* DeviceUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceUtil.swift; sourceTree = ""; }; AB7BF2D127AA3EDC001865A3 /* HapticsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticsUtil.swift; sourceTree = ""; }; - AB7BF2D327AA3F12001865A3 /* CookiesUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiesUtil.swift; sourceTree = ""; }; + AB7BF2D327AA3F12001865A3 /* CookieUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieUtil.swift; sourceTree = ""; }; AB7BF2D527AA3F4C001865A3 /* FileUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtil.swift; sourceTree = ""; }; AB7BF2D727AA3F61001865A3 /* UserDefaultsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsUtil.swift; sourceTree = ""; }; AB7BF2D927AA78CF001865A3 /* Reducer_Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reducer_Extension.swift; sourceTree = ""; }; @@ -654,7 +654,7 @@ AB0929D12781E7D500F107CA /* LoggerClient.swift */, AB0929CB2781A0B000F107CA /* HapticsClient.swift */, AB0929C5278160AE00F107CA /* LibraryClient.swift */, - AB0929C9278196ED00F107CA /* CookiesClient.swift */, + AB0929C9278196ED00F107CA /* CookieClient.swift */, AB0929CD2781AADA00F107CA /* DatabaseClient.swift */, ABBB266B2797E882007B6149 /* ClipboardClient.swift */, AB706F8F278A5F680025A48A /* AppDelegateClient.swift */, @@ -1251,7 +1251,7 @@ AB7BF2D527AA3F4C001865A3 /* FileUtil.swift */, AB7BF2CF27AA3E75001865A3 /* DeviceUtil.swift */, AB7BF2D127AA3EDC001865A3 /* HapticsUtil.swift */, - AB7BF2D327AA3F12001865A3 /* CookiesUtil.swift */, + AB7BF2D327AA3F12001865A3 /* CookieUtil.swift */, AB0CFBD427C24B3B004BD372 /* MarkdownUtil.swift */, AB7BF2D727AA3F61001865A3 /* UserDefaultsUtil.swift */, ); @@ -1681,7 +1681,7 @@ AB63EADB2699AC8200090535 /* AppEnvMO+CoreDataProperties.swift in Sources */, AB7BF2FD27ABCAD4001865A3 /* MigrationStore.swift in Sources */, AB7BF2D827AA3F61001865A3 /* UserDefaultsUtil.swift in Sources */, - AB0929CA278196ED00F107CA /* CookiesClient.swift in Sources */, + AB0929CA278196ED00F107CA /* CookieClient.swift in Sources */, AB7BF2FB27ABCA3A001865A3 /* MigrationView.swift in Sources */, AB7BF31C27ABE028001865A3 /* NSManagedObjectModel+Compatible.swift in Sources */, AB7BF31B27ABE028001865A3 /* NSManagedObjectModel+Resource.swift in Sources */, @@ -1713,7 +1713,7 @@ ABF45AF625F3313D00ECB568 /* AppearanceSettingView.swift in Sources */, AB7BF2CE27AA3E58001865A3 /* AppUtil.swift in Sources */, AB86AC1A2785C2B300E61E6A /* HomeStore.swift in Sources */, - AB7BF2D427AA3F12001865A3 /* CookiesUtil.swift in Sources */, + AB7BF2D427AA3F12001865A3 /* CookieUtil.swift in Sources */, AB7BF30A27ABDFF1001865A3 /* CoreDataMigrationStep.swift in Sources */, AB69CB8226B3DAF400699359 /* ControlPanel.swift in Sources */, AB7BF2D227AA3EDC001865A3 /* HapticsUtil.swift in Sources */, diff --git a/EhPanda/App/Tools/Clients/CookiesClient.swift b/EhPanda/App/Tools/Clients/CookieClient.swift similarity index 95% rename from EhPanda/App/Tools/Clients/CookiesClient.swift rename to EhPanda/App/Tools/Clients/CookieClient.swift index 94d2e2dc..a1b403e9 100644 --- a/EhPanda/App/Tools/Clients/CookiesClient.swift +++ b/EhPanda/App/Tools/Clients/CookieClient.swift @@ -1,5 +1,5 @@ // -// CookiesClient.swift +// CookieClient.swift // EhPanda // // Created by 荒木辰造 on R 4/01/02. @@ -8,7 +8,7 @@ import Foundation import ComposableArchitecture -struct CookiesClient { +struct CookieClient { let clearAll: () -> EffectTask let getCookie: (URL, String) -> CookieValue private let removeCookie: (URL, String) -> Void @@ -16,7 +16,7 @@ struct CookiesClient { private let initializeCookie: (HTTPCookie, String) -> HTTPCookie } -extension CookiesClient { +extension CookieClient { static let live: Self = .init( clearAll: { .fireAndForget { @@ -82,7 +82,7 @@ extension CookiesClient { } // MARK: Foundation -extension CookiesClient { +extension CookieClient { private func setCookie( for url: URL, key: String, value: String, path: String = "/", expiresTime: TimeInterval = .oneYear @@ -120,9 +120,9 @@ extension CookiesClient { } // MARK: Accessor -extension CookiesClient { +extension CookieClient { var didLogin: Bool { - CookiesUtil.didLogin + CookieUtil.didLogin } var apiuid: String { getCookie(Defaults.URL.host, Defaults.Cookie.ipbMemberId).rawValue @@ -200,7 +200,7 @@ extension CookiesClient { } // MARK: SetCookies -extension CookiesClient { +extension CookieClient { func setCookies(state: CookiesState) -> EffectTask { let effects: [EffectTask] = state.allCases.map { subState in setOrEditCookie(for: state.host.url, key: subState.key, value: subState.editingText) @@ -245,16 +245,16 @@ extension CookiesClient { } // MARK: API -enum CookiesClientKey: DependencyKey { - static let liveValue = CookiesClient.live - static let testValue = CookiesClient.noop - static let previewValue = CookiesClient.noop +enum CookieClientKey: DependencyKey { + static let liveValue = CookieClient.live + static let testValue = CookieClient.noop + static let previewValue = CookieClient.noop } extension DependencyValues { - var cookiesClient: CookiesClient { - get { self[CookiesClientKey.self] } - set { self[CookiesClientKey.self] = newValue } + var cookieClient: CookieClient { + get { self[CookieClientKey.self] } + set { self[CookieClientKey.self] = newValue } } } @@ -262,7 +262,7 @@ extension DependencyValues { #if DEBUG import XCTestDynamicOverlay -extension CookiesClient { +extension CookieClient { static let failing: Self = .init( clearAll: { .failing("\(Self.self).clearAll is unimplemented") }, getCookie: { @@ -283,7 +283,7 @@ extension CookiesClient { ) } #endif -extension CookiesClient { +extension CookieClient { static let noop: Self = .init( clearAll: { .none }, getCookie: { _, _ in .empty }, diff --git a/EhPanda/App/Tools/Utilities/CookiesUtil.swift b/EhPanda/App/Tools/Utilities/CookieUtil.swift similarity index 83% rename from EhPanda/App/Tools/Utilities/CookiesUtil.swift rename to EhPanda/App/Tools/Utilities/CookieUtil.swift index 717bda3f..a07f7ebe 100644 --- a/EhPanda/App/Tools/Utilities/CookiesUtil.swift +++ b/EhPanda/App/Tools/Utilities/CookieUtil.swift @@ -1,5 +1,5 @@ // -// CookiesUtil.swift +// CookieUtil.swift // EhPanda // // Created by 荒木辰造 on R 4/02/02. @@ -7,11 +7,11 @@ import Foundation -// MARK: Cookies -struct CookiesUtil { +// MARK: Cookie +struct CookieUtil { static var didLogin: Bool { - CookiesUtil.verify(for: Defaults.URL.ehentai, isEx: false) - || CookiesUtil.verify(for: Defaults.URL.exhentai, isEx: true) + CookieUtil.verify(for: Defaults.URL.ehentai, isEx: false) + || CookieUtil.verify(for: Defaults.URL.exhentai, isEx: true) } static func verify(for url: URL, isEx: Bool) -> Bool { diff --git a/EhPanda/DataFlow/AppDelegateStore.swift b/EhPanda/DataFlow/AppDelegateStore.swift index 1a756170..d68d9954 100644 --- a/EhPanda/DataFlow/AppDelegateStore.swift +++ b/EhPanda/DataFlow/AppDelegateStore.swift @@ -23,7 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { loggerClient: .live, hapticsClient: .live, libraryClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live, clipboardClient: .live, appDelegateClient: .live, @@ -64,7 +64,7 @@ enum AppDelegateAction { struct AppDelegateEnvironment { let dfClient: DFClient let libraryClient: LibraryClient - let cookiesClient: CookiesClient + let cookieClient: CookieClient let databaseClient: DatabaseClient } @@ -75,9 +75,9 @@ let appDelegateReducer = Reducer { state, actio if state.favoritesState.route != nil { effects.append(.init(value: .favorites(.setNavigation(nil)))) effects.append(hapticEffect) - } else if environment.cookiesClient.didLogin { + } else if environment.cookieClient.didLogin { effects.append(.init(value: .favorites(.fetchGalleries()))) effects.append(hapticEffect) } @@ -161,7 +161,7 @@ let appReducerCore = Reducer { state, actio .init(value: .tabBar(.setTabBarItemType(.setting))) ] effects.append(.init(value: .setting(.setNavigation(.account)))) - if !environment.cookiesClient.didLogin { + if !environment.cookieClient.didLogin { effects.append( .init(value: .setting(.account(.setNavigation(.login)))) .delay( @@ -219,7 +219,7 @@ let appReducer = Reducer.combine( loggerClient: $0.loggerClient, hapticsClient: $0.hapticsClient, libraryClient: $0.libraryClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, @@ -245,7 +245,7 @@ let appReducer = Reducer.combine( .init( dfClient: $0.dfClient, libraryClient: $0.libraryClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient ) } @@ -270,7 +270,7 @@ let appReducer = Reducer.combine( deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, libraryClient: $0.libraryClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, @@ -288,7 +288,7 @@ let appReducer = Reducer.combine( imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, @@ -307,7 +307,7 @@ let appReducer = Reducer.combine( imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, @@ -326,7 +326,7 @@ let appReducer = Reducer.combine( loggerClient: $0.loggerClient, hapticsClient: $0.hapticsClient, libraryClient: $0.libraryClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, diff --git a/EhPanda/View/Detail/ArchivesView.swift b/EhPanda/View/Detail/ArchivesView.swift index 9d277697..deb8ef03 100644 --- a/EhPanda/View/Detail/ArchivesView.swift +++ b/EhPanda/View/Detail/ArchivesView.swift @@ -226,7 +226,7 @@ struct ArchivesView_Previews: PreviewProvider { reducer: archivesReducer, environment: ArchivesEnvironment( hapticsClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live ) ), diff --git a/EhPanda/View/Detail/CommentsView.swift b/EhPanda/View/Detail/CommentsView.swift index 89c4b303..2c86baa6 100644 --- a/EhPanda/View/Detail/CommentsView.swift +++ b/EhPanda/View/Detail/CommentsView.swift @@ -135,7 +135,7 @@ struct CommentsView: View { } label: { Image(systemSymbol: .squareAndPencil) } - .disabled(!CookiesUtil.didLogin) + .disabled(!CookieUtil.didLogin) } } } @@ -281,7 +281,7 @@ struct CommentsView_Previews: PreviewProvider { imageClient: .live, deviceClient: .live, hapticsClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live, clipboardClient: .live, appDelegateClient: .live, diff --git a/EhPanda/View/Detail/DataFlow/ArchivesStore.swift b/EhPanda/View/Detail/DataFlow/ArchivesStore.swift index 7f1514de..fedf0a3b 100644 --- a/EhPanda/View/Detail/DataFlow/ArchivesStore.swift +++ b/EhPanda/View/Detail/DataFlow/ArchivesStore.swift @@ -45,7 +45,7 @@ enum ArchivesAction: BindableAction { struct ArchivesEnvironment { let hapticsClient: HapticsClient - let cookiesClient: CookiesClient + let cookieClient: CookieClient let databaseClient: DatabaseClient } @@ -83,7 +83,7 @@ let archivesReducer = Reducer.recurs return .none case .rateGallery: - guard let apiuid = Int(environment.cookiesClient.apiuid), let gid = Int(state.gallery.id) + guard let apiuid = Int(environment.cookieClient.apiuid), let gid = Int(state.gallery.id) else { return .none } return RateGalleryRequest( apiuid: apiuid, apikey: state.apiKey, gid: gid, @@ -329,7 +329,7 @@ let detailReducer = Reducer.recurs .effect.map(DetailAction.anyGalleryOpsDone).cancellable(id: DetailState.CancelID()) case .voteTag(let tag, let vote): - guard let apiuid = Int(environment.cookiesClient.apiuid), let gid = Int(state.gallery.id) + guard let apiuid = Int(environment.cookieClient.apiuid), let gid = Int(state.gallery.id) else { return .none } return VoteGalleryTagRequest( apiuid: apiuid, apikey: state.apiKey, gid: gid, token: state.gallery.token, tag: tag, vote: vote @@ -429,7 +429,7 @@ let detailReducer = Reducer.recurs imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient @@ -442,7 +442,7 @@ let detailReducer = Reducer.recurs environment: { .init( hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient ) } @@ -467,7 +467,7 @@ let detailReducer = Reducer.recurs imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient @@ -484,7 +484,7 @@ let detailReducer = Reducer.recurs imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, @@ -502,7 +502,7 @@ let detailReducer = Reducer.recurs imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, diff --git a/EhPanda/View/Detail/DataFlow/PreviewsStore.swift b/EhPanda/View/Detail/DataFlow/PreviewsStore.swift index 4aa322c9..df1d94ce 100644 --- a/EhPanda/View/Detail/DataFlow/PreviewsStore.swift +++ b/EhPanda/View/Detail/DataFlow/PreviewsStore.swift @@ -56,7 +56,7 @@ struct PreviewsEnvironment { let imageClient: ImageClient let deviceClient: DeviceClient let hapticsClient: HapticsClient - let cookiesClient: CookiesClient + let cookieClient: CookieClient let databaseClient: DatabaseClient let clipboardClient: ClipboardClient let appDelegateClient: AppDelegateClient @@ -151,7 +151,7 @@ let previewsReducer = Reducer.co imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, diff --git a/EhPanda/View/Home/DataFlow/HomeStore.swift b/EhPanda/View/Home/DataFlow/HomeStore.swift index 925ba623..7ba2968d 100644 --- a/EhPanda/View/Home/DataFlow/HomeStore.swift +++ b/EhPanda/View/Home/DataFlow/HomeStore.swift @@ -95,7 +95,7 @@ struct HomeEnvironment { let deviceClient: DeviceClient let hapticsClient: HapticsClient let libraryClient: LibraryClient - let cookiesClient: CookiesClient + let cookieClient: CookieClient let databaseClient: DatabaseClient let clipboardClient: ClipboardClient let appDelegateClient: AppDelegateClient @@ -266,7 +266,7 @@ let homeReducer = Reducer.combine( imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, @@ -284,7 +284,7 @@ let homeReducer = Reducer.combine( imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, @@ -302,7 +302,7 @@ let homeReducer = Reducer.combine( imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, @@ -320,7 +320,7 @@ let homeReducer = Reducer.combine( imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, @@ -338,7 +338,7 @@ let homeReducer = Reducer.combine( imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, @@ -356,7 +356,7 @@ let homeReducer = Reducer.combine( imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, diff --git a/EhPanda/View/Home/DataFlow/PopularStore.swift b/EhPanda/View/Home/DataFlow/PopularStore.swift index 1e529e55..3461e543 100644 --- a/EhPanda/View/Home/DataFlow/PopularStore.swift +++ b/EhPanda/View/Home/DataFlow/PopularStore.swift @@ -53,7 +53,7 @@ struct PopularEnvironment { let imageClient: ImageClient let deviceClient: DeviceClient let hapticsClient: HapticsClient - let cookiesClient: CookiesClient + let cookieClient: CookieClient let databaseClient: DatabaseClient let clipboardClient: ClipboardClient let appDelegateClient: AppDelegateClient @@ -135,7 +135,7 @@ let popularReducer = Reducer.co imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, diff --git a/EhPanda/View/Home/DataFlow/ToplistsStore.swift b/EhPanda/View/Home/DataFlow/ToplistsStore.swift index 4676d465..1afafe03 100644 --- a/EhPanda/View/Home/DataFlow/ToplistsStore.swift +++ b/EhPanda/View/Home/DataFlow/ToplistsStore.swift @@ -86,7 +86,7 @@ struct ToplistsEnvironment { let imageClient: ImageClient let deviceClient: DeviceClient let hapticsClient: HapticsClient - let cookiesClient: CookiesClient + let cookieClient: CookieClient let databaseClient: DatabaseClient let clipboardClient: ClipboardClient let appDelegateClient: AppDelegateClient @@ -218,7 +218,7 @@ let toplistsReducer = Reducer.co imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, diff --git a/EhPanda/View/Home/FrontpageView.swift b/EhPanda/View/Home/FrontpageView.swift index fa4b7041..92e8b085 100644 --- a/EhPanda/View/Home/FrontpageView.swift +++ b/EhPanda/View/Home/FrontpageView.swift @@ -107,7 +107,7 @@ struct FrontpageView_Previews: PreviewProvider { imageClient: .live, deviceClient: .live, hapticsClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live, clipboardClient: .live, appDelegateClient: .live, diff --git a/EhPanda/View/Home/HistoryView.swift b/EhPanda/View/Home/HistoryView.swift index 8a4c0f7a..574f55e1 100644 --- a/EhPanda/View/Home/HistoryView.swift +++ b/EhPanda/View/Home/HistoryView.swift @@ -113,7 +113,7 @@ struct HistoryView_Previews: PreviewProvider { imageClient: .live, deviceClient: .live, hapticsClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live, clipboardClient: .live, appDelegateClient: .live, diff --git a/EhPanda/View/Home/HomeView.swift b/EhPanda/View/Home/HomeView.swift index 11b2870a..41533d2e 100644 --- a/EhPanda/View/Home/HomeView.swift +++ b/EhPanda/View/Home/HomeView.swift @@ -529,7 +529,7 @@ struct HomeView_Previews: PreviewProvider { deviceClient: .live, hapticsClient: .live, libraryClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live, clipboardClient: .live, appDelegateClient: .live, diff --git a/EhPanda/View/Home/PopularView.swift b/EhPanda/View/Home/PopularView.swift index ead30687..530af91e 100644 --- a/EhPanda/View/Home/PopularView.swift +++ b/EhPanda/View/Home/PopularView.swift @@ -104,7 +104,7 @@ struct PopularView_Previews: PreviewProvider { imageClient: .live, deviceClient: .live, hapticsClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live, clipboardClient: .live, appDelegateClient: .live, diff --git a/EhPanda/View/Home/ToplistsView.swift b/EhPanda/View/Home/ToplistsView.swift index 7203995b..e857f3cf 100644 --- a/EhPanda/View/Home/ToplistsView.swift +++ b/EhPanda/View/Home/ToplistsView.swift @@ -162,7 +162,7 @@ struct ToplistsView_Previews: PreviewProvider { imageClient: .live, deviceClient: .live, hapticsClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live, clipboardClient: .live, appDelegateClient: .live, diff --git a/EhPanda/View/Home/WatchedView.swift b/EhPanda/View/Home/WatchedView.swift index cfe0b44c..f2f94e54 100644 --- a/EhPanda/View/Home/WatchedView.swift +++ b/EhPanda/View/Home/WatchedView.swift @@ -30,7 +30,7 @@ struct WatchedView: View { var body: some View { ZStack { - if CookiesUtil.didLogin { + if CookieUtil.didLogin { GenericList( galleries: viewStore.galleries, setting: setting, @@ -86,7 +86,7 @@ struct WatchedView: View { viewStore.send(.fetchGalleries()) } .onAppear { - if viewStore.galleries.isEmpty && CookiesUtil.didLogin { + if viewStore.galleries.isEmpty && CookieUtil.didLogin { DispatchQueue.main.async { viewStore.send(.fetchGalleries()) } @@ -135,7 +135,7 @@ struct WatchedView_Previews: PreviewProvider { imageClient: .live, deviceClient: .live, hapticsClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live, clipboardClient: .live, appDelegateClient: .live, diff --git a/EhPanda/View/Reading/ReadingStore.swift b/EhPanda/View/Reading/ReadingStore.swift index f2f2a4e9..8c4f4351 100644 --- a/EhPanda/View/Reading/ReadingStore.swift +++ b/EhPanda/View/Reading/ReadingStore.swift @@ -169,7 +169,7 @@ struct ReadingEnvironment { let imageClient: ImageClient let deviceClient: DeviceClient let hapticsClient: HapticsClient - let cookiesClient: CookiesClient + let cookieClient: CookieClient let databaseClient: DatabaseClient let clipboardClient: ClipboardClient let appDelegateClient: AppDelegateClient @@ -496,7 +496,7 @@ let readingReducer = Reducer { case .success(let (imageURLs, response)): var effects = [EffectTask]() if let response = response { - effects.append(environment.cookiesClient.setSkipServer(response: response).fireAndForget()) + effects.append(environment.cookieClient.setSkipServer(response: response).fireAndForget()) } guard !imageURLs.isEmpty else { state.imageURLLoadingStates[index] = .failed(.notFound) diff --git a/EhPanda/View/Reading/ReadingView.swift b/EhPanda/View/Reading/ReadingView.swift index f11f16d2..7e3a4095 100644 --- a/EhPanda/View/Reading/ReadingView.swift +++ b/EhPanda/View/Reading/ReadingView.swift @@ -606,7 +606,7 @@ struct ReadingView_Previews: PreviewProvider { imageClient: .live, deviceClient: .live, hapticsClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live, clipboardClient: .live, appDelegateClient: .live diff --git a/EhPanda/View/Search/SearchRootStore.swift b/EhPanda/View/Search/SearchRootStore.swift index 646d4b68..6a2a554d 100644 --- a/EhPanda/View/Search/SearchRootStore.swift +++ b/EhPanda/View/Search/SearchRootStore.swift @@ -87,7 +87,7 @@ struct SearchRootEnvironment { let imageClient: ImageClient let deviceClient: DeviceClient let hapticsClient: HapticsClient - let cookiesClient: CookiesClient + let cookieClient: CookieClient let databaseClient: DatabaseClient let clipboardClient: ClipboardClient let appDelegateClient: AppDelegateClient @@ -201,7 +201,7 @@ let searchRootReducer = Reducer.combin imageClient: $0.imageClient, deviceClient: $0.deviceClient, hapticsClient: $0.hapticsClient, - cookiesClient: $0.cookiesClient, + cookieClient: $0.cookieClient, databaseClient: $0.databaseClient, clipboardClient: $0.clipboardClient, appDelegateClient: $0.appDelegateClient, diff --git a/EhPanda/View/Search/SearchView.swift b/EhPanda/View/Search/SearchView.swift index b6ff2292..ea8cd187 100644 --- a/EhPanda/View/Search/SearchView.swift +++ b/EhPanda/View/Search/SearchView.swift @@ -130,7 +130,7 @@ struct SearchView_Previews: PreviewProvider { imageClient: .live, deviceClient: .live, hapticsClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live, clipboardClient: .live, appDelegateClient: .live, diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift index 2635ab4e..c1e98733 100644 --- a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift @@ -42,7 +42,7 @@ struct AccountSettingReducer: ReducerProtocol { } @Dependency(\.clipboardClient) private var clipboardClient - @Dependency(\.cookiesClient) private var cookiesClient + @Dependency(\.cookieClient) private var cookieClient @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { @@ -52,10 +52,10 @@ struct AccountSettingReducer: ReducerProtocol { return state.route == nil ? .init(value: .clearSubStates) : .none case .binding(\.$ehCookiesState): - return cookiesClient.setCookies(state: state.ehCookiesState).fireAndForget() + return cookieClient.setCookies(state: state.ehCookiesState).fireAndForget() case .binding(\.$exCookiesState): - return cookiesClient.setCookies(state: state.exCookiesState).fireAndForget() + return cookieClient.setCookies(state: state.exCookiesState).fireAndForget() case .binding: return .none @@ -76,12 +76,12 @@ struct AccountSettingReducer: ReducerProtocol { ) case .loadCookies: - state.ehCookiesState = cookiesClient.loadCookiesState(host: .ehentai) - state.exCookiesState = cookiesClient.loadCookiesState(host: .exhentai) + state.ehCookiesState = cookieClient.loadCookiesState(host: .ehentai) + state.exCookiesState = cookieClient.loadCookiesState(host: .exhentai) return .none case .copyCookies(let host): - let cookiesDescription = cookiesClient.getCookiesDescription(host: host) + let cookiesDescription = cookieClient.getCookiesDescription(host: host) return .merge( .init(value: .setNavigation(.hud)), clipboardClient.saveText(cookiesDescription).fireAndForget(), @@ -89,7 +89,7 @@ struct AccountSettingReducer: ReducerProtocol { ) case .login(.loginDone): - return cookiesClient.didLogin ? .init(value: .setNavigation(nil)) : .none + return cookieClient.didLogin ? .init(value: .setNavigation(nil)) : .none case .login: return .none diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift index 7bc49d19..6e796bd2 100644 --- a/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift @@ -123,7 +123,7 @@ private struct AccountSection: View { } var body: some View { - if !CookiesUtil.didLogin { + if !CookieUtil.didLogin { Button(L10n.Localizable.AccountSettingView.Button.login, action: loginAction) } else { Button( diff --git a/EhPanda/View/Setting/DataFlow/EhSettingStore.swift b/EhPanda/View/Setting/DataFlow/EhSettingStore.swift index 5e759d52..dadd9e20 100644 --- a/EhPanda/View/Setting/DataFlow/EhSettingStore.swift +++ b/EhPanda/View/Setting/DataFlow/EhSettingStore.swift @@ -50,7 +50,7 @@ enum EhSettingAction: BindableAction, Equatable { struct EhSettingEnvironment { let hapticsClient: HapticsClient - let cookiesClient: CookiesClient + let cookieClient: CookieClient let uiApplicationClient: UIApplicationClient } @@ -67,7 +67,7 @@ let ehSettingReducer = Reducer.co { state.setting.galleryHost = galleryHost } - if environment.cookiesClient.shouldFetchIgneous { + if environment.cookieClient.shouldFetchIgneous { effects.append(.init(value: .fetchIgneous)) } - if environment.cookiesClient.didLogin { + if environment.cookieClient.didLogin { effects.append(contentsOf: [ .init(value: .fetchUserInfo), .init(value: .fetchGreeting), @@ -269,20 +269,20 @@ let settingReducer = Reducer.co return EhProfileRequest(action: .create, name: "EhPanda").effect.fireAndForget() case .fetchIgneous: - guard environment.cookiesClient.didLogin else { return .none } + guard environment.cookieClient.didLogin else { return .none } return IgneousRequest().effect.map(SettingAction.fetchIgneousDone) case .fetchIgneousDone(let result): var effects = [EffectTask]() if case .success(let response) = result { - effects.append(environment.cookiesClient.setCredentials(response: response).fireAndForget()) + effects.append(environment.cookieClient.setCredentials(response: response).fireAndForget()) } effects.append(.init(value: .account(.loadCookies))) return .merge(effects) case .fetchUserInfo: - guard environment.cookiesClient.didLogin else { return .none } - let uid = environment.cookiesClient + guard environment.cookieClient.didLogin else { return .none } + let uid = environment.cookieClient .getCookie(Defaults.URL.host, Defaults.Cookie.ipbMemberId).rawValue if !uid.isEmpty { return UserInfoRequest(uid: uid).effect.map(SettingAction.fetchUserInfoDone) @@ -314,7 +314,7 @@ let settingReducer = Reducer.co return false } - guard environment.cookiesClient.didLogin, + guard environment.cookieClient.didLogin, state.setting.showsNewDawnGreeting else { return .none } let requestEffect = GreetingRequest().effect @@ -376,7 +376,7 @@ let settingReducer = Reducer.co return .none case .fetchEhProfileIndex: - guard environment.cookiesClient.didLogin else { return .none } + guard environment.cookieClient.didLogin else { return .none } return VerifyEhProfileRequest().effect.map(SettingAction.fetchEhProfileIndexDone) case .fetchEhProfileIndexDone(let result): @@ -388,10 +388,10 @@ let settingReducer = Reducer.co let profileValueString = String(profileValue) let selectedProfileKey = Defaults.Cookie.selectedProfile - let cookieValue = environment.cookiesClient.getCookie(hostURL, selectedProfileKey) + let cookieValue = environment.cookieClient.getCookie(hostURL, selectedProfileKey) if cookieValue.rawValue != profileValueString { effects.append( - environment.cookiesClient.setOrEditCookie( + environment.cookieClient.setOrEditCookie( for: hostURL, key: selectedProfileKey, value: profileValueString ) .fireAndForget() @@ -407,7 +407,7 @@ let settingReducer = Reducer.co return effects.isEmpty ? .none : .merge(effects) case .fetchFavoriteCategories: - guard environment.cookiesClient.didLogin else { return .none } + guard environment.cookieClient.didLogin else { return .none } return FavoriteCategoriesRequest().effect.map(SettingAction.fetchFavoriteCategoriesDone) case .fetchFavoriteCategoriesDone(let result): @@ -418,8 +418,8 @@ let settingReducer = Reducer.co case .account(.login(.loginDone)): return .merge( - environment.cookiesClient.removeYay().fireAndForget(), - environment.cookiesClient.fulfillAnotherHostField().fireAndForget(), + environment.cookieClient.removeYay().fireAndForget(), + environment.cookieClient.fulfillAnotherHostField().fireAndForget(), .init(value: .fetchIgneous), .init(value: .fetchUserInfo), .init(value: .fetchFavoriteCategories), @@ -430,7 +430,7 @@ let settingReducer = Reducer.co state.user = User() return .merge( .init(value: .syncUser), - environment.cookiesClient.clearAll().fireAndForget(), + environment.cookieClient.clearAll().fireAndForget(), environment.databaseClient.removeImageURLs().fireAndForget(), environment.libraryClient.clearWebImageDiskCache().fireAndForget() ) @@ -461,7 +461,7 @@ let settingReducer = Reducer.co // environment: { // .init( // hapticsClient: $0.hapticsClient, -// cookiesClient: $0.cookiesClient, +// cookieClient: $0.cookieClient, // clipboardClient: $0.clipboardClient, // uiApplicationClient: $0.uiApplicationClient // ) diff --git a/EhPanda/View/Setting/SettingView.swift b/EhPanda/View/Setting/SettingView.swift index 0bd5665d..c827323b 100644 --- a/EhPanda/View/Setting/SettingView.swift +++ b/EhPanda/View/Setting/SettingView.swift @@ -193,7 +193,7 @@ struct SettingView_Previews: PreviewProvider { loggerClient: .live, hapticsClient: .live, libraryClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live, clipboardClient: .live, appDelegateClient: .live, diff --git a/EhPanda/View/Setting/Support/EhSettingView.swift b/EhPanda/View/Setting/Support/EhSettingView.swift index d35d7408..f47448f8 100644 --- a/EhPanda/View/Setting/Support/EhSettingView.swift +++ b/EhPanda/View/Setting/Support/EhSettingView.swift @@ -1090,7 +1090,7 @@ struct EhSettingView_Previews: PreviewProvider { reducer: ehSettingReducer, environment: EhSettingEnvironment( hapticsClient: .live, - cookiesClient: .live, + cookieClient: .live, uiApplicationClient: .live ) ), diff --git a/EhPanda/View/TabBar/TabBarView.swift b/EhPanda/View/TabBar/TabBarView.swift index 32cbdaef..29f435fd 100644 --- a/EhPanda/View/TabBar/TabBarView.swift +++ b/EhPanda/View/TabBar/TabBarView.swift @@ -165,7 +165,7 @@ struct TabBarView_Previews: PreviewProvider { loggerClient: .live, hapticsClient: .live, libraryClient: .live, - cookiesClient: .live, + cookieClient: .live, databaseClient: .live, clipboardClient: .live, appDelegateClient: .live, diff --git a/EhPandaTests/Tests/Store/Setting/AccountSettingStoreTests.swift b/EhPandaTests/Tests/Store/Setting/AccountSettingStoreTests.swift index 07aca46c..18fa8969 100644 --- a/EhPandaTests/Tests/Store/Setting/AccountSettingStoreTests.swift +++ b/EhPandaTests/Tests/Store/Setting/AccountSettingStoreTests.swift @@ -13,7 +13,7 @@ class AccountSettingStoreTests: XCTestCase { private var noopEnvironment: AccountSettingEnvironment { .init( hapticsClient: .noop, - cookiesClient: .noop, + cookieClient: .noop, clipboardClient: .noop, uiApplicationClient: .noop ) @@ -34,7 +34,7 @@ class AccountSettingStoreTests: XCTestCase { return (ehCookiesState, exCookiesState) } private static func setCookies(with state: CookiesState) { - _ = CookiesClient.live.setCookies(state: state).sink(receiveValue: { _ in }) + _ = CookieClient.live.setCookies(state: state).sink(receiveValue: { _ in }) } @discardableResult private static func teardownCookies(value: String? = nil) -> String { let initialValue = UUID().uuidString @@ -53,7 +53,7 @@ class AccountSettingStoreTests: XCTestCase { func testCookies(with value: String) throws { [Defaults.Cookie.igneous, Defaults.Cookie.ipbMemberId, Defaults.Cookie.ipbPassHash] .flatMap({ key in [Defaults.URL.ehentai, Defaults.URL.exhentai].map({ ($0, key) }) }) - .map(CookiesClient.live.getCookie) + .map(CookieClient.live.getCookie) .forEach({ XCTAssertEqual($0, .init(rawValue: value, localizedString: .init())) }) } @@ -69,7 +69,7 @@ class AccountSettingStoreTests: XCTestCase { reducer: accountSettingReducer, environment: AccountSettingEnvironment( hapticsClient: .noop, - cookiesClient: .live, + cookieClient: .live, clipboardClient: .noop, uiApplicationClient: .noop ) @@ -135,7 +135,7 @@ class AccountSettingStoreTests: XCTestCase { reducer: accountSettingReducer, environment: AccountSettingEnvironment( hapticsClient: .noop, - cookiesClient: .live, + cookieClient: .live, clipboardClient: .noop, uiApplicationClient: .noop ) @@ -224,7 +224,7 @@ class AccountSettingStoreTests: XCTestCase { store.send(.login(.loginDone(.success(nil)))) { $0.loginState = .init(route: nil, loginState: .idle) } - if noopEnvironment.cookiesClient.didLogin { + if noopEnvironment.cookieClient.didLogin { store.receive(.setNavigation(nil)) { $0.route = nil } From b15d16a27501e11bcece986efe92cce5e1f4726a Mon Sep 17 00:00:00 2001 From: Chihchy Date: Tue, 9 May 2023 22:59:11 +0800 Subject: [PATCH 10/29] Refactor clients static members --- .../App/Tools/Clients/AppDelegateClient.swift | 7 +++- .../Tools/Clients/AuthorizationClient.swift | 20 ++++------- .../App/Tools/Clients/ClipboardClient.swift | 27 +++++---------- EhPanda/App/Tools/Clients/CookieClient.swift | 34 +++++-------------- EhPanda/App/Tools/Clients/DFClient.swift | 6 +++- .../App/Tools/Clients/DatabaseClient.swift | 24 +++++-------- EhPanda/App/Tools/Clients/DeviceClient.swift | 9 ++++- EhPanda/App/Tools/Clients/FileClient.swift | 24 +++++-------- EhPanda/App/Tools/Clients/HapticsClient.swift | 7 +++- EhPanda/App/Tools/Clients/ImageClient.swift | 9 ++++- EhPanda/App/Tools/Clients/LibraryClient.swift | 24 ++++++------- EhPanda/App/Tools/Clients/LoggerClient.swift | 17 ++++------ .../Tools/Clients/UIApplicationClient.swift | 28 +++++---------- EhPanda/App/Tools/Clients/URLClient.swift | 8 ++++- .../Tools/Clients/UserDefaultsClient.swift | 6 +++- 15 files changed, 109 insertions(+), 141 deletions(-) diff --git a/EhPanda/App/Tools/Clients/AppDelegateClient.swift b/EhPanda/App/Tools/Clients/AppDelegateClient.swift index 427ea27e..739f8da7 100644 --- a/EhPanda/App/Tools/Clients/AppDelegateClient.swift +++ b/EhPanda/App/Tools/Clients/AppDelegateClient.swift @@ -41,8 +41,8 @@ extension AppDelegateClient { // MARK: API enum AppDelegateClientKey: DependencyKey { static let liveValue = AppDelegateClient.live - static let testValue = AppDelegateClient.noop static let previewValue = AppDelegateClient.noop + static let testValue = AppDelegateClient.unimplemented } extension DependencyValues { @@ -58,4 +58,9 @@ extension AppDelegateClient { setOrientation: { _ in .none }, setOrientationMask: { _ in .none } ) + + static let unimplemented: Self = .init( + setOrientation: XCTestDynamicOverlay.unimplemented("\(Self.self).setOrientation"), + setOrientationMask: XCTestDynamicOverlay.unimplemented("\(Self.self).setOrientationMask") + ) } diff --git a/EhPanda/App/Tools/Clients/AuthorizationClient.swift b/EhPanda/App/Tools/Clients/AuthorizationClient.swift index 39f32b21..442811fe 100644 --- a/EhPanda/App/Tools/Clients/AuthorizationClient.swift +++ b/EhPanda/App/Tools/Clients/AuthorizationClient.swift @@ -43,8 +43,8 @@ extension AuthorizationClient { // MARK: API enum AuthorizationClientKey: DependencyKey { static let liveValue = AuthorizationClient.live - static let testValue = AuthorizationClient.noop static let previewValue = AuthorizationClient.noop + static let testValue = AuthorizationClient.unimplemented } extension DependencyValues { @@ -55,22 +55,14 @@ extension DependencyValues { } // MARK: Test -#if DEBUG -import XCTestDynamicOverlay - -extension AuthorizationClient { - static let failing: Self = .init( - passcodeNotSet: { - XCTFail("\(Self.self).passcodeNotSet is unimplemented") - return false - }, - localAuthroize: { .failing("\(Self.self).localAuthroize(\($0)) is unimplemented")} - ) -} -#endif extension AuthorizationClient { static let noop: Self = .init( passcodeNotSet: { false }, localAuthroize: { _ in .none } ) + + static let unimplemented: Self = .init( + passcodeNotSet: XCTestDynamicOverlay.unimplemented("\(Self.self).passcodeNotSet"), + localAuthroize: XCTestDynamicOverlay.unimplemented("\(Self.self).localAuthroize") + ) } diff --git a/EhPanda/App/Tools/Clients/ClipboardClient.swift b/EhPanda/App/Tools/Clients/ClipboardClient.swift index fdea8f18..4c72c63f 100644 --- a/EhPanda/App/Tools/Clients/ClipboardClient.swift +++ b/EhPanda/App/Tools/Clients/ClipboardClient.swift @@ -52,8 +52,8 @@ extension ClipboardClient { // MARK: API enum ClipboardClientKey: DependencyKey { static let liveValue = ClipboardClient.live - static let testValue = ClipboardClient.noop static let previewValue = ClipboardClient.noop + static let testValue = ClipboardClient.unimplemented } extension DependencyValues { @@ -64,24 +64,6 @@ extension DependencyValues { } // MARK: Test -#if DEBUG -import XCTestDynamicOverlay - -extension ClipboardClient { - static let failing: Self = .init( - url: { - XCTFail("\(Self.self).url is unimplemented") - return nil - }, - changeCount: { - XCTFail("\(Self.self).changeCount is unimplemented") - return 0 - }, - saveText: { .failing("\(Self.self).saveText(\($0)) is unimplemented") }, - saveImage: { .failing("\(Self.self).saveImage(\($0), \($1)) is unimplemented") } - ) -} -#endif extension ClipboardClient { static let noop: Self = .init( url: { nil }, @@ -89,4 +71,11 @@ extension ClipboardClient { saveText: { _ in .none }, saveImage: { _, _ in .none } ) + + 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") + ) } diff --git a/EhPanda/App/Tools/Clients/CookieClient.swift b/EhPanda/App/Tools/Clients/CookieClient.swift index a1b403e9..4402d95c 100644 --- a/EhPanda/App/Tools/Clients/CookieClient.swift +++ b/EhPanda/App/Tools/Clients/CookieClient.swift @@ -247,8 +247,8 @@ extension CookieClient { // MARK: API enum CookieClientKey: DependencyKey { static let liveValue = CookieClient.live - static let testValue = CookieClient.noop static let previewValue = CookieClient.noop + static let testValue = CookieClient.unimplemented } extension DependencyValues { @@ -259,30 +259,6 @@ extension DependencyValues { } // MARK: Test -#if DEBUG -import XCTestDynamicOverlay - -extension CookieClient { - static let failing: Self = .init( - clearAll: { .failing("\(Self.self).clearAll is unimplemented") }, - getCookie: { - XCTFail("\(Self.self).getCookie(\($0), \($1)) is unimplemented") - return .empty - }, - removeCookie: { - XCTFail("\(Self.self).removeCookie(\($0), \($1)) is unimplemented") - }, - checkExistence: { - XCTFail("\(Self.self).checkExistence(\($0), \($1)) is unimplemented") - return false - }, - initializeCookie: { - XCTFail("\(Self.self).initializeCookie(\($0), \($1)) is unimplemented") - return .init() - } - ) -} -#endif extension CookieClient { static let noop: Self = .init( clearAll: { .none }, @@ -291,4 +267,12 @@ extension CookieClient { checkExistence: { _, _ in false }, initializeCookie: { _, _ in .init() } ) + + 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") + ) } diff --git a/EhPanda/App/Tools/Clients/DFClient.swift b/EhPanda/App/Tools/Clients/DFClient.swift index b43a4487..54a900c5 100644 --- a/EhPanda/App/Tools/Clients/DFClient.swift +++ b/EhPanda/App/Tools/Clients/DFClient.swift @@ -34,8 +34,8 @@ extension DFClient { // MARK: API enum DFClientKey: DependencyKey { static let liveValue = DFClient.live - static let testValue = DFClient.noop static let previewValue = DFClient.noop + static let testValue = DFClient.unimplemented } extension DependencyValues { @@ -50,4 +50,8 @@ extension DFClient { static let noop: Self = .init( setActive: { _ in .none } ) + + static let unimplemented: Self = .init( + setActive: XCTestDynamicOverlay.unimplemented("\(Self.self).setActive") + ) } diff --git a/EhPanda/App/Tools/Clients/DatabaseClient.swift b/EhPanda/App/Tools/Clients/DatabaseClient.swift index e7a3b4d2..91f6d415 100644 --- a/EhPanda/App/Tools/Clients/DatabaseClient.swift +++ b/EhPanda/App/Tools/Clients/DatabaseClient.swift @@ -538,8 +538,8 @@ extension DatabaseClient { // MARK: API enum DatabaseClientKey: DependencyKey { static let liveValue = DatabaseClient.live - static let testValue = DatabaseClient.noop static let previewValue = DatabaseClient.noop + static let testValue = DatabaseClient.unimplemented } extension DependencyValues { @@ -550,21 +550,6 @@ extension DependencyValues { } // MARK: Test -#if DEBUG -import XCTestDynamicOverlay - -extension DatabaseClient { - static let failing: Self = .init( - prepareDatabase: { .failing("\(Self.self).prepareDatabase is unimplemented") }, - dropDatabase: { .failing("\(Self.self).dropDatabase is unimplemented") }, - saveContext: { XCTFail("\(Self.self).saveContext is unimplemented") }, - materializedObjects: { - XCTFail("\(Self.self).materializedObjects(\($0), \($1)) is unimplemented") - return .init() - } - ) -} -#endif extension DatabaseClient { static let noop: Self = .init( prepareDatabase: { .none }, @@ -572,4 +557,11 @@ extension DatabaseClient { saveContext: {}, materializedObjects: { _, _ in .init() } ) + + 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") + ) } diff --git a/EhPanda/App/Tools/Clients/DeviceClient.swift b/EhPanda/App/Tools/Clients/DeviceClient.swift index 0c437388..6e660117 100644 --- a/EhPanda/App/Tools/Clients/DeviceClient.swift +++ b/EhPanda/App/Tools/Clients/DeviceClient.swift @@ -35,8 +35,8 @@ extension DeviceClient { // MARK: API enum DeviceClientKey: DependencyKey { static let liveValue = DeviceClient.live - static let testValue = DeviceClient.noop static let previewValue = DeviceClient.noop + static let testValue = DeviceClient.unimplemented } extension DependencyValues { @@ -54,4 +54,11 @@ extension DeviceClient { absWindowH: { .zero }, touchPoint: { .zero } ) + + 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") + ) } diff --git a/EhPanda/App/Tools/Clients/FileClient.swift b/EhPanda/App/Tools/Clients/FileClient.swift index 60478ebc..15461dd9 100644 --- a/EhPanda/App/Tools/Clients/FileClient.swift +++ b/EhPanda/App/Tools/Clients/FileClient.swift @@ -108,8 +108,8 @@ extension FileClient { // MARK: API enum FileClientKey: DependencyKey { static let liveValue = FileClient.live - static let testValue = FileClient.noop static let previewValue = FileClient.noop + static let testValue = FileClient.unimplemented } extension DependencyValues { @@ -120,21 +120,6 @@ extension DependencyValues { } // MARK: Test -#if DEBUG -import XCTestDynamicOverlay - -extension FileClient { - static let failing: Self = .init( - createFile: { - XCTFail("\(Self.self).createFile(\($0), \(String(describing: $1))) is unimplemented") - return false - }, - fetchLogs: { .failing("\(Self.self).fetchLogs is unimplemented") }, - deleteLog: { .failing("\(Self.self).deleteLog(\($0)) is unimplemented") }, - importTagTranslator: { .failing("\(Self.self).importTagTranslator(\($0)) is unimplemented") } - ) -} -#endif extension FileClient { static let noop: Self = .init( createFile: { _, _ in false }, @@ -142,4 +127,11 @@ extension FileClient { deleteLog: { _ in .none }, importTagTranslator: { _ in .none } ) + + 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") + ) } diff --git a/EhPanda/App/Tools/Clients/HapticsClient.swift b/EhPanda/App/Tools/Clients/HapticsClient.swift index 6cb939d5..591ea4d9 100644 --- a/EhPanda/App/Tools/Clients/HapticsClient.swift +++ b/EhPanda/App/Tools/Clients/HapticsClient.swift @@ -31,8 +31,8 @@ extension HapticsClient { // MARK: API enum HapticsClientKey: DependencyKey { static let liveValue = HapticsClient.live - static let testValue = HapticsClient.noop static let previewValue = HapticsClient.noop + static let testValue = HapticsClient.unimplemented } extension DependencyValues { @@ -48,4 +48,9 @@ extension HapticsClient { generateFeedback: { _ in .none }, generateNotificationFeedback: { _ in .none } ) + + static let unimplemented: Self = .init( + generateFeedback: XCTestDynamicOverlay.unimplemented("\(Self.self).generateFeedback"), + generateNotificationFeedback: XCTestDynamicOverlay.unimplemented("\(Self.self).generateNotificationFeedback") + ) } diff --git a/EhPanda/App/Tools/Clients/ImageClient.swift b/EhPanda/App/Tools/Clients/ImageClient.swift index 53747340..f31a8638 100644 --- a/EhPanda/App/Tools/Clients/ImageClient.swift +++ b/EhPanda/App/Tools/Clients/ImageClient.swift @@ -107,8 +107,8 @@ private final class ImageSaver: NSObject { // MARK: API enum ImageClientKey: DependencyKey { static let liveValue = ImageClient.live - static let testValue = ImageClient.noop static let previewValue = ImageClient.noop + static let testValue = ImageClient.unimplemented } extension DependencyValues { @@ -126,4 +126,11 @@ extension ImageClient { downloadImage: { _ in .none }, retrieveImage: { _ in .none } ) + + 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") + ) } diff --git a/EhPanda/App/Tools/Clients/LibraryClient.swift b/EhPanda/App/Tools/Clients/LibraryClient.swift index a4142815..5f566e54 100644 --- a/EhPanda/App/Tools/Clients/LibraryClient.swift +++ b/EhPanda/App/Tools/Clients/LibraryClient.swift @@ -91,8 +91,8 @@ extension LibraryClient { // MARK: API enum LibraryClientKey: DependencyKey { static let liveValue = LibraryClient.live - static let testValue = LibraryClient.noop static let previewValue = LibraryClient.noop + static let testValue = LibraryClient.unimplemented } extension DependencyValues { @@ -103,19 +103,6 @@ extension DependencyValues { } // MARK: Test -#if DEBUG -import XCTestDynamicOverlay - -extension LibraryClient { - static let failing: Self = .init( - initializeLogger: { .failing("\(Self.self).initializeLogger is unimplemented") }, - initializeWebImage: { .failing("\(Self.self).initializeWebImage is unimplemented") }, - clearWebImageDiskCache: { .failing("\(Self.self).clearWebImageDiskCache is unimplemented") }, - analyzeImageColors: { _ in .failing("\(Self.self).analyzeImageColors is unimplemented") }, - calculateWebImageDiskCacheSize: { .failing("\(Self.self).calculateWebImageDiskCacheSize is unimplemented") } - ) -} -#endif extension LibraryClient { static let noop: Self = .init( initializeLogger: { .none }, @@ -124,4 +111,13 @@ extension LibraryClient { analyzeImageColors: { _ in .none }, calculateWebImageDiskCacheSize: { .none } ) + + 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"), + calculateWebImageDiskCacheSize: + XCTestDynamicOverlay.unimplemented("\(Self.self).calculateWebImageDiskCacheSize") + ) } diff --git a/EhPanda/App/Tools/Clients/LoggerClient.swift b/EhPanda/App/Tools/Clients/LoggerClient.swift index f4eb8184..ebeefb83 100644 --- a/EhPanda/App/Tools/Clients/LoggerClient.swift +++ b/EhPanda/App/Tools/Clients/LoggerClient.swift @@ -30,8 +30,8 @@ extension LoggerClient { // MARK: API enum LoggerClientKey: DependencyKey { static let liveValue = LoggerClient.live - static let testValue = LoggerClient.noop static let previewValue = LoggerClient.noop + static let testValue = LoggerClient.unimplemented } extension DependencyValues { @@ -42,19 +42,14 @@ extension DependencyValues { } // MARK: Test -#if DEBUG -import XCTestDynamicOverlay - -extension LoggerClient { - static let failing: Self = .init( - info: { .failing("\(Self.self).info(\($0), \(String(describing: $1))) is unimplemented") }, - error: { .failing("\(Self.self).error(\($0), \(String(describing: $1))) is unimplemented") } - ) -} -#endif extension LoggerClient { static let noop: Self = .init( info: { _, _ in .none }, error: { _, _ in .none } ) + + static let unimplemented: Self = .init( + info: XCTestDynamicOverlay.unimplemented("\(Self.self).info"), + error: XCTestDynamicOverlay.unimplemented("\(Self.self).error") + ) } diff --git a/EhPanda/App/Tools/Clients/UIApplicationClient.swift b/EhPanda/App/Tools/Clients/UIApplicationClient.swift index 153d8f8e..97aef088 100644 --- a/EhPanda/App/Tools/Clients/UIApplicationClient.swift +++ b/EhPanda/App/Tools/Clients/UIApplicationClient.swift @@ -70,8 +70,8 @@ extension UIApplicationClient { // MARK: API enum UIApplicationClientKey: DependencyKey { static let liveValue = UIApplicationClient.live - static let testValue = UIApplicationClient.noop static let previewValue = UIApplicationClient.noop + static let testValue = UIApplicationClient.unimplemented } extension DependencyValues { @@ -82,24 +82,6 @@ extension DependencyValues { } // MARK: Test -// swiftlint:disable line_length -#if DEBUG -import XCTestDynamicOverlay - -extension UIApplicationClient { - static let failing: Self = .init( - openURL: { .failing("\(Self.self).openURL(\($0)) is unimplemented") }, - hideKeyboard: { .failing("\(Self.self).hideKeyboard is unimplemented") }, - alternateIconName: { - XCTFail("\(Self.self).alternateIconName is unimplemented") - return nil - }, - setAlternateIconName: { .failing("\(Self.self).setAlternateIconName(\(String(describing: $0))) is unimplemented") }, - setUserInterfaceStyle: { .failing("\(Self.self).setUserInterfaceStyle(\($0)) is unimplemented") } - ) -} -#endif -// swiftlint:enable line_length extension UIApplicationClient { static let noop: Self = .init( openURL: { _ in .none}, @@ -108,4 +90,12 @@ extension UIApplicationClient { setAlternateIconName: { _ in .none }, setUserInterfaceStyle: { _ in .none } ) + + 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") + ) } diff --git a/EhPanda/App/Tools/Clients/URLClient.swift b/EhPanda/App/Tools/Clients/URLClient.swift index 935a1253..30ea498c 100644 --- a/EhPanda/App/Tools/Clients/URLClient.swift +++ b/EhPanda/App/Tools/Clients/URLClient.swift @@ -71,8 +71,8 @@ extension URLClient { // MARK: API enum URLClientKey: DependencyKey { static let liveValue = URLClient.live - static let testValue = URLClient.noop static let previewValue = URLClient.noop + static let testValue = URLClient.unimplemented } extension DependencyValues { @@ -89,4 +89,10 @@ extension URLClient { checkIfMPVURL: { _ in false }, parseGalleryID: { _ in .init() } ) + + static let unimplemented: Self = .init( + checkIfHandleable: XCTestDynamicOverlay.unimplemented("\(Self.self).checkIfHandleable"), + checkIfMPVURL: XCTestDynamicOverlay.unimplemented("\(Self.self).checkIfMPVURL"), + parseGalleryID: XCTestDynamicOverlay.unimplemented("\(Self.self).parseGalleryID") + ) } diff --git a/EhPanda/App/Tools/Clients/UserDefaultsClient.swift b/EhPanda/App/Tools/Clients/UserDefaultsClient.swift index 45847af6..c403f278 100644 --- a/EhPanda/App/Tools/Clients/UserDefaultsClient.swift +++ b/EhPanda/App/Tools/Clients/UserDefaultsClient.swift @@ -29,8 +29,8 @@ extension UserDefaultsClient { // MARK: API enum UserDefaultsClientKey: DependencyKey { static let liveValue = UserDefaultsClient.live - static let testValue = UserDefaultsClient.noop static let previewValue = UserDefaultsClient.noop + static let testValue = UserDefaultsClient.unimplemented } extension DependencyValues { @@ -45,4 +45,8 @@ extension UserDefaultsClient { static let noop: Self = .init( setValue: { _, _ in .none } ) + + static let unimplemented: Self = .init( + setValue: XCTestDynamicOverlay.unimplemented("\(Self.self).setValue") + ) } From 367cab2c98245c6452afb5741d1c51ee3f400ac0 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Tue, 9 May 2023 23:01:56 +0800 Subject: [PATCH 11/29] Refactor SettingReducer Refactor EhSettingReducer Refactor LogsReducer Refactor GeneralSettingReducer Refactor AppearanceSettingReducer Refactor LoginReducer --- EhPanda.xcodeproj/project.pbxproj | 136 +++-- EhPanda/App/Tools/Parser.swift | 4 +- EhPanda/App/de.lproj/Localizable.strings | 20 +- EhPanda/App/en.lproj/Constant.strings | 154 +++--- EhPanda/App/en.lproj/Localizable.strings | 20 +- EhPanda/App/ja.lproj/Localizable.strings | 20 +- EhPanda/App/ko.lproj/Localizable.strings | 20 +- EhPanda/App/zh-Hans.lproj/Localizable.strings | 20 +- .../App/zh-Hant-HK.lproj/Localizable.strings | 20 +- .../App/zh-Hant-TW.lproj/Localizable.strings | 20 +- EhPanda/App/zh-Hant.lproj/Localizable.strings | 20 +- EhPanda/DataFlow/AppStore.swift | 47 +- EhPanda/Models/Persistent/AppEnv.swift | 2 +- EhPanda/Network/Request.swift | 6 +- .../AccountSettingReducer.swift | 9 +- .../AppearanceSettingReducer.swift | 38 ++ .../AppearanceSettingView.swift | 11 +- .../View/Setting/Components/AboutView.swift | 263 ++++++++++ .../LaboratorySettingView.swift | 0 .../{ => Components}/ReadingSettingView.swift | 0 .../{Support => Components}/WebView.swift | 0 .../DataFlow/AppearanceSettingStore.swift | 36 -- .../Setting/DataFlow/EhSettingStore.swift | 138 ----- .../DataFlow/GeneralSettingStore.swift | 116 ----- .../View/Setting/DataFlow/LoginStore.swift | 95 ---- EhPanda/View/Setting/DataFlow/LogsStore.swift | 80 --- .../View/Setting/DataFlow/SettingStore.swift | 491 ------------------ EhPanda/View/Setting/EhPandaView.swift | 263 ---------- .../Setting/EhSetting/EhSettingReducer.swift | 142 +++++ .../EhSettingView.swift | 21 +- .../GeneralSettingReducer.swift | 107 ++++ .../GeneralSettingView.swift | 24 +- EhPanda/View/Setting/Login/LoginReducer.swift | 98 ++++ .../{Support => Login}/LoginView.swift | 20 +- EhPanda/View/Setting/Logs/LogsReducer.swift | 84 +++ .../Setting/{Support => Logs}/LogsView.swift | 14 +- EhPanda/View/Setting/SettingReducer.swift | 461 ++++++++++++++++ EhPanda/View/Setting/SettingView.swift | 59 +-- 38 files changed, 1526 insertions(+), 1553 deletions(-) create mode 100644 EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift rename EhPanda/View/Setting/{ => AppearanceSetting}/AppearanceSettingView.swift (94%) create mode 100644 EhPanda/View/Setting/Components/AboutView.swift rename EhPanda/View/Setting/{ => Components}/LaboratorySettingView.swift (100%) rename EhPanda/View/Setting/{ => Components}/ReadingSettingView.swift (100%) rename EhPanda/View/Setting/{Support => Components}/WebView.swift (100%) delete mode 100644 EhPanda/View/Setting/DataFlow/AppearanceSettingStore.swift delete mode 100644 EhPanda/View/Setting/DataFlow/EhSettingStore.swift delete mode 100644 EhPanda/View/Setting/DataFlow/GeneralSettingStore.swift delete mode 100644 EhPanda/View/Setting/DataFlow/LoginStore.swift delete mode 100644 EhPanda/View/Setting/DataFlow/LogsStore.swift delete mode 100644 EhPanda/View/Setting/DataFlow/SettingStore.swift delete mode 100644 EhPanda/View/Setting/EhPandaView.swift create mode 100644 EhPanda/View/Setting/EhSetting/EhSettingReducer.swift rename EhPanda/View/Setting/{Support => EhSetting}/EhSettingView.swift (97%) create mode 100644 EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift rename EhPanda/View/Setting/{ => GeneralSetting}/GeneralSettingView.swift (91%) create mode 100644 EhPanda/View/Setting/Login/LoginReducer.swift rename EhPanda/View/Setting/{Support => Login}/LoginView.swift (87%) create mode 100644 EhPanda/View/Setting/Logs/LogsReducer.swift rename EhPanda/View/Setting/{Support => Logs}/LogsView.swift (92%) create mode 100644 EhPanda/View/Setting/SettingReducer.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index e1304e4e..496e5f3d 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -8,8 +8,8 @@ /* Begin PBXBuildFile section */ AB0929B6277F043D00F107CA /* AccountSettingReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929B5277F043D00F107CA /* AccountSettingReducer.swift */; }; - AB0929BE2780032400F107CA /* EhSettingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929BD2780032400F107CA /* EhSettingStore.swift */; }; - AB0929C027805A8200F107CA /* LoginStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929BF27805A8200F107CA /* LoginStore.swift */; }; + AB0929BE2780032400F107CA /* EhSettingReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929BD2780032400F107CA /* EhSettingReducer.swift */; }; + AB0929C027805A8200F107CA /* LoginReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929BF27805A8200F107CA /* LoginReducer.swift */; }; AB0929C6278160AE00F107CA /* LibraryClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929C5278160AE00F107CA /* LibraryClient.swift */; }; AB0929C82781938A00F107CA /* DFClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929C72781938A00F107CA /* DFClient.swift */; }; AB0929CA278196ED00F107CA /* CookieClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929C9278196ED00F107CA /* CookieClient.swift */; }; @@ -18,7 +18,7 @@ AB0929D02781E1CC00F107CA /* UIApplicationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929CF2781E1CC00F107CA /* UIApplicationClient.swift */; }; AB0929D22781E7D500F107CA /* LoggerClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929D12781E7D500F107CA /* LoggerClient.swift */; }; AB0929D42781EDDC00F107CA /* UserDefaultsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929D32781EDDC00F107CA /* UserDefaultsClient.swift */; }; - AB0929D62782A65F00F107CA /* GeneralSettingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929D52782A65F00F107CA /* GeneralSettingStore.swift */; }; + AB0929D62782A65F00F107CA /* GeneralSettingReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929D52782A65F00F107CA /* GeneralSettingReducer.swift */; }; AB0929D82782A83A00F107CA /* AuthorizationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0929D72782A83A00F107CA /* AuthorizationClient.swift */; }; AB0ABCB526C5406400AD970F /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0ABCB426C5406400AD970F /* LoginView.swift */; }; AB0ABCB726C541A400AD970F /* WaveForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0ABCB626C541A400AD970F /* WaveForm.swift */; }; @@ -167,10 +167,10 @@ AB7BF31D27ABE028001865A3 /* FileManager+ApplicationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF31627ABE028001865A3 /* FileManager+ApplicationSupport.swift */; }; AB7BF31E27ABE028001865A3 /* NSPersistentStoreCoordinator+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF31827ABE028001865A3 /* NSPersistentStoreCoordinator+SQLite.swift */; }; AB7E6B3025D24FE00035CC68 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = AB7E6B3225D24FE00035CC68 /* InfoPlist.strings */; }; - AB86ABF52782DAB300E61E6A /* LogsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86ABF42782DAB300E61E6A /* LogsStore.swift */; }; + AB86ABF52782DAB300E61E6A /* LogsReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86ABF42782DAB300E61E6A /* LogsReducer.swift */; }; AB86ABF72782DDE600E61E6A /* FileClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86ABF62782DDE600E61E6A /* FileClient.swift */; }; - AB86ABF92782EC0D00E61E6A /* EhPandaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86ABF82782EC0D00E61E6A /* EhPandaView.swift */; }; - AB86AC0A2782FAFA00E61E6A /* AppearanceSettingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC092782FAFA00E61E6A /* AppearanceSettingStore.swift */; }; + AB86ABF92782EC0D00E61E6A /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86ABF82782EC0D00E61E6A /* AboutView.swift */; }; + AB86AC0A2782FAFA00E61E6A /* AppearanceSettingReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC092782FAFA00E61E6A /* AppearanceSettingReducer.swift */; }; AB86AC1027831AD100E61E6A /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = AB86AC0F27831AD100E61E6A /* ComposableArchitecture */; }; AB86AC1327856F2700E61E6A /* AppLockStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC1227856F2700E61E6A /* AppLockStore.swift */; }; AB86AC1A2785C2B300E61E6A /* HomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC192785C2B300E61E6A /* HomeStore.swift */; }; @@ -238,7 +238,6 @@ ABD49D60277C7722003D1A07 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D5F277C7722003D1A07 /* TabBarView.swift */; }; ABD49D64277C7AD5003D1A07 /* TabBarStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D63277C7AD5003D1A07 /* TabBarStore.swift */; }; ABD49D67277EAC90003D1A07 /* URLUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D66277EAC90003D1A07 /* URLUtil.swift */; }; - ABD49D6A277EEF73003D1A07 /* SettingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D69277EEF73003D1A07 /* SettingStore.swift */; }; ABD5FDD4263D05110021A4C6 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = ABD5FDD3263D05110021A4C6 /* .swiftlint.yml */; }; ABD7005926B1C31500DC59C9 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = ABD7005826B1C31500DC59C9 /* Kanna */; }; ABD9770E27B65A7300983DE7 /* ListParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD9770D27B65A7300983DE7 /* ListParserTests.swift */; }; @@ -274,6 +273,7 @@ ABF45AF725F3313D00ECB568 /* SettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF45ADD25F3313D00ECB568 /* SettingView.swift */; }; ABF75F3F25A19CD200544D29 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF75F3E25A19CD200544D29 /* User.swift */; }; ABF9720A26DE6E1300118887 /* GalleryDetailWithGreeting.html in Resources */ = {isa = PBXBuildFile; fileRef = ABF9720926DE6E1300118887 /* GalleryDetailWithGreeting.html */; }; + EA2E2E7F2A1F7E500038A261 /* SettingReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2E2E7E2A1F7E500038A261 /* SettingReducer.swift */; }; EAE63E2129E2A6330048C601 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = EAE63E2029E2A6330048C601 /* SwiftyBeaver */; }; /* End PBXBuildFile section */ @@ -310,8 +310,8 @@ /* Begin PBXFileReference section */ AB0929B5277F043D00F107CA /* AccountSettingReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingReducer.swift; sourceTree = ""; }; - AB0929BD2780032400F107CA /* EhSettingStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EhSettingStore.swift; sourceTree = ""; }; - AB0929BF27805A8200F107CA /* LoginStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginStore.swift; sourceTree = ""; }; + AB0929BD2780032400F107CA /* EhSettingReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EhSettingReducer.swift; sourceTree = ""; }; + AB0929BF27805A8200F107CA /* LoginReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginReducer.swift; sourceTree = ""; }; AB0929C5278160AE00F107CA /* LibraryClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryClient.swift; sourceTree = ""; }; AB0929C72781938A00F107CA /* DFClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DFClient.swift; sourceTree = ""; }; AB0929C9278196ED00F107CA /* CookieClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieClient.swift; sourceTree = ""; }; @@ -320,7 +320,7 @@ AB0929CF2781E1CC00F107CA /* UIApplicationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationClient.swift; sourceTree = ""; }; AB0929D12781E7D500F107CA /* LoggerClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerClient.swift; sourceTree = ""; }; AB0929D32781EDDC00F107CA /* UserDefaultsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsClient.swift; sourceTree = ""; }; - AB0929D52782A65F00F107CA /* GeneralSettingStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingStore.swift; sourceTree = ""; }; + AB0929D52782A65F00F107CA /* GeneralSettingReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingReducer.swift; sourceTree = ""; }; AB0929D72782A83A00F107CA /* AuthorizationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationClient.swift; sourceTree = ""; }; AB0ABCB426C5406400AD970F /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; AB0ABCB626C541A400AD970F /* WaveForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveForm.swift; sourceTree = ""; }; @@ -469,10 +469,10 @@ AB7E6B3125D24FE00035CC68 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; AB7E6B3425D24FE40035CC68 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; AB7E6B3525D24FE50035CC68 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - AB86ABF42782DAB300E61E6A /* LogsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsStore.swift; sourceTree = ""; }; + AB86ABF42782DAB300E61E6A /* LogsReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsReducer.swift; sourceTree = ""; }; AB86ABF62782DDE600E61E6A /* FileClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileClient.swift; sourceTree = ""; }; - AB86ABF82782EC0D00E61E6A /* EhPandaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EhPandaView.swift; sourceTree = ""; }; - AB86AC092782FAFA00E61E6A /* AppearanceSettingStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingStore.swift; sourceTree = ""; }; + AB86ABF82782EC0D00E61E6A /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; + AB86AC092782FAFA00E61E6A /* AppearanceSettingReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingReducer.swift; sourceTree = ""; }; AB86AC1227856F2700E61E6A /* AppLockStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockStore.swift; sourceTree = ""; }; AB86AC192785C2B300E61E6A /* HomeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeStore.swift; sourceTree = ""; }; AB8C821826BF801700E8C5E6 /* EhSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EhSetting.swift; sourceTree = ""; }; @@ -542,7 +542,6 @@ ABD49D5F277C7722003D1A07 /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; ABD49D63277C7AD5003D1A07 /* TabBarStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarStore.swift; sourceTree = ""; }; ABD49D66277EAC90003D1A07 /* URLUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLUtil.swift; sourceTree = ""; }; - ABD49D69277EEF73003D1A07 /* SettingStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingStore.swift; sourceTree = ""; }; ABD5FDD3263D05110021A4C6 /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = SOURCE_ROOT; }; ABD9770D27B65A7300983DE7 /* ListParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListParserTests.swift; sourceTree = ""; }; ABD9770F27B65E3400983DE7 /* GalleryDetailParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryDetailParserTests.swift; sourceTree = ""; }; @@ -587,6 +586,7 @@ ABF53F4725A306D200AB5918 /* EhPanda.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EhPanda.entitlements; sourceTree = ""; }; ABF75F3E25A19CD200544D29 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; ABF9720926DE6E1300118887 /* GalleryDetailWithGreeting.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = GalleryDetailWithGreeting.html; sourceTree = ""; }; + EA2E2E7E2A1F7E500038A261 /* SettingReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingReducer.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -630,19 +630,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - AB0929BA277F1B7400F107CA /* DataFlow */ = { - isa = PBXGroup; - children = ( - ABD49D69277EEF73003D1A07 /* SettingStore.swift */, - AB0929D52782A65F00F107CA /* GeneralSettingStore.swift */, - AB86AC092782FAFA00E61E6A /* AppearanceSettingStore.swift */, - AB0929BD2780032400F107CA /* EhSettingStore.swift */, - AB0929BF27805A8200F107CA /* LoginStore.swift */, - AB86ABF42782DAB300E61E6A /* LogsStore.swift */, - ); - path = DataFlow; - sourceTree = ""; - }; AB0929C12781589000F107CA /* Clients */ = { isa = PBXGroup; children = ( @@ -699,17 +686,6 @@ path = Favorites; sourceTree = ""; }; - AB24C560276757940085C33A /* Support */ = { - isa = PBXGroup; - children = ( - ABF45ADA25F3313D00ECB568 /* WebView.swift */, - AB6DE896268822390087C579 /* LogsView.swift */, - ABBC332726BE31AE0084A331 /* EhSettingView.swift */, - AB0ABCB426C5406400AD970F /* LoginView.swift */, - ); - path = Support; - sourceTree = ""; - }; AB24C561276757A30085C33A /* Support */ = { isa = PBXGroup; children = ( @@ -1368,15 +1344,15 @@ ABF45AD725F3313D00ECB568 /* Setting */ = { isa = PBXGroup; children = ( + EA2E2E792A1F78980038A261 /* Logs */, + EA2B9B062A0A8A7C00E7BA07 /* Login */, + EAEC870B2A1F74D500E1A97A /* EhSetting */, + EA2E2E7B2A1F7AEF0038A261 /* GeneralSetting */, EA2B9B042A0A89C900E7BA07 /* AccountSetting */, + EA2E2E7D2A1F7D390038A261 /* AppearanceSetting */, + EA2E2E802A1F7F2A0038A261 /* Components */, ABF45ADD25F3313D00ECB568 /* SettingView.swift */, - ABF45AD825F3313D00ECB568 /* GeneralSettingView.swift */, - ABF45ADC25F3313D00ECB568 /* AppearanceSettingView.swift */, - ABF45ADB25F3313D00ECB568 /* ReadingSettingView.swift */, - ABE1867726A1733000689FDC /* LaboratorySettingView.swift */, - AB86ABF82782EC0D00E61E6A /* EhPandaView.swift */, - AB0929BA277F1B7400F107CA /* DataFlow */, - AB24C560276757940085C33A /* Support */, + EA2E2E7E2A1F7E500038A261 /* SettingReducer.swift */, ); path = Setting; sourceTree = ""; @@ -1390,6 +1366,62 @@ path = AccountSetting; sourceTree = ""; }; + EA2B9B062A0A8A7C00E7BA07 /* Login */ = { + isa = PBXGroup; + children = ( + AB0ABCB426C5406400AD970F /* LoginView.swift */, + AB0929BF27805A8200F107CA /* LoginReducer.swift */, + ); + path = Login; + sourceTree = ""; + }; + EA2E2E792A1F78980038A261 /* Logs */ = { + isa = PBXGroup; + children = ( + AB6DE896268822390087C579 /* LogsView.swift */, + AB86ABF42782DAB300E61E6A /* LogsReducer.swift */, + ); + path = Logs; + sourceTree = ""; + }; + EA2E2E7B2A1F7AEF0038A261 /* GeneralSetting */ = { + isa = PBXGroup; + children = ( + ABF45AD825F3313D00ECB568 /* GeneralSettingView.swift */, + AB0929D52782A65F00F107CA /* GeneralSettingReducer.swift */, + ); + path = GeneralSetting; + sourceTree = ""; + }; + EA2E2E7D2A1F7D390038A261 /* AppearanceSetting */ = { + isa = PBXGroup; + children = ( + ABF45ADC25F3313D00ECB568 /* AppearanceSettingView.swift */, + AB86AC092782FAFA00E61E6A /* AppearanceSettingReducer.swift */, + ); + path = AppearanceSetting; + sourceTree = ""; + }; + EA2E2E802A1F7F2A0038A261 /* Components */ = { + isa = PBXGroup; + children = ( + ABF45ADB25F3313D00ECB568 /* ReadingSettingView.swift */, + ABE1867726A1733000689FDC /* LaboratorySettingView.swift */, + AB86ABF82782EC0D00E61E6A /* AboutView.swift */, + ABF45ADA25F3313D00ECB568 /* WebView.swift */, + ); + path = Components; + sourceTree = ""; + }; + EAEC870B2A1F74D500E1A97A /* EhSetting */ = { + isa = PBXGroup; + children = ( + ABBC332726BE31AE0084A331 /* EhSettingView.swift */, + AB0929BD2780032400F107CA /* EhSettingReducer.swift */, + ); + path = EhSetting; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1684,6 +1716,7 @@ AB0929CA278196ED00F107CA /* CookieClient.swift in Sources */, AB7BF2FB27ABCA3A001865A3 /* MigrationView.swift in Sources */, AB7BF31C27ABE028001865A3 /* NSManagedObjectModel+Compatible.swift in Sources */, + EA2E2E7F2A1F7E500038A261 /* SettingReducer.swift in Sources */, AB7BF31B27ABE028001865A3 /* NSManagedObjectModel+Resource.swift in Sources */, ABBC332826BE31AE0084A331 /* EhSettingView.swift in Sources */, AB7BF2C827A968F7001865A3 /* GalleryComment.swift in Sources */, @@ -1695,9 +1728,9 @@ AB63EADD2699AC9100090535 /* AppEnvMO+CoreDataClass.swift in Sources */, AB7BF2C027A9669A001865A3 /* TagNamespace.swift in Sources */, ABCA93BE26918DE100A98BC6 /* Persistence.swift in Sources */, - AB86ABF92782EC0D00E61E6A /* EhPandaView.swift in Sources */, + AB86ABF92782EC0D00E61E6A /* AboutView.swift in Sources */, AB7BF2BA27A96562001865A3 /* Gallery.swift in Sources */, - AB0929BE2780032400F107CA /* EhSettingStore.swift in Sources */, + AB0929BE2780032400F107CA /* EhSettingReducer.swift in Sources */, ABC8356127B357C50091DCDB /* GestureHandler.swift in Sources */, AB0929D42781EDDC00F107CA /* UserDefaultsClient.swift in Sources */, AB0929D82782A83A00F107CA /* AuthorizationClient.swift in Sources */, @@ -1724,7 +1757,6 @@ ABF45AEB25F3313D00ECB568 /* GalleryDetailCell.swift in Sources */, AB69CB8026B3DABC00699359 /* AdvancedList.swift in Sources */, ABC3C7892593699B00E0C11B /* Defaults.swift in Sources */, - ABD49D6A277EEF73003D1A07 /* SettingStore.swift in Sources */, AB8C821926BF801700E8C5E6 /* EhSetting.swift in Sources */, AB86AC1327856F2700E61E6A /* AppLockStore.swift in Sources */, AB58A5AC2776B2BC00C0D285 /* AppDelegateStore.swift in Sources */, @@ -1739,7 +1771,7 @@ AB0CFBCB27C0B07F004BD372 /* TagSuggestion.swift in Sources */, AB7BF30727ABDFF1001865A3 /* CoreDataMigrator.swift in Sources */, ABC3C78F2593699B00E0C11B /* ViewModifiers.swift in Sources */, - AB86ABF52782DAB300E61E6A /* LogsStore.swift in Sources */, + AB86ABF52782DAB300E61E6A /* LogsReducer.swift in Sources */, AB7BF2AB27A642FB001865A3 /* BrowsingCountry.swift in Sources */, ABD49D60277C7722003D1A07 /* TabBarView.swift in Sources */, AB26F59027ABF21000AB3468 /* Model5toModel6.xcmappingmodel in Sources */, @@ -1757,7 +1789,7 @@ ABC681F326898D46007BBD69 /* Model.xcdatamodeld in Sources */, ABBB266627977C2A007B6149 /* ArchivesStore.swift in Sources */, ABBB2640279417EC007B6149 /* CommentsStore.swift in Sources */, - AB0929C027805A8200F107CA /* LoginStore.swift in Sources */, + AB0929C027805A8200F107CA /* LoginReducer.swift in Sources */, ABBB2631278E6EF3007B6149 /* SearchView.swift in Sources */, AB706F92278A6E8C0025A48A /* WatchedStore.swift in Sources */, AB706F80278981370025A48A /* AlertKit_Extension.swift in Sources */, @@ -1791,11 +1823,11 @@ ABBB263A2792588F007B6149 /* TTProgressHUD_Extension.swift in Sources */, AB7BF2D627AA3F4C001865A3 /* FileUtil.swift in Sources */, AB0ABCB726C541A400AD970F /* WaveForm.swift in Sources */, - AB0929D62782A65F00F107CA /* GeneralSettingStore.swift in Sources */, + AB0929D62782A65F00F107CA /* GeneralSettingReducer.swift in Sources */, AB706FA3278BCF2F0025A48A /* DetailStore.swift in Sources */, ABBCCC9026C95F6E007D8A36 /* GalleryInfosView.swift in Sources */, AB7BF2A927A63C89001865A3 /* Language.swift in Sources */, - AB86AC0A2782FAFA00E61E6A /* AppearanceSettingStore.swift in Sources */, + AB86AC0A2782FAFA00E61E6A /* AppearanceSettingReducer.swift in Sources */, AB7BF2C427A9683F001865A3 /* GalleryArchive.swift in Sources */, ABF45AF525F3313D00ECB568 /* ReadingSettingView.swift in Sources */, AB0CFBD727C3B2D0004BD372 /* TagDetailView.swift in Sources */, diff --git a/EhPanda/App/Tools/Parser.swift b/EhPanda/App/Tools/Parser.swift index 9b02dc0b..fe1081cf 100644 --- a/EhPanda/App/Tools/Parser.swift +++ b/EhPanda/App/Tools/Parser.swift @@ -1500,7 +1500,7 @@ extension Parser { } // MARK: Profile - static func parseProfileIndex(doc: HTMLDocument) throws -> (Int?, Bool) { + static func parseProfileIndex(doc: HTMLDocument) throws -> VerifyEhProfileResponse { var profileNotFound = true var profileValue: Int? @@ -1515,7 +1515,7 @@ extension Parser { profileValue = Int(link["value"] ?? "") } - return (profileValue, profileNotFound) + return .init(profileValue: profileValue, isProfileNotFound: profileNotFound) } // MARK: CommentContent diff --git a/EhPanda/App/de.lproj/Localizable.strings b/EhPanda/App/de.lproj/Localizable.strings index a9ed2e2b..360ea078 100644 --- a/EhPanda/App/de.lproj/Localizable.strings +++ b/EhPanda/App/de.lproj/Localizable.strings @@ -159,7 +159,7 @@ "enum.setting_state_route.value.appearance" = "Oberfläche"; "enum.setting_state_route.value.reading" = "Am Lesen"; "enum.setting_state_route.value.laboratory" = "Experimentelles"; -"enum.setting_state_route.value.ehPanda" = "Über EhPanda"; +"enum.setting_state_route.value.about" = "Über EhPanda"; // MARK: AccountSettingView "account_setting_view.title.account" = "Konto"; @@ -254,15 +254,15 @@ "laboratory_setting_view.title.laboratory" = "Experimentelles"; "laboratory_setting_view.title.bypasses_SNI_filtering" = "SNI Filter umgehen"; -// MARK: EhPandaView -"ehPanda_view.title.ehPanda" = "EhPanda"; -"ehPanda_view.button.website" = "Website"; -"ehPanda_view.button.altStore_source" = "AltStore Quelle"; -"ehPanda_view.title.version" = "Version"; -"ehPanda_view.section.title.special_thanks" = "Special thanks"; -"ehPanda_view.section.title.code_level_contributors" = "Code-level contributors"; -"ehPanda_view.section.title.translation_contributors" = "Translation contributors"; -"ehPanda_view.section.title.acknowledgements" = "OK"; +// MARK: AboutView +"about_view.title.ehPanda" = "EhPanda"; +"about_view.button.website" = "Website"; +"about_view.button.altStore_source" = "AltStore Quelle"; +"about_view.title.version" = "Version"; +"about_view.section.title.special_thanks" = "Special thanks"; +"about_view.section.title.code_level_contributors" = "Code-level contributors"; +"about_view.section.title.translation_contributors" = "Translation contributors"; +"about_view.section.title.acknowledgements" = "OK"; // MARK: DetailView "detail_view.button.read" = "Lesen"; diff --git a/EhPanda/App/en.lproj/Constant.strings b/EhPanda/App/en.lproj/Constant.strings index e959b5c2..8418542a 100644 --- a/EhPanda/App/en.lproj/Constant.strings +++ b/EhPanda/App/en.lproj/Constant.strings @@ -12,87 +12,87 @@ "website.response.invalid_resolution" = "The requested gallery cannot be downloaded with the selected resolution."; "website.response.gallery_unavailable" = "This gallery has been removed or is unavailable."; -// MARK: EhPanda -"EhPanda.copyright" = "Copyright © 2023 EhPanda Team"; +// MARK: App +"app.copyright" = "Copyright © 2023 EhPanda Team"; -// Contacts -"EhPanda.contacts.link.website" = "https://ehpanda.app"; -"EhPanda.contacts.link.gitHub" = "https://github.com/EhPanda-Team/EhPanda"; -"EhPanda.contacts.link.discord" = "https://discord.gg/BSBE9FCBTq"; -"EhPanda.contacts.link.telegram" = "https://t.me/ehpanda"; -"EhPanda.contacts.link.altStore" = "altstore://source?url=https://github.com/EhPanda-Team/EhPanda/raw/main/AltStore.json"; -"EhPanda.contacts.text.gitHub" = "GitHub"; -"EhPanda.contacts.text.discord" = "Discord"; -"EhPanda.contacts.text.telegram" = "Telegram"; +// Contact +"app.contact.link.website" = "https://ehpanda.app"; +"app.contact.link.gitHub" = "https://github.com/EhPanda-Team/EhPanda"; +"app.contact.link.discord" = "https://discord.gg/BSBE9FCBTq"; +"app.contact.link.telegram" = "https://t.me/ehpanda"; +"app.contact.link.altStore" = "altstore://source?url=https://github.com/EhPanda-Team/EhPanda/raw/main/AltStore.json"; +"app.contact.text.gitHub" = "GitHub"; +"app.contact.text.discord" = "Discord"; +"app.contact.text.telegram" = "Telegram"; // Special thanks -"EhPanda.special_thanks.link.taylorlannister" = "https://github.com/taylorlannister"; -"EhPanda.special_thanks.link.luminescent_yq" = ""; -"EhPanda.special_thanks.link.caxerx" = "https://github.com/caxerx"; -"EhPanda.special_thanks.link.honjow" = "https://github.com/honjow"; -"EhPanda.special_thanks.text.taylorlannister" = "taylorlannister"; -"EhPanda.special_thanks.text.luminescent_yq" = "Luminescent_yq"; -"EhPanda.special_thanks.text.caxerx" = "caxerx"; -"EhPanda.special_thanks.text.honjow" = "honjow"; +"app.special_thanks.link.taylorlannister" = "https://github.com/taylorlannister"; +"app.special_thanks.link.luminescent_yq" = ""; +"app.special_thanks.link.caxerx" = "https://github.com/caxerx"; +"app.special_thanks.link.honjow" = "https://github.com/honjow"; +"app.special_thanks.text.taylorlannister" = "taylorlannister"; +"app.special_thanks.text.luminescent_yq" = "Luminescent_yq"; +"app.special_thanks.text.caxerx" = "caxerx"; +"app.special_thanks.text.honjow" = "honjow"; -// Code level contributors -"EhPanda.code_level_contributors.link.tatsuz0u" = "https://github.com/tatsuz0u"; -"EhPanda.code_level_contributors.link.chihchy" = "https://github.com/chihchy"; -"EhPanda.code_level_contributors.link.xioxin" = "https://github.com/xioxin"; -"EhPanda.code_level_contributors.link.leng-yue" = "https://github.com/leng-yue"; -"EhPanda.code_level_contributors.link.ethanChinCN" = "https://github.com/EthanChinCN"; -"EhPanda.code_level_contributors.text.tatsuz0u" = "Tatsuzo Araki"; -"EhPanda.code_level_contributors.text.chihchy" = "Chihchy"; -"EhPanda.code_level_contributors.text.xioxin" = "xioxin"; -"EhPanda.code_level_contributors.text.leng-yue" = "LengYue"; -"EhPanda.code_level_contributors.text.ethanChinCN" = "Ethan Chin"; +// Code level contributor +"app.code_level_contributor.link.tatsuz0u" = "https://github.com/tatsuz0u"; +"app.code_level_contributor.link.chihchy" = "https://github.com/chihchy"; +"app.code_level_contributor.link.xioxin" = "https://github.com/xioxin"; +"app.code_level_contributor.link.leng-yue" = "https://github.com/leng-yue"; +"app.code_level_contributor.link.ethanChinCN" = "https://github.com/EthanChinCN"; +"app.code_level_contributor.text.tatsuz0u" = "Tatsuzo Araki"; +"app.code_level_contributor.text.chihchy" = "Chihchy"; +"app.code_level_contributor.text.xioxin" = "xioxin"; +"app.code_level_contributor.text.leng-yue" = "LengYue"; +"app.code_level_contributor.text.ethanChinCN" = "Ethan Chin"; -// Translation contributors -"EhPanda.translation_contributors.link.tatsuz0u" = "https://github.com/tatsuz0u"; -"EhPanda.translation_contributors.link.nebulosa-cat" = "https://github.com/Nebulosa-Cat"; -"EhPanda.translation_contributors.link.paulHaeussler" = "https://github.com/PaulHaeussler"; -"EhPanda.translation_contributors.link.caxerx" = "https://github.com/caxerx"; -"EhPanda.translation_contributors.link.nyaanim" = "https://github.com/nyaanim"; -"EhPanda.translation_contributors.text.tatsuz0u" = "Tatsuzo Araki"; -"EhPanda.translation_contributors.text.nebulosa-cat" = "雲豹 ΦωΦ"; -"EhPanda.translation_contributors.text.paulHaeussler" = "PaulHaeussler"; -"EhPanda.translation_contributors.text.caxerx" = "caxerx"; -"EhPanda.translation_contributors.text.nyaanim" = "nyaanim"; +// Translation contributor +"app.translation_contributor.link.tatsuz0u" = "https://github.com/tatsuz0u"; +"app.translation_contributor.link.nebulosa-cat" = "https://github.com/Nebulosa-Cat"; +"app.translation_contributor.link.paulHaeussler" = "https://github.com/PaulHaeussler"; +"app.translation_contributor.link.caxerx" = "https://github.com/caxerx"; +"app.translation_contributor.link.nyaanim" = "https://github.com/nyaanim"; +"app.translation_contributor.text.tatsuz0u" = "Tatsuzo Araki"; +"app.translation_contributor.text.nebulosa-cat" = "雲豹 ΦωΦ"; +"app.translation_contributor.text.paulHaeussler" = "PaulHaeussler"; +"app.translation_contributor.text.caxerx" = "caxerx"; +"app.translation_contributor.text.nyaanim" = "nyaanim"; -// Acknowledgements links -"EhPanda.acknowledgements.link.kanna" = "https://github.com/tid-kijyun/Kanna"; -"EhPanda.acknowledgements.link.swiftGen" = "https://github.com/SwiftGen/SwiftGen"; -"EhPanda.acknowledgements.link.alertKit" = "https://github.com/rebeloper/AlertKit"; -"EhPanda.acknowledgements.link.colorful" = "https://github.com/Co2333/Colorful"; -"EhPanda.acknowledgements.link.filePicker" = "https://github.com/markrenaud/FilePicker"; -"EhPanda.acknowledgements.link.kingfisher" = "https://github.com/onevcat/Kingfisher"; -"EhPanda.acknowledgements.link.swiftUIPager" = "https://github.com/fermoya/SwiftUIPager"; -"EhPanda.acknowledgements.link.swiftyBeaver" = "https://github.com/SwiftyBeaver/SwiftyBeaver"; -"EhPanda.acknowledgements.link.waterfallGrid" = "https://github.com/paololeonardi/WaterfallGrid"; -"EhPanda.acknowledgements.link.swiftyOpenCC" = "https://github.com/ddddxxx/SwiftyOpenCC"; -"EhPanda.acknowledgements.link.uiImageColors" = "https://github.com/jathu/UIImageColors"; -"EhPanda.acknowledgements.link.sfSafeSymbols" = "https://github.com/SFSafeSymbols/SFSafeSymbols"; -"EhPanda.acknowledgements.link.ttProgressHUD" = "https://github.com/honkmaster/TTProgressHUD"; -"EhPanda.acknowledgements.link.swiftUINavigation" = "https://github.com/pointfreeco/swiftui-navigation"; -"EhPanda.acknowledgements.link.swiftCommonMark" = "https://github.com/gonzalezreal/SwiftCommonMark"; -"EhPanda.acknowledgements.link.ehTagTranslationDatabase" = "https://github.com/EhTagTranslation/Database"; -"EhPanda.acknowledgements.link.tca" = "https://github.com/pointfreeco/swift-composable-architecture"; +// Acknowledgement link +"app.acknowledgement.link.kanna" = "https://github.com/tid-kijyun/Kanna"; +"app.acknowledgement.link.swiftGen" = "https://github.com/SwiftGen/SwiftGen"; +"app.acknowledgement.link.alertKit" = "https://github.com/rebeloper/AlertKit"; +"app.acknowledgement.link.colorful" = "https://github.com/Co2333/Colorful"; +"app.acknowledgement.link.filePicker" = "https://github.com/markrenaud/FilePicker"; +"app.acknowledgement.link.kingfisher" = "https://github.com/onevcat/Kingfisher"; +"app.acknowledgement.link.swiftUIPager" = "https://github.com/fermoya/SwiftUIPager"; +"app.acknowledgement.link.swiftyBeaver" = "https://github.com/SwiftyBeaver/SwiftyBeaver"; +"app.acknowledgement.link.waterfallGrid" = "https://github.com/paololeonardi/WaterfallGrid"; +"app.acknowledgement.link.swiftyOpenCC" = "https://github.com/ddddxxx/SwiftyOpenCC"; +"app.acknowledgement.link.uiImageColors" = "https://github.com/jathu/UIImageColors"; +"app.acknowledgement.link.sfSafeSymbols" = "https://github.com/SFSafeSymbols/SFSafeSymbols"; +"app.acknowledgement.link.ttProgressHUD" = "https://github.com/honkmaster/TTProgressHUD"; +"app.acknowledgement.link.swiftUINavigation" = "https://github.com/pointfreeco/swiftui-navigation"; +"app.acknowledgement.link.swiftCommonMark" = "https://github.com/gonzalezreal/SwiftCommonMark"; +"app.acknowledgement.link.ehTagTranslationDatabase" = "https://github.com/EhTagTranslation/Database"; +"app.acknowledgement.link.tca" = "https://github.com/pointfreeco/swift-composable-architecture"; -// Acknowledgements texts -"EhPanda.acknowledgements.text.kanna" = "Kanna"; -"EhPanda.acknowledgements.text.swiftGen" = "SwiftGen"; -"EhPanda.acknowledgements.text.alertKit" = "AlertKit"; -"EhPanda.acknowledgements.text.colorful" = "Colorful"; -"EhPanda.acknowledgements.text.filePicker" = "FilePicker"; -"EhPanda.acknowledgements.text.kingfisher" = "Kingfisher"; -"EhPanda.acknowledgements.text.swiftUIPager" = "SwiftUIPager"; -"EhPanda.acknowledgements.text.swiftyBeaver" = "SwiftyBeaver"; -"EhPanda.acknowledgements.text.waterfallGrid" = "WaterfallGrid"; -"EhPanda.acknowledgements.text.swiftyOpenCC" = "SwiftyOpenCC"; -"EhPanda.acknowledgements.text.uiImageColors" = "UIImageColors"; -"EhPanda.acknowledgements.text.sfSafeSymbols" = "SFSafeSymbols"; -"EhPanda.acknowledgements.text.ttProgressHUD" = "TTProgressHUD"; -"EhPanda.acknowledgements.text.swiftUINavigation" = "SwiftUI Navigation"; -"EhPanda.acknowledgements.text.swiftCommonMark" = "SwiftCommonMark"; -"EhPanda.acknowledgements.text.ehTagTranslationDatabase" = "EhTagTranslation/Database"; -"EhPanda.acknowledgements.text.tca" = "The Composable Architecture"; +// Acknowledgement text +"app.acknowledgement.text.kanna" = "Kanna"; +"app.acknowledgement.text.swiftGen" = "SwiftGen"; +"app.acknowledgement.text.alertKit" = "AlertKit"; +"app.acknowledgement.text.colorful" = "Colorful"; +"app.acknowledgement.text.filePicker" = "FilePicker"; +"app.acknowledgement.text.kingfisher" = "Kingfisher"; +"app.acknowledgement.text.swiftUIPager" = "SwiftUIPager"; +"app.acknowledgement.text.swiftyBeaver" = "SwiftyBeaver"; +"app.acknowledgement.text.waterfallGrid" = "WaterfallGrid"; +"app.acknowledgement.text.swiftyOpenCC" = "SwiftyOpenCC"; +"app.acknowledgement.text.uiImageColors" = "UIImageColors"; +"app.acknowledgement.text.sfSafeSymbols" = "SFSafeSymbols"; +"app.acknowledgement.text.ttProgressHUD" = "TTProgressHUD"; +"app.acknowledgement.text.swiftUINavigation" = "SwiftUI Navigation"; +"app.acknowledgement.text.swiftCommonMark" = "SwiftCommonMark"; +"app.acknowledgement.text.ehTagTranslationDatabase" = "EhTagTranslation/Database"; +"app.acknowledgement.text.tca" = "The Composable Architecture"; diff --git a/EhPanda/App/en.lproj/Localizable.strings b/EhPanda/App/en.lproj/Localizable.strings index 7b93af03..0cf29b6d 100644 --- a/EhPanda/App/en.lproj/Localizable.strings +++ b/EhPanda/App/en.lproj/Localizable.strings @@ -159,7 +159,7 @@ "enum.setting_state_route.value.appearance" = "Appearance"; "enum.setting_state_route.value.reading" = "Reading"; "enum.setting_state_route.value.laboratory" = "Laboratory"; -"enum.setting_state_route.value.ehPanda" = "About EhPanda"; +"enum.setting_state_route.value.about" = "About EhPanda"; // MARK: AccountSettingView "account_setting_view.title.account" = "Account"; @@ -254,15 +254,15 @@ "laboratory_setting_view.title.laboratory" = "Laboratory"; "laboratory_setting_view.title.bypasses_SNI_filtering" = "Bypasses SNI Filtering"; -// MARK: EhPandaView -"ehPanda_view.title.ehPanda" = "EhPanda"; -"ehPanda_view.button.website" = "Website"; -"ehPanda_view.button.altStore_source" = "AltStore source"; -"ehPanda_view.title.version" = "Version"; -"ehPanda_view.section.title.special_thanks" = "Special thanks"; -"ehPanda_view.section.title.code_level_contributors" = "Code-level contributors"; -"ehPanda_view.section.title.translation_contributors" = "Translation contributors"; -"ehPanda_view.section.title.acknowledgements" = "Acknowledgements"; +// MARK: AboutView +"about_view.title.ehPanda" = "EhPanda"; +"about_view.button.website" = "Website"; +"about_view.button.altStore_source" = "AltStore source"; +"about_view.title.version" = "Version"; +"about_view.section.title.special_thanks" = "Special thanks"; +"about_view.section.title.code_level_contributors" = "Code-level contributors"; +"about_view.section.title.translation_contributors" = "Translation contributors"; +"about_view.section.title.acknowledgements" = "Acknowledgements"; // MARK: DetailView "detail_view.button.read" = "Read"; diff --git a/EhPanda/App/ja.lproj/Localizable.strings b/EhPanda/App/ja.lproj/Localizable.strings index 15e26795..b39bf2d2 100644 --- a/EhPanda/App/ja.lproj/Localizable.strings +++ b/EhPanda/App/ja.lproj/Localizable.strings @@ -159,7 +159,7 @@ "enum.setting_state_route.value.appearance" = "外観"; "enum.setting_state_route.value.reading" = "閲覧"; "enum.setting_state_route.value.laboratory" = "ラボ"; -"enum.setting_state_route.value.ehPanda" = "EhPanda について"; +"enum.setting_state_route.value.about" = "EhPanda について"; // MARK: AccountSettingView "account_setting_view.title.account" = "アカウント"; @@ -254,15 +254,15 @@ "laboratory_setting_view.title.laboratory" = "ラボ"; "laboratory_setting_view.title.bypasses_SNI_filtering" = "SNI フィルタリング回避"; -// MARK: EhPandaView -"ehPanda_view.title.ehPanda" = "EhPanda"; -"ehPanda_view.button.website" = "ウェブサイト"; -"ehPanda_view.button.altStore_source" = "AltStore ソース"; -"ehPanda_view.title.version" = "バージョン"; -"ehPanda_view.section.title.special_thanks" = "特別な感謝"; -"ehPanda_view.section.title.code_level_contributors" = "コードレベル貢献者"; -"ehPanda_view.section.title.translation_contributors" = "翻訳貢献者"; -"ehPanda_view.section.title.acknowledgements" = "謝辞"; +// MARK: AboutView +"about_view.title.ehPanda" = "EhPanda"; +"about_view.button.website" = "ウェブサイト"; +"about_view.button.altStore_source" = "AltStore ソース"; +"about_view.title.version" = "バージョン"; +"about_view.section.title.special_thanks" = "特別な感謝"; +"about_view.section.title.code_level_contributors" = "コードレベル貢献者"; +"about_view.section.title.translation_contributors" = "翻訳貢献者"; +"about_view.section.title.acknowledgements" = "謝辞"; // MARK: DetailView "detail_view.button.read" = "閲覧"; diff --git a/EhPanda/App/ko.lproj/Localizable.strings b/EhPanda/App/ko.lproj/Localizable.strings index 225e822a..ec09077d 100644 --- a/EhPanda/App/ko.lproj/Localizable.strings +++ b/EhPanda/App/ko.lproj/Localizable.strings @@ -159,7 +159,7 @@ "enum.setting_state_route.value.appearance" = "외관"; "enum.setting_state_route.value.reading" = "읽기"; "enum.setting_state_route.value.laboratory" = "실험실"; -"enum.setting_state_route.value.ehPanda" = "EhPanda 정보"; +"enum.setting_state_route.value.about" = "EhPanda 정보"; // MARK: AccountSettingView "account_setting_view.title.account" = "계정"; @@ -254,15 +254,15 @@ "laboratory_setting_view.title.laboratory" = "실험실"; "laboratory_setting_view.title.bypasses_SNI_filtering" = "SNI 차단 우회"; -// MARK: EhPandaView -"ehPanda_view.title.ehPanda" = "EhPanda"; -"ehPanda_view.button.website" = "웹사이트"; -"ehPanda_view.button.altStore_source" = "AltStore 소스"; -"ehPanda_view.title.version" = "버전"; -"ehPanda_view.section.title.special_thanks" = "Special thanks"; -"ehPanda_view.section.title.code_level_contributors" = "Code-level contributors"; -"ehPanda_view.section.title.translation_contributors" = "Translation contributors"; -"ehPanda_view.section.title.acknowledgements" = "도움을 주신 분들"; +// MARK: AboutView +"about_view.title.ehPanda" = "EhPanda"; +"about_view.button.website" = "웹사이트"; +"about_view.button.altStore_source" = "AltStore 소스"; +"about_view.title.version" = "버전"; +"about_view.section.title.special_thanks" = "Special thanks"; +"about_view.section.title.code_level_contributors" = "Code-level contributors"; +"about_view.section.title.translation_contributors" = "Translation contributors"; +"about_view.section.title.acknowledgements" = "도움을 주신 분들"; // MARK: DetailView "detail_view.button.read" = "읽기"; diff --git a/EhPanda/App/zh-Hans.lproj/Localizable.strings b/EhPanda/App/zh-Hans.lproj/Localizable.strings index d75212cd..57f0bb98 100644 --- a/EhPanda/App/zh-Hans.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hans.lproj/Localizable.strings @@ -159,7 +159,7 @@ "enum.setting_state_route.value.appearance" = "外观"; "enum.setting_state_route.value.reading" = "阅读"; "enum.setting_state_route.value.laboratory" = "实验室"; -"enum.setting_state_route.value.ehPanda" = "关于 EhPanda"; +"enum.setting_state_route.value.about" = "关于 EhPanda"; // MARK: AccountSettingView "account_setting_view.title.account" = "账户"; @@ -254,15 +254,15 @@ "laboratory_setting_view.title.laboratory" = "实验室"; "laboratory_setting_view.title.bypasses_SNI_filtering" = "域前置绕过 SNI 阻断"; -// MARK: EhPandaView -"ehPanda_view.title.ehPanda" = "EhPanda"; -"ehPanda_view.button.website" = "网站"; -"ehPanda_view.button.altStore_source" = "AltStore 源"; -"ehPanda_view.title.version" = "版本"; -"ehPanda_view.section.title.special_thanks" = "特别致谢"; -"ehPanda_view.section.title.code_level_contributors" = "代码级贡献者"; -"ehPanda_view.section.title.translation_contributors" = "翻译贡献者"; -"ehPanda_view.section.title.acknowledgements" = "致谢"; +// MARK: AboutView +"about_view.title.ehPanda" = "EhPanda"; +"about_view.button.website" = "网站"; +"about_view.button.altStore_source" = "AltStore 源"; +"about_view.title.version" = "版本"; +"about_view.section.title.special_thanks" = "特别致谢"; +"about_view.section.title.code_level_contributors" = "代码级贡献者"; +"about_view.section.title.translation_contributors" = "翻译贡献者"; +"about_view.section.title.acknowledgements" = "致谢"; // MARK: DetailView "detail_view.button.read" = "阅读"; diff --git a/EhPanda/App/zh-Hant-HK.lproj/Localizable.strings b/EhPanda/App/zh-Hant-HK.lproj/Localizable.strings index 745e10de..911614f2 100644 --- a/EhPanda/App/zh-Hant-HK.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hant-HK.lproj/Localizable.strings @@ -159,7 +159,7 @@ "enum.setting_state_route.value.appearance" = "外觀"; "enum.setting_state_route.value.reading" = "閱讀"; "enum.setting_state_route.value.laboratory" = "實驗室"; -"enum.setting_state_route.value.ehPanda" = "關於 EhPanda"; +"enum.setting_state_route.value.about" = "關於 EhPanda"; // MARK: AccountSettingView "account_setting_view.title.account" = "帳號"; @@ -254,15 +254,15 @@ "laboratory_setting_view.title.laboratory" = "實驗室"; "laboratory_setting_view.title.bypasses_SNI_filtering" = "域前置繞過 SNI 阻斷"; -// MARK: EhPandaView -"ehPanda_view.title.ehPanda" = "EhPanda"; -"ehPanda_view.button.website" = "網站"; -"ehPanda_view.button.altStore_source" = "AltStore 源"; -"ehPanda_view.title.version" = "版本"; -"ehPanda_view.section.title.special_thanks" = "Special thanks"; -"ehPanda_view.section.title.code_level_contributors" = "Code-level contributors"; -"ehPanda_view.section.title.translation_contributors" = "Translation contributors"; -"ehPanda_view.section.title.acknowledgements" = "致謝"; +// MARK: AboutView +"about_view.title.ehPanda" = "EhPanda"; +"about_view.button.website" = "網站"; +"about_view.button.altStore_source" = "AltStore 源"; +"about_view.title.version" = "版本"; +"about_view.section.title.special_thanks" = "Special thanks"; +"about_view.section.title.code_level_contributors" = "Code-level contributors"; +"about_view.section.title.translation_contributors" = "Translation contributors"; +"about_view.section.title.acknowledgements" = "致謝"; // MARK: DetailView "detail_view.button.read" = "閱讀"; diff --git a/EhPanda/App/zh-Hant-TW.lproj/Localizable.strings b/EhPanda/App/zh-Hant-TW.lproj/Localizable.strings index d1b4746b..43113e41 100644 --- a/EhPanda/App/zh-Hant-TW.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hant-TW.lproj/Localizable.strings @@ -159,7 +159,7 @@ "enum.setting_state_route.value.appearance" = "外觀"; "enum.setting_state_route.value.reading" = "閱讀"; "enum.setting_state_route.value.laboratory" = "實驗性功能"; -"enum.setting_state_route.value.ehPanda" = "關於 EhPanda"; +"enum.setting_state_route.value.about" = "關於 EhPanda"; // MARK: AccountSettingView "account_setting_view.title.account" = "帳號設定"; @@ -254,15 +254,15 @@ "laboratory_setting_view.title.laboratory" = "實驗性功能"; "laboratory_setting_view.title.bypasses_SNI_filtering" = "繞過 SNI 過濾"; -// MARK: EhPandaView -"ehPanda_view.title.ehPanda" = "EhPanda"; -"ehPanda_view.button.website" = "官方網站"; -"ehPanda_view.button.altStore_source" = "AltStore source"; -"ehPanda_view.title.version" = "版本"; -"ehPanda_view.section.title.special_thanks" = "特別銘謝"; -"ehPanda_view.section.title.code_level_contributors" = "程式碼貢獻者"; -"ehPanda_view.section.title.translation_contributors" = "翻譯貢獻者"; -"ehPanda_view.section.title.acknowledgements" = "致謝"; +// MARK: AboutView +"about_view.title.ehPanda" = "EhPanda"; +"about_view.button.website" = "官方網站"; +"about_view.button.altStore_source" = "AltStore source"; +"about_view.title.version" = "版本"; +"about_view.section.title.special_thanks" = "特別銘謝"; +"about_view.section.title.code_level_contributors" = "程式碼貢獻者"; +"about_view.section.title.translation_contributors" = "翻譯貢獻者"; +"about_view.section.title.acknowledgements" = "致謝"; // MARK: DetailView "detail_view.button.read" = "閱讀"; diff --git a/EhPanda/App/zh-Hant.lproj/Localizable.strings b/EhPanda/App/zh-Hant.lproj/Localizable.strings index 2b650f26..4d093dd6 100644 --- a/EhPanda/App/zh-Hant.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hant.lproj/Localizable.strings @@ -159,7 +159,7 @@ "enum.setting_state_route.value.appearance" = "外觀"; "enum.setting_state_route.value.reading" = "閱讀"; "enum.setting_state_route.value.laboratory" = "實驗性功能"; -"enum.setting_state_route.value.ehPanda" = "關於 EhPanda"; +"enum.setting_state_route.value.about" = "關於 EhPanda"; // MARK: AccountSettingView "account_setting_view.title.account" = "帳號設定"; @@ -254,15 +254,15 @@ "laboratory_setting_view.title.laboratory" = "實驗性功能"; "laboratory_setting_view.title.bypasses_SNI_filtering" = "繞過 SNI 過濾"; -// MARK: EhPandaView -"ehPanda_view.title.ehPanda" = "EhPanda"; -"ehPanda_view.button.website" = "官方網站"; -"ehPanda_view.button.altStore_source" = "AltStore source"; -"ehPanda_view.title.version" = "版本"; -"ehPanda_view.section.title.special_thanks" = "特別銘謝"; -"ehPanda_view.section.title.code_level_contributors" = "程式碼貢獻者"; -"ehPanda_view.section.title.translation_contributors" = "翻譯貢獻者"; -"ehPanda_view.section.title.acknowledgements" = "致謝"; +// MARK: AboutView +"about_view.title.ehPanda" = "EhPanda"; +"about_view.button.website" = "官方網站"; +"about_view.button.altStore_source" = "AltStore source"; +"about_view.title.version" = "版本"; +"about_view.section.title.special_thanks" = "特別銘謝"; +"about_view.section.title.code_level_contributors" = "程式碼貢獻者"; +"about_view.section.title.translation_contributors" = "翻譯貢獻者"; +"about_view.section.title.acknowledgements" = "致謝"; // MARK: DetailView "detail_view.button.read" = "閱讀"; diff --git a/EhPanda/DataFlow/AppStore.swift b/EhPanda/DataFlow/AppStore.swift index f2b43fcb..8250b858 100644 --- a/EhPanda/DataFlow/AppStore.swift +++ b/EhPanda/DataFlow/AppStore.swift @@ -16,7 +16,7 @@ struct AppState: Equatable { var homeState = HomeState() var favoritesState = FavoritesState() var searchRootState = SearchRootState() - var settingState = SettingState() + var settingState = SettingReducer.State() } enum AppAction: BindableAction { @@ -32,7 +32,7 @@ enum AppAction: BindableAction { case home(HomeAction) case favorites(FavoritesAction) case searchRoot(SearchRootAction) - case setting(SettingAction) + case setting(SettingReducer.Action) } struct AnyEnvironment {} @@ -314,27 +314,28 @@ let appReducer = Reducer.combine( uiApplicationClient: $0.uiApplicationClient ) } - ), - settingReducer.pullback( - state: \.settingState, - action: /AppAction.setting, - environment: { - .init( - dfClient: $0.dfClient, - fileClient: $0.fileClient, - deviceClient: $0.deviceClient, - loggerClient: $0.loggerClient, - hapticsClient: $0.hapticsClient, - libraryClient: $0.libraryClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - userDefaultsClient: $0.userDefaultsClient, - uiApplicationClient: $0.uiApplicationClient, - authorizationClient: $0.authorizationClient - ) - } ) +// , +// settingReducer.pullback( +// state: \.settingState, +// action: /AppAction.setting, +// environment: { +// .init( +// dfClient: $0.dfClient, +// fileClient: $0.fileClient, +// deviceClient: $0.deviceClient, +// loggerClient: $0.loggerClient, +// hapticsClient: $0.hapticsClient, +// libraryClient: $0.libraryClient, +// cookieClient: $0.cookieClient, +// databaseClient: $0.databaseClient, +// clipboardClient: $0.clipboardClient, +// appDelegateClient: $0.appDelegateClient, +// userDefaultsClient: $0.userDefaultsClient, +// uiApplicationClient: $0.uiApplicationClient, +// authorizationClient: $0.authorizationClient +// ) +// } +// ) ) .logging() diff --git a/EhPanda/Models/Persistent/AppEnv.swift b/EhPanda/Models/Persistent/AppEnv.swift index 9d101598..e77dafef 100644 --- a/EhPanda/Models/Persistent/AppEnv.swift +++ b/EhPanda/Models/Persistent/AppEnv.swift @@ -5,7 +5,7 @@ // Created by 荒木辰造 on R 4/02/04. // -struct AppEnv: Codable { +struct AppEnv: Codable, Equatable { let user: User let setting: Setting let searchFilter: Filter diff --git a/EhPanda/Network/Request.swift b/EhPanda/Network/Request.swift index 32249759..5fb49897 100644 --- a/EhPanda/Network/Request.swift +++ b/EhPanda/Network/Request.swift @@ -704,8 +704,12 @@ struct IgneousRequest: Request { } } +struct VerifyEhProfileResponse: Equatable { + let profileValue: Int? + let isProfileNotFound: Bool +} struct VerifyEhProfileRequest: Request { - var publisher: AnyPublisher<(Int?, Bool), AppError> { + var publisher: AnyPublisher { URLSession.shared.dataTaskPublisher(for: Defaults.URL.uConfig) .genericRetry() .tryMap { try Kanna.HTML(html: $0.data, encoding: .utf8) } diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift index c1e98733..fc942e8d 100644 --- a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift @@ -25,7 +25,7 @@ struct AccountSettingReducer: ReducerProtocol { var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded var loginState = LoginReducer.State() - var ehSettingState = EhSettingState() + var ehSettingState = EhSettingReducer.State() } enum Action: BindableAction, Equatable { @@ -38,7 +38,7 @@ struct AccountSettingReducer: ReducerProtocol { case copyCookies(GalleryHost) case login(LoginReducer.Action) - case ehSetting(EhSettingAction) + case ehSetting(EhSettingReducer.Action) } @Dependency(\.clipboardClient) private var clipboardClient @@ -104,9 +104,8 @@ struct AccountSettingReducer: ReducerProtocol { hapticsClient: hapticsClient ) -// // TODO: Child reducers -// Scope(state: \.loginState, action: /Action.login, child: { loginReducer }) -// Scope(state: \.ehSettingState, action: /Action.ehSetting, child: { ehSettingReducer }) + Scope(state: \.loginState, action: /Action.login, child: LoginReducer.init) + Scope(state: \.ehSettingState, action: /Action.ehSetting, child: EhSettingReducer.init) BindingReducer() } diff --git a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift new file mode 100644 index 00000000..6f285ca3 --- /dev/null +++ b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift @@ -0,0 +1,38 @@ +// +// AppearanceSettingReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/03. +// + +import ComposableArchitecture + +struct AppearanceSettingReducer: ReducerProtocol { + enum Route { + case appIcon + } + + struct State: Equatable { + @BindingState var route: Route? + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case setNavigation(Route?) + } + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return .none + } + } + + BindingReducer() + } +} diff --git a/EhPanda/View/Setting/AppearanceSettingView.swift b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift similarity index 94% rename from EhPanda/View/Setting/AppearanceSettingView.swift rename to EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift index 880b73b7..82fdcb6b 100644 --- a/EhPanda/View/Setting/AppearanceSettingView.swift +++ b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift @@ -9,8 +9,8 @@ import SwiftUI import ComposableArchitecture struct AppearanceSettingView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf @Binding private var preferredColorScheme: PreferredColorScheme @Binding private var accentColor: Color @@ -21,7 +21,7 @@ struct AppearanceSettingView: View { @Binding private var displaysJapaneseTitle: Bool init( - store: Store, + store: StoreOf, preferredColorScheme: Binding, accentColor: Binding, appIconType: Binding, @@ -107,7 +107,7 @@ struct AppearanceSettingView: View { } private var navigationLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /AppearanceSettingState.Route.appIcon) { _ in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /AppearanceSettingReducer.Route.appIcon) { _ in AppIconView(appIconType: $appIconType) } } @@ -230,8 +230,7 @@ struct AppearanceSettingView_Previews: PreviewProvider { AppearanceSettingView( store: .init( initialState: .init(), - reducer: appearanceSettingReducer, - environment: AppearanceSettingEnvironment() + reducer: AppearanceSettingReducer() ), preferredColorScheme: .constant(.automatic), accentColor: .constant(.blue), diff --git a/EhPanda/View/Setting/Components/AboutView.swift b/EhPanda/View/Setting/Components/AboutView.swift new file mode 100644 index 00000000..b01c48b5 --- /dev/null +++ b/EhPanda/View/Setting/Components/AboutView.swift @@ -0,0 +1,263 @@ +// +// AboutView.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/01/18. +// + +import SwiftUI + +struct AboutView: View { + private var version: String { + [ + L10n.Localizable.AboutView.Title.version, + AppUtil.version, "(\(AppUtil.build))" + ] + .joined(separator: " ") + } + + var body: some View { + HStack { + VStack(alignment: .leading) { + Text(L10n.Constant.App.copyright) + Text(version) + } + .foregroundStyle(.gray).font(.caption2.bold()) + Spacer() + } + .padding(.horizontal) + Form { + Section { + ForEach(contacts) { contact in + LinkRow(urlString: contact.urlString, text: contact.text) + } + } + Section(L10n.Localizable.AboutView.Section.Title.specialThanks) { + ForEach(specialThanks) { specialThank in + LinkRow(urlString: specialThank.urlString, text: specialThank.text) + } + } + Section(L10n.Localizable.AboutView.Section.Title.codeLevelContributors) { + ForEach(codeLevelContributors) { codeLevelContributor in + LinkRow(urlString: codeLevelContributor.urlString, text: codeLevelContributor.text) + } + } + Section(L10n.Localizable.AboutView.Section.Title.translationContributors) { + ForEach(translationContributors) { translationContributor in + LinkRow(urlString: translationContributor.urlString, text: translationContributor.text) + } + } + Section(L10n.Localizable.AboutView.Section.Title.acknowledgements) { + ForEach(acknowledgements) { acknowledgement in + LinkRow(urlString: acknowledgement.urlString, text: acknowledgement.text) + } + } + } + .navigationTitle(L10n.Localizable.AboutView.Title.ehPanda) + } + + // MARK: Contacts + private let contacts: [Info] = {[ + .init( + urlString: L10n.Constant.App.Contact.Link.website, + text: L10n.Localizable.AboutView.Button.website + ), + .init( + urlString: L10n.Constant.App.Contact.Link.gitHub, + text: L10n.Constant.App.Contact.Text.gitHub + ), + .init( + urlString: L10n.Constant.App.Contact.Link.discord, + text: L10n.Constant.App.Contact.Text.discord + ), + .init( + urlString: L10n.Constant.App.Contact.Link.telegram, + text: L10n.Constant.App.Contact.Text.telegram + ), + .init( + urlString: L10n.Constant.App.Contact.Link.altStore, + text: L10n.Localizable.AboutView.Button.altStoreSource + ) + ]}() + + // MARK: Special thanks + private let specialThanks: [Info] = {[ + .init( + urlString: L10n.Constant.App.SpecialThanks.Link.taylorlannister, + text: L10n.Constant.App.SpecialThanks.Text.taylorlannister + ), + .init( + urlString: L10n.Constant.App.SpecialThanks.Link.luminescentYq, + text: L10n.Constant.App.SpecialThanks.Text.luminescentYq + ), + .init( + urlString: L10n.Constant.App.SpecialThanks.Link.caxerx, + text: L10n.Constant.App.SpecialThanks.Text.caxerx + ), + .init( + urlString: L10n.Constant.App.SpecialThanks.Link.honjow, + text: L10n.Constant.App.SpecialThanks.Text.honjow + ) + ]}() + + // MARK: Code level contributors + private let codeLevelContributors: [Info] = {[ + .init( + urlString: L10n.Constant.App.CodeLevelContributor.Link.tatsuz0u, + text: L10n.Constant.App.CodeLevelContributor.Text.tatsuz0u + ), + .init( + urlString: L10n.Constant.App.CodeLevelContributor.Link.chihchy, + text: L10n.Constant.App.CodeLevelContributor.Text.chihchy + ), + .init( + urlString: L10n.Constant.App.CodeLevelContributor.Link.xioxin, + text: L10n.Constant.App.CodeLevelContributor.Text.xioxin + ), + .init( + urlString: L10n.Constant.App.CodeLevelContributor.Link.ethanChinCN, + text: L10n.Constant.App.CodeLevelContributor.Text.ethanChinCN + ), + .init( + urlString: L10n.Constant.App.CodeLevelContributor.Link.lengYue, + text: L10n.Constant.App.CodeLevelContributor.Text.lengYue + ) + ]}() + + // MARK: Translation contributors + private let translationContributors: [Info] = {[ + .init( + urlString: L10n.Constant.App.TranslationContributor.Link.tatsuz0u, + text: L10n.Constant.App.TranslationContributor.Text.tatsuz0u + ), + .init( + urlString: L10n.Constant.App.TranslationContributor.Link.nebulosaCat, + text: L10n.Constant.App.TranslationContributor.Text.nebulosaCat + ), + .init( + urlString: L10n.Constant.App.TranslationContributor.Link.paulHaeussler, + text: L10n.Constant.App.TranslationContributor.Text.paulHaeussler + ), + .init( + urlString: L10n.Constant.App.TranslationContributor.Link.caxerx, + text: L10n.Constant.App.TranslationContributor.Text.caxerx + ), + .init( + urlString: L10n.Constant.App.TranslationContributor.Link.nyaanim, + text: L10n.Constant.App.TranslationContributor.Text.nyaanim + ) + ]}() + + // MARK: Acknowledgements + private let acknowledgements: [Info] = {[ + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.kanna, + text: L10n.Constant.App.Acknowledgement.Text.kanna + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.swiftGen, + text: L10n.Constant.App.Acknowledgement.Text.swiftGen + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.alertKit, + text: L10n.Constant.App.Acknowledgement.Text.alertKit + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.colorful, + text: L10n.Constant.App.Acknowledgement.Text.colorful + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.filePicker, + text: L10n.Constant.App.Acknowledgement.Text.filePicker + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.kingfisher, + text: L10n.Constant.App.Acknowledgement.Text.kingfisher + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.swiftUIPager, + text: L10n.Constant.App.Acknowledgement.Text.swiftUIPager + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.swiftyBeaver, + text: L10n.Constant.App.Acknowledgement.Text.swiftyBeaver + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.waterfallGrid, + text: L10n.Constant.App.Acknowledgement.Text.waterfallGrid + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.swiftyOpenCC, + text: L10n.Constant.App.Acknowledgement.Text.swiftyOpenCC + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.uiImageColors, + text: L10n.Constant.App.Acknowledgement.Text.uiImageColors + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.sfSafeSymbols, + text: L10n.Constant.App.Acknowledgement.Text.sfSafeSymbols + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.ttProgressHUD, + text: L10n.Constant.App.Acknowledgement.Text.ttProgressHUD + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.swiftUINavigation, + text: L10n.Constant.App.Acknowledgement.Text.swiftUINavigation + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.swiftCommonMark, + text: L10n.Constant.App.Acknowledgement.Text.swiftCommonMark + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.ehTagTranslationDatabase, + text: L10n.Constant.App.Acknowledgement.Text.ehTagTranslationDatabase + ), + .init( + urlString: L10n.Constant.App.Acknowledgement.Link.tca, + text: L10n.Constant.App.Acknowledgement.Text.tca + ) + ]}() +} + +// MARK: LinkRow +private struct LinkRow: View { + private let urlString: String + private let text: String + + init(urlString: String, text: String) { + self.urlString = urlString + self.text = text + } + + var body: some View { + ZStack { + let text = Text(text).fontWeight(.medium) + if let url = URL(string: urlString) { + Link(destination: url) { + text.withArrow() + } + } else { + text + } + } + .foregroundColor(.primary) + } +} + +// MARK: Definition +private struct Info: Identifiable { + var id: String { urlString } + + let urlString: String + let text: String +} + +struct EhPandaView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + AboutView() + } + } +} diff --git a/EhPanda/View/Setting/LaboratorySettingView.swift b/EhPanda/View/Setting/Components/LaboratorySettingView.swift similarity index 100% rename from EhPanda/View/Setting/LaboratorySettingView.swift rename to EhPanda/View/Setting/Components/LaboratorySettingView.swift diff --git a/EhPanda/View/Setting/ReadingSettingView.swift b/EhPanda/View/Setting/Components/ReadingSettingView.swift similarity index 100% rename from EhPanda/View/Setting/ReadingSettingView.swift rename to EhPanda/View/Setting/Components/ReadingSettingView.swift diff --git a/EhPanda/View/Setting/Support/WebView.swift b/EhPanda/View/Setting/Components/WebView.swift similarity index 100% rename from EhPanda/View/Setting/Support/WebView.swift rename to EhPanda/View/Setting/Components/WebView.swift diff --git a/EhPanda/View/Setting/DataFlow/AppearanceSettingStore.swift b/EhPanda/View/Setting/DataFlow/AppearanceSettingStore.swift deleted file mode 100644 index 9c41c843..00000000 --- a/EhPanda/View/Setting/DataFlow/AppearanceSettingStore.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AppearanceSettingStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/03. -// - -import ComposableArchitecture - -struct AppearanceSettingState: Equatable { - enum Route { - case appIcon - } - - @BindingState var route: Route? -} - -enum AppearanceSettingAction: BindableAction { - case binding(BindingAction) - case setNavigation(AppearanceSettingState.Route?) -} - -struct AppearanceSettingEnvironment {} - -let appearanceSettingReducer = Reducer -{ state, action, _ in - switch action { - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return .none - } -} -.binding() diff --git a/EhPanda/View/Setting/DataFlow/EhSettingStore.swift b/EhPanda/View/Setting/DataFlow/EhSettingStore.swift deleted file mode 100644 index dadd9e20..00000000 --- a/EhPanda/View/Setting/DataFlow/EhSettingStore.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// EhSettingStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/01. -// - -import Foundation -import ComposableArchitecture - -struct EhSettingState: Equatable { - enum Route: Equatable { - case webView(URL) - case deleteProfile - } - struct CancelID: Hashable { - let id = String(describing: EhSettingState.self) - } - - @BindingState var route: Route? - @BindingState var editingProfileName = "" - @BindingState var ehSetting: EhSetting? - @BindingState var ehProfile: EhProfile? - var loadingState: LoadingState = .idle - var submittingState: LoadingState = .idle - - mutating func setEhSetting(_ ehSetting: EhSetting) { - let ehProfile: EhProfile = ehSetting.ehProfiles - .filter(\.isSelected).first.forceUnwrapped - self.ehSetting = ehSetting - self.ehProfile = ehProfile - editingProfileName = ehProfile.name - } -} - -enum EhSettingAction: BindableAction, Equatable { - case binding(BindingAction) - case setNavigation(EhSettingState.Route?) - case setKeyboardHidden - case setDefaultProfile(Int) - - case teardown - case fetchEhSetting - case fetchEhSettingDone(Result) - case submitChanges - case submitChangesDone(Result) - case performAction(EhProfileAction?, String?, Int) - case performActionDone(Result) -} - -struct EhSettingEnvironment { - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let uiApplicationClient: UIApplicationClient -} - -let ehSettingReducer = Reducer { state, action, environment in - switch action { - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return .none - - case .setKeyboardHidden: - return environment.uiApplicationClient.hideKeyboard().fireAndForget() - - case .setDefaultProfile(let profileSet): - return environment.cookieClient.setOrEditCookie( - for: Defaults.URL.host, key: Defaults.Cookie.selectedProfile, value: String(profileSet) - ) - .fireAndForget() - - case .teardown: - return .cancel(id: EhSettingState.CancelID()) - - case .fetchEhSetting: - guard state.loadingState != .loading else { return .none } - state.loadingState = .loading - return EhSettingRequest().effect.map(EhSettingAction.fetchEhSettingDone) - .cancellable(id: EhSettingState.CancelID()) - - case .fetchEhSettingDone(let result): - state.loadingState = .idle - - switch result { - case .success(let ehSetting): - state.setEhSetting(ehSetting) - case .failure(let error): - state.loadingState = .failed(error) - } - return .none - - case .submitChanges: - guard state.submittingState != .loading, - let ehSetting = state.ehSetting - else { return .none } - - state.submittingState = .loading - return SubmitEhSettingChangesRequest(ehSetting: ehSetting) - .effect.map(EhSettingAction.submitChangesDone).cancellable(id: EhSettingState.CancelID()) - - case .submitChangesDone(let result): - state.submittingState = .idle - - switch result { - case .success(let ehSetting): - state.setEhSetting(ehSetting) - case .failure(let error): - state.submittingState = .failed(error) - } - return .none - - case .performAction(let action, let name, let set): - guard state.submittingState != .loading else { return .none } - state.submittingState = .loading - return EhProfileRequest(action: action, name: name, set: set) - .effect.map(EhSettingAction.performActionDone).cancellable(id: EhSettingState.CancelID()) - - case .performActionDone(let result): - state.submittingState = .idle - - switch result { - case .success(let ehSetting): - state.setEhSetting(ehSetting) - case .failure(let error): - state.submittingState = .failed(error) - } - return .none - } -} -.haptics( - unwrapping: \.route, - case: /EhSettingState.Route.webView, - hapticsClient: \.hapticsClient -) -.binding() diff --git a/EhPanda/View/Setting/DataFlow/GeneralSettingStore.swift b/EhPanda/View/Setting/DataFlow/GeneralSettingStore.swift deleted file mode 100644 index ef7206ea..00000000 --- a/EhPanda/View/Setting/DataFlow/GeneralSettingStore.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// GeneralSettingStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/03. -// - -import Kingfisher -import LocalAuthentication -import ComposableArchitecture - -struct GeneralSettingState: Equatable { - enum Route { - case logs - case clearCache - case removeCustomTranslations - } - - @BindingState var route: Route? - - var loadingState: LoadingState = .idle - var diskImageCacheSize = "0 KB" - var passcodeNotSet = false - - var logsState = LogsState() -} - -enum GeneralSettingAction: BindableAction, Equatable { - case binding(BindingAction) - case setNavigation(GeneralSettingState.Route?) - case clearSubStates - case onTranslationsFilePicked(URL) - case onRemoveCustomTranslations - - case clearWebImageCache - case checkPasscodeSetting - case navigateToSystemSetting - case calculateWebImageDiskCache - case calculateWebImageDiskCacheDone(UInt?) - - case logs(LogsAction) -} - -struct GeneralSettingEnvironment { - let fileClient: FileClient - let loggerClient: LoggerClient - let libraryClient: LibraryClient - let databaseClient: DatabaseClient - let uiApplicationClient: UIApplicationClient - let authorizationClient: AuthorizationClient -} - -let generalSettingReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .clearSubStates: - state.logsState = .init() - return .init(value: .logs(.teardown)) - - case .onTranslationsFilePicked: - return .none - - case .onRemoveCustomTranslations: - return .none - - case .clearWebImageCache: - return .merge( - environment.libraryClient.clearWebImageDiskCache().fireAndForget(), - environment.databaseClient.removeImageURLs().fireAndForget(), - .init(value: .calculateWebImageDiskCache) - ) - - case .checkPasscodeSetting: - state.passcodeNotSet = environment.authorizationClient.passcodeNotSet() - return .none - - case .navigateToSystemSetting: - return environment.uiApplicationClient.openSettings().fireAndForget() - - case .calculateWebImageDiskCache: - return environment.libraryClient.calculateWebImageDiskCacheSize() - .map(GeneralSettingAction.calculateWebImageDiskCacheDone) - - case .calculateWebImageDiskCacheDone(let bytes): - guard let bytes = bytes else { return .none } - let formatter = ByteCountFormatter() - formatter.allowedUnits = .useAll - state.diskImageCacheSize = formatter.string(fromByteCount: .init(bytes)) - return .none - - case .logs: - return .none - } - } - .binding(), - logsReducer.pullback( - state: \.logsState, - action: /GeneralSettingAction.logs, - environment: { - .init( - fileClient: $0.fileClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ) -) diff --git a/EhPanda/View/Setting/DataFlow/LoginStore.swift b/EhPanda/View/Setting/DataFlow/LoginStore.swift deleted file mode 100644 index b5928eed..00000000 --- a/EhPanda/View/Setting/DataFlow/LoginStore.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// LoginStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/01. -// - -import SwiftUI -import ComposableArchitecture - -struct LoginState: Equatable { - enum Route: Equatable { - case webView(URL) - } - enum FocusedField { - case username - case password - } - struct CancelID: Hashable { - let id = String(describing: LoginState.self) - } - - @BindingState var route: Route? - @BindingState var focusedField: FocusedField? - @BindingState var username = "" - @BindingState var password = "" - var loginState: LoadingState = .idle - - var loginButtonDisabled: Bool { - username.isEmpty || password.isEmpty - } - var loginButtonColor: Color { - loginState == .loading ? .clear : loginButtonDisabled - ? .primary.opacity(0.25) : .primary.opacity(0.75) - } -} - -enum LoginAction: BindableAction, Equatable { - case binding(BindingAction) - case setNavigation(LoginState.Route?) - - case teardown - case login - case loginDone(Result) -} - -struct LoginEnvironment { - let hapticClient: HapticClient - let cookiesClient: CookiesClient -} - -let loginReducer = Reducer { state, action, environment in - switch action { - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return .none - - case .teardown: - return .cancel(id: LoginState.CancelID()) - - case .login: - guard !state.loginButtonDisabled || state.loginState == .loading else { return .none } - state.focusedField = nil - state.loginState = .loading - return .merge( - environment.hapticClient.generateFeedback(.soft).fireAndForget(), - LoginRequest(username: state.username, password: state.password) - .effect.map(LoginAction.loginDone).cancellable(id: LoginState.CancelID()) - ) - - case .loginDone(let result): - state.route = nil - var effects = [EffectTask]() - if environment.cookiesClient.didLogin { - state.loginState = .idle - effects.append(environment.hapticClient.generateNotificationFeedback(.success).fireAndForget()) - } else { - state.loginState = .failed(.unknown) - effects.append(environment.hapticClient.generateNotificationFeedback(.error).fireAndForget()) - } - if case .success(let response) = result, let response = response { - effects.append(environment.cookiesClient.setCredentials(response: response).fireAndForget()) - } - return .merge(effects) - } -} -.haptics( - unwrapping: \.route, - case: /LoginState.Route.webView, - hapticClient: \.hapticClient -) -.binding() diff --git a/EhPanda/View/Setting/DataFlow/LogsStore.swift b/EhPanda/View/Setting/DataFlow/LogsStore.swift deleted file mode 100644 index aeb41833..00000000 --- a/EhPanda/View/Setting/DataFlow/LogsStore.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// LogsStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/03. -// - -import ComposableArchitecture - -struct LogsState: Equatable { - enum Route: Equatable { - case log(Log) - } - struct CancelID: Hashable { - let id = String(describing: LogsState.self) - } - - @BindingState var route: Route? - var loadingState: LoadingState = .idle - var logs = [Log]() -} - -enum LogsAction: BindableAction, Equatable { - case binding(BindingAction) - case setNavigation(LogsState.Route?) - case navigateToFileApp - - case teardown - case fetchLogs - case fetchLogsDone(Result<[Log], AppError>) - case deleteLog(String) - case deleteLogDone(Result) -} - -struct LogsEnvironment { - let fileClient: FileClient - let uiApplicationClient: UIApplicationClient -} - -let logsReducer = Reducer { state, action, environment in - switch action { - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return .none - - case .navigateToFileApp: - return environment.uiApplicationClient.openFileApp().fireAndForget() - - case .teardown: - return .cancel(id: LogsState.CancelID()) - - case .fetchLogs: - guard state.loadingState != .loading else { return .none } - state.loadingState = .loading - return environment.fileClient.fetchLogs().map(LogsAction.fetchLogsDone).cancellable(id: LogsState.CancelID()) - - case .fetchLogsDone(let result): - switch result { - case .success(let logs): - state.logs = logs - state.loadingState = .idle - case .failure(let error): - state.loadingState = .failed(error) - } - return .none - - case .deleteLog(let fileName): - return environment.fileClient.deleteLog(fileName).map(LogsAction.deleteLogDone) - - case .deleteLogDone(let result): - if case .success(let fileName) = result { - state.logs = state.logs.filter({ $0.fileName != fileName }) - } - return .none - } -} -.binding() diff --git a/EhPanda/View/Setting/DataFlow/SettingStore.swift b/EhPanda/View/Setting/DataFlow/SettingStore.swift deleted file mode 100644 index eee6d74a..00000000 --- a/EhPanda/View/Setting/DataFlow/SettingStore.swift +++ /dev/null @@ -1,491 +0,0 @@ -// -// SettingStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 3/12/31. -// - -import Foundation -import ComposableArchitecture - -struct SettingState: Equatable { - enum Route: Int, Hashable, Identifiable, CaseIterable { - var id: Int { rawValue } - - case account - case general - case appearance - case reading - case laboratory - case ehpanda - } - - // AppEnvStorage - @BindingState var setting = Setting() - var tagTranslator = TagTranslator() - var user = User() - - @BindingState var route: Route? - var tagTranslatorLoadingState: LoadingState = .idle - - var accountSettingState = AccountSettingReducer.State() - var generalSettingState = GeneralSettingState() - var appearanceSettingState = AppearanceSettingState() - - mutating func setGreeting(_ greeting: Greeting) { - guard let currDate = greeting.updateTime else { return } - - if let prevGreeting = user.greeting, - let prevDate = prevGreeting.updateTime, - prevDate < currDate - { - user.greeting = greeting - } else if user.greeting == nil { - user.greeting = greeting - } - } - mutating func updateUser(_ user: User) { - if let displayName = user.displayName { - self.user.displayName = displayName - } - if let avatarURL = user.avatarURL { - self.user.avatarURL = avatarURL - } - if let galleryPoints = user.galleryPoints, - let credits = user.credits - { - self.user.galleryPoints = galleryPoints - self.user.credits = credits - } - } -} - -enum SettingAction: BindableAction { - case binding(BindingAction) - case setNavigation(SettingState.Route?) - case clearSubStates - - case syncAppIconType - case syncUserInterfaceStyle - case syncSetting - case syncTagTranslator - case syncUser - - case loadUserSettings - case onLoadUserSettings(AppEnv) - case loadUserSettingsDone - case createDefaultEhProfile - case fetchIgneous - case fetchIgneousDone(Result) - case fetchUserInfo - case fetchUserInfoDone(Result) - case fetchGreeting - case fetchGreetingDone(Result) - case fetchTagTranslator - case fetchTagTranslatorDone(Result) - case fetchEhProfileIndex - case fetchEhProfileIndexDone(Result<(Int?, Bool), AppError>) - case fetchFavoriteCategories - case fetchFavoriteCategoriesDone(Result<[Int: String], AppError>) - - case account(AccountSettingReducer.Action) - case general(GeneralSettingAction) - case appearance(AppearanceSettingAction) -} - -struct SettingEnvironment { - let dfClient: DFClient - let fileClient: FileClient - let deviceClient: DeviceClient - let loggerClient: LoggerClient - let hapticsClient: HapticsClient - let libraryClient: LibraryClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let userDefaultsClient: UserDefaultsClient - let uiApplicationClient: UIApplicationClient - let authorizationClient: AuthorizationClient -} - -let settingReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$setting.galleryHost): - return .merge( - .init(value: .syncSetting), - environment.userDefaultsClient - .setValue(state.setting.galleryHost.rawValue, .galleryHost).fireAndForget() - ) - - case .binding(\.$setting.enablesTagsExtension): - var effects: [EffectTask] = [ - .init(value: .syncSetting) - ] - if state.setting.enablesTagsExtension { - effects.append(.init(value: .fetchTagTranslator)) - } - return .merge(effects) - - case .binding(\.$setting.preferredColorScheme): - return .merge( - .init(value: .syncSetting), - .init(value: .syncUserInterfaceStyle) - ) - - case .binding(\.$setting.appIconType): - return .merge( - .init(value: .syncSetting), - environment.uiApplicationClient.setAlternateIconName(state.setting.appIconType.filename) - .map { _ in SettingAction.syncAppIconType } - ) - - case .binding(\.$setting.autoLockPolicy): - if state.setting.autoLockPolicy != .never - && state.setting.backgroundBlurRadius == 0 - { - state.setting.backgroundBlurRadius = 10 - } - return .init(value: .syncSetting) - - case .binding(\.$setting.backgroundBlurRadius): - if state.setting.autoLockPolicy != .never - && state.setting.backgroundBlurRadius == 0 - { - state.setting.autoLockPolicy = .never - } - return .init(value: .syncSetting) - - case .binding(\.$setting.enablesLandscape): - var effects: [EffectTask] = [ - .init(value: .syncSetting) - ] - if !state.setting.enablesLandscape && !environment.deviceClient.isPad() { - effects.append(environment.appDelegateClient.setPortraitOrientationMask().fireAndForget()) - } - return .merge(effects) - - case .binding(\.$setting.maximumScaleFactor): - if state.setting.doubleTapScaleFactor > state.setting.maximumScaleFactor { - state.setting.doubleTapScaleFactor = state.setting.maximumScaleFactor - } - return .init(value: .syncSetting) - - case .binding(\.$setting.doubleTapScaleFactor): - if state.setting.maximumScaleFactor < state.setting.doubleTapScaleFactor { - state.setting.maximumScaleFactor = state.setting.doubleTapScaleFactor - } - return .init(value: .syncSetting) - - case .binding(\.$setting.bypassesSNIFiltering): - return .merge( - .init(value: .syncSetting), - environment.hapticsClient.generateFeedback(.soft).fireAndForget(), - environment.dfClient.setActive(state.setting.bypassesSNIFiltering).fireAndForget() - ) - - case .binding(\.$setting): - return .init(value: .syncSetting) - - case .binding(\.$route): - return .none - - case .binding: - return .merge( - .init(value: .syncUser), - .init(value: .syncSetting), - .init(value: .syncTagTranslator) - ) - - case .setNavigation(let route): - state.route = route - return .none - - case .clearSubStates: - state.accountSettingState = .init() - state.generalSettingState = .init() - state.appearanceSettingState = .init() - return .none - - case .syncAppIconType: - if let iconName = environment.uiApplicationClient.alternateIconName() { - state.setting.appIconType = AppIconType.allCases.filter({ - iconName.contains($0.filename) - }).first ?? .default - } - return .none - - case .syncUserInterfaceStyle: - let style = state.setting.preferredColorScheme.userInterfaceStyle - return environment.uiApplicationClient.setUserInterfaceStyle(style) - .subscribe(on: DispatchQueue.main).fireAndForget() - - case .syncSetting: - return environment.databaseClient.updateSetting(state.setting).fireAndForget() - case .syncTagTranslator: - return environment.databaseClient.updateTagTranslator(state.tagTranslator).fireAndForget() - case .syncUser: - return environment.databaseClient.updateUser(state.user).fireAndForget() - - case .loadUserSettings: - return environment.databaseClient.fetchAppEnv().map(SettingAction.onLoadUserSettings) - - case .onLoadUserSettings(let appEnv): - state.setting = appEnv.setting - state.tagTranslator = appEnv.tagTranslator - state.user = appEnv.user - var effects: [EffectTask] = [ - .init(value: .syncAppIconType), - .init(value: .loadUserSettingsDone), - .init(value: .syncUserInterfaceStyle), - environment.dfClient.setActive(state.setting.bypassesSNIFiltering).fireAndForget() - ] - if let value: String = environment.userDefaultsClient.getValue(.galleryHost), - let galleryHost = GalleryHost(rawValue: value) - { - state.setting.galleryHost = galleryHost - } - if environment.cookieClient.shouldFetchIgneous { - effects.append(.init(value: .fetchIgneous)) - } - if environment.cookieClient.didLogin { - effects.append(contentsOf: [ - .init(value: .fetchUserInfo), - .init(value: .fetchGreeting), - .init(value: .fetchFavoriteCategories), - .init(value: .fetchEhProfileIndex) - ]) - } - if state.setting.enablesTagsExtension { - effects.append(.init(value: .fetchTagTranslator)) - } - return .merge(effects) - - case .loadUserSettingsDone: - return .none - - case .createDefaultEhProfile: - return EhProfileRequest(action: .create, name: "EhPanda").effect.fireAndForget() - - case .fetchIgneous: - guard environment.cookieClient.didLogin else { return .none } - return IgneousRequest().effect.map(SettingAction.fetchIgneousDone) - - case .fetchIgneousDone(let result): - var effects = [EffectTask]() - if case .success(let response) = result { - effects.append(environment.cookieClient.setCredentials(response: response).fireAndForget()) - } - effects.append(.init(value: .account(.loadCookies))) - return .merge(effects) - - case .fetchUserInfo: - guard environment.cookieClient.didLogin else { return .none } - let uid = environment.cookieClient - .getCookie(Defaults.URL.host, Defaults.Cookie.ipbMemberId).rawValue - if !uid.isEmpty { - return UserInfoRequest(uid: uid).effect.map(SettingAction.fetchUserInfoDone) - } - return .none - - case .fetchUserInfoDone(let result): - if case .success(let user) = result { - state.updateUser(user) - return .init(value: .syncUser) - } - return .none - - case .fetchGreeting: - func verifyDate(with updateTime: Date?) -> Bool { - guard let updateTime = updateTime else { return true } - - let currentTime = Date() - let formatter = DateFormatter() - formatter.locale = Locale.current - formatter.timeZone = TimeZone(secondsFromGMT: 0) - formatter.dateFormat = Defaults.DateFormat.greeting - - let currentTimeString = formatter.string(from: currentTime) - if let currentDay = formatter.date(from: currentTimeString) { - return currentTime > currentDay && updateTime < currentDay - } - - return false - } - - guard environment.cookieClient.didLogin, - state.setting.showsNewDawnGreeting - else { return .none } - let requestEffect = GreetingRequest().effect - .map(SettingAction.fetchGreetingDone) - if let greeting = state.user.greeting { - if verifyDate(with: greeting.updateTime) { - return requestEffect - } - } else { - return requestEffect - } - return .none - - case .fetchGreetingDone(let result): - switch result { - case .success(let greeting): - state.setGreeting(greeting) - return .init(value: .syncUser) - case .failure(let error): - if case .parseFailed = error { - var greeting = Greeting() - greeting.updateTime = Date() - state.setGreeting(greeting) - return .init(value: .syncUser) - } - } - return .none - - case .fetchTagTranslator: - guard state.tagTranslatorLoadingState != .loading, - !state.tagTranslator.hasCustomTranslations, - let language = TranslatableLanguage.current - else { return .none } - state.tagTranslatorLoadingState = .loading - - var databaseEffect: EffectTask? - if state.tagTranslator.language != language { - state.tagTranslator = TagTranslator(language: language) - databaseEffect = .init(value: .syncTagTranslator) - } - let updatedDate = state.tagTranslator.updatedDate - let requestEffect = TagTranslatorRequest(language: language, updatedDate: updatedDate) - .effect.map(SettingAction.fetchTagTranslatorDone) - if let databaseEffect = databaseEffect { - return .merge(databaseEffect, requestEffect) - } else { - return requestEffect - } - - case .fetchTagTranslatorDone(let result): - state.tagTranslatorLoadingState = .idle - switch result { - case .success(let tagTranslator): - state.tagTranslator = tagTranslator - return .init(value: .syncTagTranslator) - case .failure(let error): - state.tagTranslatorLoadingState = .failed(error) - } - return .none - - case .fetchEhProfileIndex: - guard environment.cookieClient.didLogin else { return .none } - return VerifyEhProfileRequest().effect.map(SettingAction.fetchEhProfileIndexDone) - - case .fetchEhProfileIndexDone(let result): - var effects = [EffectTask]() - - if case .success(let (profileValue, profileNotFound)) = result { - if let profileValue = profileValue { - let hostURL = Defaults.URL.host - let profileValueString = String(profileValue) - let selectedProfileKey = Defaults.Cookie.selectedProfile - - let cookieValue = environment.cookieClient.getCookie(hostURL, selectedProfileKey) - if cookieValue.rawValue != profileValueString { - effects.append( - environment.cookieClient.setOrEditCookie( - for: hostURL, key: selectedProfileKey, value: profileValueString - ) - .fireAndForget() - ) - } - } else if profileNotFound { - effects.append(.init(value: .createDefaultEhProfile)) - } else { - let message = "Found profile but failed in parsing value." - effects.append(environment.loggerClient.error(message, nil).fireAndForget()) - } - } - return effects.isEmpty ? .none : .merge(effects) - - case .fetchFavoriteCategories: - guard environment.cookieClient.didLogin else { return .none } - return FavoriteCategoriesRequest().effect.map(SettingAction.fetchFavoriteCategoriesDone) - - case .fetchFavoriteCategoriesDone(let result): - if case .success(let categories) = result { - state.user.favoriteCategories = categories - } - return .none - - case .account(.login(.loginDone)): - return .merge( - environment.cookieClient.removeYay().fireAndForget(), - environment.cookieClient.fulfillAnotherHostField().fireAndForget(), - .init(value: .fetchIgneous), - .init(value: .fetchUserInfo), - .init(value: .fetchFavoriteCategories), - .init(value: .fetchEhProfileIndex) - ) - - case .account(.onLogoutConfirmButtonTapped): - state.user = User() - return .merge( - .init(value: .syncUser), - environment.cookieClient.clearAll().fireAndForget(), - environment.databaseClient.removeImageURLs().fireAndForget(), - environment.libraryClient.clearWebImageDiskCache().fireAndForget() - ) - - case .account: - return .none - - case .general(.onTranslationsFilePicked(let url)): - return environment.fileClient.importTagTranslator(url).map(SettingAction.fetchTagTranslatorDone) - - case .general(.onRemoveCustomTranslations): - state.tagTranslator.hasCustomTranslations = false - state.tagTranslator.translations = .init() - return .init(value: .syncTagTranslator) - - case .general: - return .none - - case .appearance: - return .none - } - } - .binding(), -// // TODO: Parent reducer -// accountSettingReducer.pullback( -// state: \.accountSettingState, -// action: /SettingAction.account, -// environment: { -// .init( -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// clipboardClient: $0.clipboardClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ), - generalSettingReducer.pullback( - state: \.generalSettingState, - action: /SettingAction.general, - environment: { - .init( - fileClient: $0.fileClient, - loggerClient: $0.loggerClient, - libraryClient: $0.libraryClient, - databaseClient: $0.databaseClient, - uiApplicationClient: $0.uiApplicationClient, - authorizationClient: $0.authorizationClient - ) - } - ), - appearanceSettingReducer.pullback( - state: \.appearanceSettingState, - action: /SettingAction.appearance, - environment: { _ in - .init() - } - ) -) diff --git a/EhPanda/View/Setting/EhPandaView.swift b/EhPanda/View/Setting/EhPandaView.swift deleted file mode 100644 index 7205d2ff..00000000 --- a/EhPanda/View/Setting/EhPandaView.swift +++ /dev/null @@ -1,263 +0,0 @@ -// -// EhPandaView.swift -// EhPanda -// -// Created by 荒木辰造 on R 3/01/18. -// - -import SwiftUI - -struct EhPandaView: View { - private var version: String { - [ - L10n.Localizable.EhPandaView.Title.version, - AppUtil.version, "(\(AppUtil.build))" - ] - .joined(separator: " ") - } - - var body: some View { - HStack { - VStack(alignment: .leading) { - Text(L10n.Constant.EhPanda.copyright) - Text(version) - } - .foregroundStyle(.gray).font(.caption2.bold()) - Spacer() - } - .padding(.horizontal) - Form { - Section { - ForEach(contacts) { contact in - LinkRow(urlString: contact.urlString, text: contact.text) - } - } - Section(L10n.Localizable.EhPandaView.Section.Title.specialThanks) { - ForEach(specialThanks) { specialThank in - LinkRow(urlString: specialThank.urlString, text: specialThank.text) - } - } - Section(L10n.Localizable.EhPandaView.Section.Title.codeLevelContributors) { - ForEach(codeLevelContributors) { codeLevelContributor in - LinkRow(urlString: codeLevelContributor.urlString, text: codeLevelContributor.text) - } - } - Section(L10n.Localizable.EhPandaView.Section.Title.translationContributors) { - ForEach(translationContributors) { translationContributor in - LinkRow(urlString: translationContributor.urlString, text: translationContributor.text) - } - } - Section(L10n.Localizable.EhPandaView.Section.Title.acknowledgements) { - ForEach(acknowledgements) { acknowledgement in - LinkRow(urlString: acknowledgement.urlString, text: acknowledgement.text) - } - } - } - .navigationTitle(L10n.Localizable.EhPandaView.Title.ehPanda) - } - - // MARK: Contacts - private let contacts: [Info] = {[ - .init( - urlString: L10n.Constant.EhPanda.Contacts.Link.website, - text: L10n.Localizable.EhPandaView.Button.website - ), - .init( - urlString: L10n.Constant.EhPanda.Contacts.Link.gitHub, - text: L10n.Constant.EhPanda.Contacts.Text.gitHub - ), - .init( - urlString: L10n.Constant.EhPanda.Contacts.Link.discord, - text: L10n.Constant.EhPanda.Contacts.Text.discord - ), - .init( - urlString: L10n.Constant.EhPanda.Contacts.Link.telegram, - text: L10n.Constant.EhPanda.Contacts.Text.telegram - ), - .init( - urlString: L10n.Constant.EhPanda.Contacts.Link.altStore, - text: L10n.Localizable.EhPandaView.Button.altStoreSource - ) - ]}() - - // MARK: Special thanks - private let specialThanks: [Info] = {[ - .init( - urlString: L10n.Constant.EhPanda.SpecialThanks.Link.taylorlannister, - text: L10n.Constant.EhPanda.SpecialThanks.Text.taylorlannister - ), - .init( - urlString: L10n.Constant.EhPanda.SpecialThanks.Link.luminescentYq, - text: L10n.Constant.EhPanda.SpecialThanks.Text.luminescentYq - ), - .init( - urlString: L10n.Constant.EhPanda.SpecialThanks.Link.caxerx, - text: L10n.Constant.EhPanda.SpecialThanks.Text.caxerx - ), - .init( - urlString: L10n.Constant.EhPanda.SpecialThanks.Link.honjow, - text: L10n.Constant.EhPanda.SpecialThanks.Text.honjow - ) - ]}() - - // MARK: Code level contributors - private let codeLevelContributors: [Info] = {[ - .init( - urlString: L10n.Constant.EhPanda.CodeLevelContributors.Link.tatsuz0u, - text: L10n.Constant.EhPanda.CodeLevelContributors.Text.tatsuz0u - ), - .init( - urlString: L10n.Constant.EhPanda.CodeLevelContributors.Link.chihchy, - text: L10n.Constant.EhPanda.CodeLevelContributors.Text.chihchy - ), - .init( - urlString: L10n.Constant.EhPanda.CodeLevelContributors.Link.xioxin, - text: L10n.Constant.EhPanda.CodeLevelContributors.Text.xioxin - ), - .init( - urlString: L10n.Constant.EhPanda.CodeLevelContributors.Link.ethanChinCN, - text: L10n.Constant.EhPanda.CodeLevelContributors.Text.ethanChinCN - ), - .init( - urlString: L10n.Constant.EhPanda.CodeLevelContributors.Link.lengYue, - text: L10n.Constant.EhPanda.CodeLevelContributors.Text.lengYue - ) - ]}() - - // MARK: Translation contributors - private let translationContributors: [Info] = {[ - .init( - urlString: L10n.Constant.EhPanda.TranslationContributors.Link.tatsuz0u, - text: L10n.Constant.EhPanda.TranslationContributors.Text.tatsuz0u - ), - .init( - urlString: L10n.Constant.EhPanda.TranslationContributors.Link.nebulosaCat, - text: L10n.Constant.EhPanda.TranslationContributors.Text.nebulosaCat - ), - .init( - urlString: L10n.Constant.EhPanda.TranslationContributors.Link.paulHaeussler, - text: L10n.Constant.EhPanda.TranslationContributors.Text.paulHaeussler - ), - .init( - urlString: L10n.Constant.EhPanda.TranslationContributors.Link.caxerx, - text: L10n.Constant.EhPanda.TranslationContributors.Text.caxerx - ), - .init( - urlString: L10n.Constant.EhPanda.TranslationContributors.Link.nyaanim, - text: L10n.Constant.EhPanda.TranslationContributors.Text.nyaanim - ) - ]}() - - // MARK: Acknowledgements - private let acknowledgements: [Info] = {[ - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.kanna, - text: L10n.Constant.EhPanda.Acknowledgements.Text.kanna - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.swiftGen, - text: L10n.Constant.EhPanda.Acknowledgements.Text.swiftGen - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.alertKit, - text: L10n.Constant.EhPanda.Acknowledgements.Text.alertKit - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.colorful, - text: L10n.Constant.EhPanda.Acknowledgements.Text.colorful - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.filePicker, - text: L10n.Constant.EhPanda.Acknowledgements.Text.filePicker - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.kingfisher, - text: L10n.Constant.EhPanda.Acknowledgements.Text.kingfisher - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.swiftUIPager, - text: L10n.Constant.EhPanda.Acknowledgements.Text.swiftUIPager - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.swiftyBeaver, - text: L10n.Constant.EhPanda.Acknowledgements.Text.swiftyBeaver - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.waterfallGrid, - text: L10n.Constant.EhPanda.Acknowledgements.Text.waterfallGrid - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.swiftyOpenCC, - text: L10n.Constant.EhPanda.Acknowledgements.Text.swiftyOpenCC - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.uiImageColors, - text: L10n.Constant.EhPanda.Acknowledgements.Text.uiImageColors - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.sfSafeSymbols, - text: L10n.Constant.EhPanda.Acknowledgements.Text.sfSafeSymbols - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.ttProgressHUD, - text: L10n.Constant.EhPanda.Acknowledgements.Text.ttProgressHUD - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.swiftUINavigation, - text: L10n.Constant.EhPanda.Acknowledgements.Text.swiftUINavigation - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.swiftCommonMark, - text: L10n.Constant.EhPanda.Acknowledgements.Text.swiftCommonMark - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.ehTagTranslationDatabase, - text: L10n.Constant.EhPanda.Acknowledgements.Text.ehTagTranslationDatabase - ), - .init( - urlString: L10n.Constant.EhPanda.Acknowledgements.Link.tca, - text: L10n.Constant.EhPanda.Acknowledgements.Text.tca - ) - ]}() -} - -// MARK: LinkRow -private struct LinkRow: View { - private let urlString: String - private let text: String - - init(urlString: String, text: String) { - self.urlString = urlString - self.text = text - } - - var body: some View { - ZStack { - let text = Text(text).fontWeight(.medium) - if let url = URL(string: urlString) { - Link(destination: url) { - text.withArrow() - } - } else { - text - } - } - .foregroundColor(.primary) - } -} - -// MARK: Definition -private struct Info: Identifiable { - var id: String { urlString } - - let urlString: String - let text: String -} - -struct EhPandaView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - EhPandaView() - } - } -} diff --git a/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift b/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift new file mode 100644 index 00000000..580ccd6d --- /dev/null +++ b/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift @@ -0,0 +1,142 @@ +// +// EhSettingReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/01. +// + +import Foundation +import ComposableArchitecture + +struct EhSettingReducer: ReducerProtocol { + enum Route: Equatable { + case webView(URL) + case deleteProfile + } + + struct CancelID: Hashable { + let id = String(describing: EhSettingReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var editingProfileName = "" + @BindingState var ehSetting: EhSetting? + @BindingState var ehProfile: EhProfile? + var loadingState: LoadingState = .idle + var submittingState: LoadingState = .idle + + mutating func setEhSetting(_ ehSetting: EhSetting) { + let ehProfile: EhProfile = ehSetting.ehProfiles + .filter(\.isSelected).first.forceUnwrapped + self.ehSetting = ehSetting + self.ehProfile = ehProfile + editingProfileName = ehProfile.name + } + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case setNavigation(Route?) + case setKeyboardHidden + case setDefaultProfile(Int) + + case teardown + case fetchEhSetting + case fetchEhSettingDone(Result) + case submitChanges + case submitChangesDone(Result) + case performAction(EhProfileAction?, String?, Int) + case performActionDone(Result) + } + + @Dependency(\.uiApplicationClient) private var uiApplicationClient + @Dependency(\.hapticsClient) private var hapticsClient + @Dependency(\.cookieClient) private var cookieClient + + public var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return .none + + case .setKeyboardHidden: + return uiApplicationClient.hideKeyboard().fireAndForget() + + case .setDefaultProfile(let profileSet): + return cookieClient.setOrEditCookie( + for: Defaults.URL.host, key: Defaults.Cookie.selectedProfile, value: String(profileSet) + ) + .fireAndForget() + + case .teardown: + return .cancel(id: CancelID()) + + case .fetchEhSetting: + guard state.loadingState != .loading else { return .none } + state.loadingState = .loading + return EhSettingRequest().effect.map(Action.fetchEhSettingDone) + .cancellable(id: CancelID()) + + case .fetchEhSettingDone(let result): + state.loadingState = .idle + + switch result { + case .success(let ehSetting): + state.setEhSetting(ehSetting) + case .failure(let error): + state.loadingState = .failed(error) + } + return .none + + case .submitChanges: + guard state.submittingState != .loading, + let ehSetting = state.ehSetting + else { return .none } + + state.submittingState = .loading + return SubmitEhSettingChangesRequest(ehSetting: ehSetting) + .effect.map(Action.submitChangesDone).cancellable(id: CancelID()) + + case .submitChangesDone(let result): + state.submittingState = .idle + + switch result { + case .success(let ehSetting): + state.setEhSetting(ehSetting) + case .failure(let error): + state.submittingState = .failed(error) + } + return .none + + case .performAction(let action, let name, let set): + guard state.submittingState != .loading else { return .none } + state.submittingState = .loading + return EhProfileRequest(action: action, name: name, set: set) + .effect.map(Action.performActionDone).cancellable(id: CancelID()) + + case .performActionDone(let result): + state.submittingState = .idle + + switch result { + case .success(let ehSetting): + state.setEhSetting(ehSetting) + case .failure(let error): + state.submittingState = .failed(error) + } + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.webView, + hapticsClient: hapticsClient + ) + + BindingReducer() + } +} diff --git a/EhPanda/View/Setting/Support/EhSettingView.swift b/EhPanda/View/Setting/EhSetting/EhSettingView.swift similarity index 97% rename from EhPanda/View/Setting/Support/EhSettingView.swift rename to EhPanda/View/Setting/EhSetting/EhSettingView.swift index f47448f8..79147ff3 100644 --- a/EhPanda/View/Setting/Support/EhSettingView.swift +++ b/EhPanda/View/Setting/EhSetting/EhSettingView.swift @@ -9,12 +9,12 @@ import SwiftUI import ComposableArchitecture struct EhSettingView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let bypassesSNIFiltering: Bool private let blurRadius: Double - init(store: Store, bypassesSNIFiltering: Bool, blurRadius: Double) { + init(store: StoreOf, bypassesSNIFiltering: Bool, blurRadius: Double) { self.store = store viewStore = ViewStore(store) self.bypassesSNIFiltering = bypassesSNIFiltering @@ -50,7 +50,7 @@ struct EhSettingView: View { viewStore.send(.setDefaultProfile(profileSet)) } } - .sheet(unwrapping: viewStore.binding(\.$route), case: /EhSettingState.Route.webView) { route in + .sheet(unwrapping: viewStore.binding(\.$route), case: /EhSettingReducer.Route.webView) { route in WebView(url: route.wrappedValue) .autoBlur(radius: blurRadius) } @@ -139,7 +139,7 @@ struct EhSettingView: View { // MARK: EhProfileSection private struct EhProfileSection: View { - @Binding private var route: EhSettingState.Route? + @Binding private var route: EhSettingReducer.Route? @Binding private var ehSetting: EhSetting @Binding private var ehProfile: EhProfile @Binding private var editingProfileName: String @@ -150,7 +150,7 @@ private struct EhProfileSection: View { @FocusState private var isFocused init( - route: Binding, ehSetting: Binding, + route: Binding, ehSetting: Binding, ehProfile: Binding, editingProfileName: Binding, deleteAction: @escaping () -> Void, deleteDialogAction: @escaping () -> Void, performEhProfileAction: @escaping (EhProfileAction?, String?, Int) -> Void @@ -187,7 +187,7 @@ private struct EhProfileSection: View { .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.delete, unwrapping: $route, - case: /EhSettingState.Route.deleteProfile + case: /EhSettingReducer.Route.deleteProfile ) { Button( L10n.Localizable.ConfirmationDialog.Button.delete, @@ -1087,12 +1087,7 @@ struct EhSettingView_Previews: PreviewProvider { EhSettingView( store: .init( initialState: .init(ehSetting: .empty, ehProfile: .empty, loadingState: .idle), - reducer: ehSettingReducer, - environment: EhSettingEnvironment( - hapticsClient: .live, - cookieClient: .live, - uiApplicationClient: .live - ) + reducer: EhSettingReducer() ), bypassesSNIFiltering: false, blurRadius: 0 diff --git a/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift b/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift new file mode 100644 index 00000000..2aa40761 --- /dev/null +++ b/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift @@ -0,0 +1,107 @@ +// +// GeneralSettingReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/03. +// + +import Kingfisher +import LocalAuthentication +import ComposableArchitecture + +struct GeneralSettingReducer: ReducerProtocol { + enum Route { + case logs + case clearCache + case removeCustomTranslations + } + + struct State: Equatable { + @BindingState var route: Route? + + var loadingState: LoadingState = .idle + var diskImageCacheSize = "0 KB" + var passcodeNotSet = false + + var logsState = LogsReducer.State() + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + case onTranslationsFilePicked(URL) + case onRemoveCustomTranslations + + case clearWebImageCache + case checkPasscodeSetting + case navigateToSystemSetting + case calculateWebImageDiskCache + case calculateWebImageDiskCacheDone(UInt?) + + case logs(LogsReducer.Action) + } + + @Dependency(\.authorizationClient) private var authorizationClient + @Dependency(\.uiApplicationClient) private var uiApplicationClient + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.libraryClient) private var libraryClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .clearSubStates: + state.logsState = .init() + return .init(value: .logs(.teardown)) + + case .onTranslationsFilePicked: + return .none + + case .onRemoveCustomTranslations: + return .none + + case .clearWebImageCache: + return .merge( + libraryClient.clearWebImageDiskCache().fireAndForget(), + databaseClient.removeImageURLs().fireAndForget(), + .init(value: .calculateWebImageDiskCache) + ) + + case .checkPasscodeSetting: + state.passcodeNotSet = authorizationClient.passcodeNotSet() + return .none + + case .navigateToSystemSetting: + return uiApplicationClient.openSettings().fireAndForget() + + case .calculateWebImageDiskCache: + return libraryClient.calculateWebImageDiskCacheSize() + .map(Action.calculateWebImageDiskCacheDone) + + case .calculateWebImageDiskCacheDone(let bytes): + guard let bytes = bytes else { return .none } + let formatter = ByteCountFormatter() + formatter.allowedUnits = .useAll + state.diskImageCacheSize = formatter.string(fromByteCount: .init(bytes)) + return .none + + case .logs: + return .none + } + } + + Scope(state: \.logsState, action: /Action.logs, child: LogsReducer.init) + + BindingReducer() + } +} diff --git a/EhPanda/View/Setting/GeneralSettingView.swift b/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift similarity index 91% rename from EhPanda/View/Setting/GeneralSettingView.swift rename to EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift index 80649cd4..f85c77db 100644 --- a/EhPanda/View/Setting/GeneralSettingView.swift +++ b/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift @@ -10,8 +10,8 @@ import FilePicker import ComposableArchitecture struct GeneralSettingView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let tagTranslatorLoadingState: LoadingState private let tagTranslatorEmpty: Bool private let tagTranslatorHasCustomTranslations: Bool @@ -25,7 +25,7 @@ struct GeneralSettingView: View { @Binding private var autoLockPolicy: AutoLockPolicy init( - store: Store, + store: StoreOf, tagTranslatorLoadingState: LoadingState, tagTranslatorEmpty: Bool, tagTranslatorHasCustomTranslations: Bool, enablesTagsExtension: Binding, translatesTags: Binding, showsTagsSearchSuggestion: Binding, @@ -107,7 +107,7 @@ struct GeneralSettingView: View { .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.removeCustomTranslations, unwrapping: viewStore.binding(\.$route), - case: /GeneralSettingState.Route.removeCustomTranslations + case: /GeneralSettingReducer.Route.removeCustomTranslations ) { Button(L10n.Localizable.ConfirmationDialog.Button.remove, role: .destructive) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { @@ -165,7 +165,7 @@ struct GeneralSettingView: View { .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.clear, unwrapping: viewStore.binding(\.$route), - case: /GeneralSettingState.Route.clearCache + case: /GeneralSettingReducer.Route.clearCache ) { Button(L10n.Localizable.ConfirmationDialog.Button.clear, role: .destructive) { viewStore.send(.clearWebImageCache) @@ -186,8 +186,8 @@ struct GeneralSettingView: View { } private var navigationLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /GeneralSettingState.Route.logs) { _ in - LogsView(store: store.scope(state: \.logsState, action: GeneralSettingAction.logs)) + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /GeneralSettingReducer.Route.logs) { _ in + LogsView(store: store.scope(state: \.logsState, action: GeneralSettingReducer.Action.logs)) } } } @@ -198,15 +198,7 @@ struct GeneralSettingView_Previews: PreviewProvider { GeneralSettingView( store: .init( initialState: .init(), - reducer: generalSettingReducer, - environment: GeneralSettingEnvironment( - fileClient: .live, - loggerClient: .live, - libraryClient: .live, - databaseClient: .live, - uiApplicationClient: .live, - authorizationClient: .live - ) + reducer: GeneralSettingReducer() ), tagTranslatorLoadingState: .idle, tagTranslatorEmpty: false, diff --git a/EhPanda/View/Setting/Login/LoginReducer.swift b/EhPanda/View/Setting/Login/LoginReducer.swift new file mode 100644 index 00000000..d9505a4c --- /dev/null +++ b/EhPanda/View/Setting/Login/LoginReducer.swift @@ -0,0 +1,98 @@ +// +// LoginReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/01. +// + +import SwiftUI +import ComposableArchitecture + +struct LoginReducer: ReducerProtocol { + private enum CancelID: Hashable {} + + enum Route: Equatable { + case webView(URL) + } + + enum FocusedField { + case username + case password + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var focusedField: FocusedField? + @BindingState var username = "" + @BindingState var password = "" + var loginState: LoadingState = .idle + + var loginButtonDisabled: Bool { + username.isEmpty || password.isEmpty + } + var loginButtonColor: Color { + loginState == .loading ? .clear : loginButtonDisabled + ? .primary.opacity(0.25) : .primary.opacity(0.75) + } + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case setNavigation(Route?) + + case teardown + case login + case loginDone(Result) + } + + @Dependency(\.hapticsClient) private var hapticsClient + @Dependency(\.cookieClient) private var cookieClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return .none + + case .teardown: + return .cancel(id: CancelID.self) + + case .login: + guard !state.loginButtonDisabled || state.loginState == .loading else { return .none } + state.focusedField = nil + state.loginState = .loading + return .merge( + hapticsClient.generateFeedback(.soft).fireAndForget(), + LoginRequest(username: state.username, password: state.password) + .effect.map(Action.loginDone).cancellable(id: CancelID.self) + ) + + case .loginDone(let result): + state.route = nil + var effects = [EffectTask]() + if cookieClient.didLogin { + state.loginState = .idle + effects.append(hapticsClient.generateNotificationFeedback(.success).fireAndForget()) + } else { + state.loginState = .failed(.unknown) + effects.append(hapticsClient.generateNotificationFeedback(.error).fireAndForget()) + } + if case .success(let response) = result, let response = response { + effects.append(cookieClient.setCredentials(response: response).fireAndForget()) + } + return .merge(effects) + } + } + .haptics( + unwrapping: \.route, + case: /Route.webView, + hapticsClient: hapticsClient + ) + + BindingReducer() + } +} diff --git a/EhPanda/View/Setting/Support/LoginView.swift b/EhPanda/View/Setting/Login/LoginView.swift similarity index 87% rename from EhPanda/View/Setting/Support/LoginView.swift rename to EhPanda/View/Setting/Login/LoginView.swift index 59e80285..eb5bde54 100644 --- a/EhPanda/View/Setting/Support/LoginView.swift +++ b/EhPanda/View/Setting/Login/LoginView.swift @@ -9,14 +9,14 @@ import SwiftUI import ComposableArchitecture struct LoginView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let bypassesSNIFiltering: Bool private let blurRadius: Double - @FocusState private var focusedField: LoginState.FocusedField? + @FocusState private var focusedField: LoginReducer.FocusedField? - init(store: Store, bypassesSNIFiltering: Bool, blurRadius: Double) { + init(store: StoreOf, bypassesSNIFiltering: Bool, blurRadius: Double) { self.store = store viewStore = ViewStore(store) self.bypassesSNIFiltering = bypassesSNIFiltering @@ -56,7 +56,7 @@ struct LoginView: View { } } .synchronize(viewStore.binding(\.$focusedField), $focusedField) - .sheet(unwrapping: viewStore.binding(\.$route), case: /LoginState.Route.webView) { route in + .sheet(unwrapping: viewStore.binding(\.$route), case: /LoginReducer.Route.webView) { route in WebView(url: route.wrappedValue) { viewStore.send(.loginDone(.success(nil))) } @@ -92,7 +92,7 @@ struct LoginView: View { // MARK: LoginTextField private struct LoginTextField: View { @Environment(\.colorScheme) private var colorScheme - private let focusedField: FocusState.Binding + private let focusedField: FocusState.Binding @Binding private var text: String private let description: String private let isPassword: Bool @@ -102,7 +102,7 @@ private struct LoginTextField: View { } init( - focusedField: FocusState.Binding, + focusedField: FocusState.Binding, text: Binding, description: String, isPassword: Bool ) { self.focusedField = focusedField @@ -136,11 +136,7 @@ struct LoginView_Previews: PreviewProvider { LoginView( store: .init( initialState: .init(), - reducer: loginReducer, - environment: LoginEnvironment( - hapticClient: .live, - cookiesClient: .live - ) + reducer: LoginReducer() ), bypassesSNIFiltering: false, blurRadius: 0 diff --git a/EhPanda/View/Setting/Logs/LogsReducer.swift b/EhPanda/View/Setting/Logs/LogsReducer.swift new file mode 100644 index 00000000..b3c03eba --- /dev/null +++ b/EhPanda/View/Setting/Logs/LogsReducer.swift @@ -0,0 +1,84 @@ +// +// LogsReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/03. +// + +import ComposableArchitecture + +struct LogsReducer: ReducerProtocol { + enum Route: Equatable { + case log(Log) + } + + struct CancelID: Hashable { + let id = String(describing: LogsReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + var loadingState: LoadingState = .idle + var logs = [Log]() + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case setNavigation(Route?) + case navigateToFileApp + + case teardown + case fetchLogs + case fetchLogsDone(Result<[Log], AppError>) + case deleteLog(String) + case deleteLogDone(Result) + } + + @Dependency(\.uiApplicationClient) private var uiApplicationClient + @Dependency(\.fileClient) private var fileClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return .none + + case .navigateToFileApp: + return uiApplicationClient.openFileApp().fireAndForget() + + case .teardown: + return .cancel(id: CancelID()) + + case .fetchLogs: + guard state.loadingState != .loading else { return .none } + state.loadingState = .loading + return fileClient.fetchLogs().map(Action.fetchLogsDone).cancellable(id: CancelID()) + + case .fetchLogsDone(let result): + switch result { + case .success(let logs): + state.logs = logs + state.loadingState = .idle + case .failure(let error): + state.loadingState = .failed(error) + } + return .none + + case .deleteLog(let fileName): + return fileClient.deleteLog(fileName).map(Action.deleteLogDone) + + case .deleteLogDone(let result): + if case .success(let fileName) = result { + state.logs = state.logs.filter({ $0.fileName != fileName }) + } + return .none + } + } + + BindingReducer() + } +} diff --git a/EhPanda/View/Setting/Support/LogsView.swift b/EhPanda/View/Setting/Logs/LogsView.swift similarity index 92% rename from EhPanda/View/Setting/Support/LogsView.swift rename to EhPanda/View/Setting/Logs/LogsView.swift index 5de76b43..0d519b11 100644 --- a/EhPanda/View/Setting/Support/LogsView.swift +++ b/EhPanda/View/Setting/Logs/LogsView.swift @@ -9,10 +9,10 @@ import SwiftUI import ComposableArchitecture struct LogsView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf - init(store: Store) { + init(store: StoreOf) { self.store = store viewStore = ViewStore(store) } @@ -56,7 +56,7 @@ struct LogsView: View { } private var navigationLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /LogsState.Route.log) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /LogsReducer.Route.log) { route in LogView(log: route.wrappedValue) } } @@ -177,11 +177,7 @@ struct LogsView_Previews: PreviewProvider { LogsView( store: .init( initialState: .init(), - reducer: logsReducer, - environment: .init( - fileClient: .live, - uiApplicationClient: .live - ) + reducer: LogsReducer() ) ) } diff --git a/EhPanda/View/Setting/SettingReducer.swift b/EhPanda/View/Setting/SettingReducer.swift new file mode 100644 index 00000000..f4e57ae4 --- /dev/null +++ b/EhPanda/View/Setting/SettingReducer.swift @@ -0,0 +1,461 @@ +// +// SettingReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/12/31. +// + +import Foundation +import ComposableArchitecture + +struct SettingReducer: ReducerProtocol { + enum Route: Int, Equatable, Hashable, Identifiable, CaseIterable { + var id: Int { rawValue } + + case account + case general + case appearance + case reading + case laboratory + case about + } + + struct State: Equatable { + // AppEnvStorage + @BindingState var setting = Setting() + var tagTranslator = TagTranslator() + var user = User() + + @BindingState var route: Route? + var tagTranslatorLoadingState: LoadingState = .idle + + var accountSettingState = AccountSettingReducer.State() + var generalSettingState = GeneralSettingReducer.State() + var appearanceSettingState = AppearanceSettingReducer.State() + + mutating func setGreeting(_ greeting: Greeting) { + guard let currDate = greeting.updateTime else { return } + + if let prevGreeting = user.greeting, + let prevDate = prevGreeting.updateTime, + prevDate < currDate + { + user.greeting = greeting + } else if user.greeting == nil { + user.greeting = greeting + } + } + + mutating func updateUser(_ user: User) { + if let displayName = user.displayName { + self.user.displayName = displayName + } + if let avatarURL = user.avatarURL { + self.user.avatarURL = avatarURL + } + if let galleryPoints = user.galleryPoints, + let credits = user.credits + { + self.user.galleryPoints = galleryPoints + self.user.credits = credits + } + } + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + + case syncAppIconType + case syncUserInterfaceStyle + case syncSetting + case syncTagTranslator + case syncUser + + case loadUserSettings + case onLoadUserSettings(AppEnv) + case loadUserSettingsDone + case createDefaultEhProfile + case fetchIgneous + case fetchIgneousDone(Result) + case fetchUserInfo + case fetchUserInfoDone(Result) + case fetchGreeting + case fetchGreetingDone(Result) + case fetchTagTranslator + case fetchTagTranslatorDone(Result) + case fetchEhProfileIndex + case fetchEhProfileIndexDone(Result) + case fetchFavoriteCategories + case fetchFavoriteCategoriesDone(Result<[Int: String], AppError>) + + case account(AccountSettingReducer.Action) + case general(GeneralSettingReducer.Action) + case appearance(AppearanceSettingReducer.Action) + } + + @Dependency(\.uiApplicationClient) private var uiApplicationClient + @Dependency(\.userDefaultsClient) private var userDefaultsClient + @Dependency(\.appDelegateClient) private var appDelegateClient + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.libraryClient) private var libraryClient + @Dependency(\.hapticsClient) private var hapticsClient + @Dependency(\.loggerClient) private var loggerClient + @Dependency(\.cookieClient) private var cookieClient + @Dependency(\.deviceClient) private var deviceClient + @Dependency(\.fileClient) private var fileClient + @Dependency(\.dfClient) private var dfClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$setting.galleryHost): + return .merge( + .init(value: .syncSetting), + userDefaultsClient + .setValue(state.setting.galleryHost.rawValue, .galleryHost).fireAndForget() + ) + + case .binding(\.$setting.enablesTagsExtension): + var effects: [EffectTask] = [ + .init(value: .syncSetting) + ] + if state.setting.enablesTagsExtension { + effects.append(.init(value: .fetchTagTranslator)) + } + return .merge(effects) + + case .binding(\.$setting.preferredColorScheme): + return .merge( + .init(value: .syncSetting), + .init(value: .syncUserInterfaceStyle) + ) + + case .binding(\.$setting.appIconType): + return .merge( + .init(value: .syncSetting), + uiApplicationClient.setAlternateIconName(state.setting.appIconType.filename) + .map { _ in Action.syncAppIconType } + ) + + case .binding(\.$setting.autoLockPolicy): + if state.setting.autoLockPolicy != .never + && state.setting.backgroundBlurRadius == 0 + { + state.setting.backgroundBlurRadius = 10 + } + return .init(value: .syncSetting) + + case .binding(\.$setting.backgroundBlurRadius): + if state.setting.autoLockPolicy != .never + && state.setting.backgroundBlurRadius == 0 + { + state.setting.autoLockPolicy = .never + } + return .init(value: .syncSetting) + + case .binding(\.$setting.enablesLandscape): + var effects: [EffectTask] = [ + .init(value: .syncSetting) + ] + if !state.setting.enablesLandscape && !deviceClient.isPad() { + effects.append(appDelegateClient.setPortraitOrientationMask().fireAndForget()) + } + return .merge(effects) + + case .binding(\.$setting.maximumScaleFactor): + if state.setting.doubleTapScaleFactor > state.setting.maximumScaleFactor { + state.setting.doubleTapScaleFactor = state.setting.maximumScaleFactor + } + return .init(value: .syncSetting) + + case .binding(\.$setting.doubleTapScaleFactor): + if state.setting.maximumScaleFactor < state.setting.doubleTapScaleFactor { + state.setting.maximumScaleFactor = state.setting.doubleTapScaleFactor + } + return .init(value: .syncSetting) + + case .binding(\.$setting.bypassesSNIFiltering): + return .merge( + .init(value: .syncSetting), + hapticsClient.generateFeedback(.soft).fireAndForget(), + dfClient.setActive(state.setting.bypassesSNIFiltering).fireAndForget() + ) + + case .binding(\.$setting): + return .init(value: .syncSetting) + + case .binding(\.$route): + return .none + + case .binding: + return .merge( + .init(value: .syncUser), + .init(value: .syncSetting), + .init(value: .syncTagTranslator) + ) + + case .setNavigation(let route): + state.route = route + return .none + + case .clearSubStates: + state.accountSettingState = .init() + state.generalSettingState = .init() + state.appearanceSettingState = .init() + return .none + + case .syncAppIconType: + if let iconName = uiApplicationClient.alternateIconName() { + state.setting.appIconType = AppIconType.allCases.filter({ + iconName.contains($0.filename) + }).first ?? .default + } + return .none + + case .syncUserInterfaceStyle: + let style = state.setting.preferredColorScheme.userInterfaceStyle + return uiApplicationClient.setUserInterfaceStyle(style) + .subscribe(on: DispatchQueue.main).fireAndForget() + + case .syncSetting: + return databaseClient.updateSetting(state.setting).fireAndForget() + case .syncTagTranslator: + return databaseClient.updateTagTranslator(state.tagTranslator).fireAndForget() + case .syncUser: + return databaseClient.updateUser(state.user).fireAndForget() + + case .loadUserSettings: + return databaseClient.fetchAppEnv().map(Action.onLoadUserSettings) + + case .onLoadUserSettings(let appEnv): + state.setting = appEnv.setting + state.tagTranslator = appEnv.tagTranslator + state.user = appEnv.user + var effects: [EffectTask] = [ + .init(value: .syncAppIconType), + .init(value: .loadUserSettingsDone), + .init(value: .syncUserInterfaceStyle), + dfClient.setActive(state.setting.bypassesSNIFiltering).fireAndForget() + ] + if let value: String = userDefaultsClient.getValue(.galleryHost), + let galleryHost = GalleryHost(rawValue: value) + { + state.setting.galleryHost = galleryHost + } + if cookieClient.shouldFetchIgneous { + effects.append(.init(value: .fetchIgneous)) + } + if cookieClient.didLogin { + effects.append(contentsOf: [ + .init(value: .fetchUserInfo), + .init(value: .fetchGreeting), + .init(value: .fetchFavoriteCategories), + .init(value: .fetchEhProfileIndex) + ]) + } + if state.setting.enablesTagsExtension { + effects.append(.init(value: .fetchTagTranslator)) + } + return .merge(effects) + + case .loadUserSettingsDone: + return .none + + case .createDefaultEhProfile: + return EhProfileRequest(action: .create, name: "EhPanda").effect.fireAndForget() + + case .fetchIgneous: + guard cookieClient.didLogin else { return .none } + return IgneousRequest().effect.map(Action.fetchIgneousDone) + + case .fetchIgneousDone(let result): + var effects = [EffectTask]() + if case .success(let response) = result { + effects.append(cookieClient.setCredentials(response: response).fireAndForget()) + } + effects.append(.init(value: .account(.loadCookies))) + return .merge(effects) + + case .fetchUserInfo: + guard cookieClient.didLogin else { return .none } + let uid = cookieClient + .getCookie(Defaults.URL.host, Defaults.Cookie.ipbMemberId).rawValue + if !uid.isEmpty { + return UserInfoRequest(uid: uid).effect.map(Action.fetchUserInfoDone) + } + return .none + + case .fetchUserInfoDone(let result): + if case .success(let user) = result { + state.updateUser(user) + return .init(value: .syncUser) + } + return .none + + case .fetchGreeting: + func verifyDate(with updateTime: Date?) -> Bool { + guard let updateTime = updateTime else { return true } + + let currentTime = Date() + let formatter = DateFormatter() + formatter.locale = Locale.current + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.dateFormat = Defaults.DateFormat.greeting + + let currentTimeString = formatter.string(from: currentTime) + if let currentDay = formatter.date(from: currentTimeString) { + return currentTime > currentDay && updateTime < currentDay + } + + return false + } + + guard cookieClient.didLogin, + state.setting.showsNewDawnGreeting + else { return .none } + let requestEffect = GreetingRequest().effect + .map(Action.fetchGreetingDone) + if let greeting = state.user.greeting { + if verifyDate(with: greeting.updateTime) { + return requestEffect + } + } else { + return requestEffect + } + return .none + + case .fetchGreetingDone(let result): + switch result { + case .success(let greeting): + state.setGreeting(greeting) + return .init(value: .syncUser) + case .failure(let error): + if case .parseFailed = error { + var greeting = Greeting() + greeting.updateTime = Date() + state.setGreeting(greeting) + return .init(value: .syncUser) + } + } + return .none + + case .fetchTagTranslator: + guard state.tagTranslatorLoadingState != .loading, + !state.tagTranslator.hasCustomTranslations, + let language = TranslatableLanguage.current + else { return .none } + state.tagTranslatorLoadingState = .loading + + var databaseEffect: EffectTask? + if state.tagTranslator.language != language { + state.tagTranslator = TagTranslator(language: language) + databaseEffect = .init(value: .syncTagTranslator) + } + let updatedDate = state.tagTranslator.updatedDate + let requestEffect = TagTranslatorRequest(language: language, updatedDate: updatedDate) + .effect.map(Action.fetchTagTranslatorDone) + if let databaseEffect = databaseEffect { + return .merge(databaseEffect, requestEffect) + } else { + return requestEffect + } + + case .fetchTagTranslatorDone(let result): + state.tagTranslatorLoadingState = .idle + switch result { + case .success(let tagTranslator): + state.tagTranslator = tagTranslator + return .init(value: .syncTagTranslator) + case .failure(let error): + state.tagTranslatorLoadingState = .failed(error) + } + return .none + + case .fetchEhProfileIndex: + guard cookieClient.didLogin else { return .none } + return VerifyEhProfileRequest().effect.map(Action.fetchEhProfileIndexDone) + + case .fetchEhProfileIndexDone(let result): + var effects = [EffectTask]() + + if case .success(let response) = result { + if let profileValue = response.profileValue { + let hostURL = Defaults.URL.host + let profileValueString = String(profileValue) + let selectedProfileKey = Defaults.Cookie.selectedProfile + + let cookieValue = cookieClient.getCookie(hostURL, selectedProfileKey) + if cookieValue.rawValue != profileValueString { + effects.append( + cookieClient.setOrEditCookie( + for: hostURL, key: selectedProfileKey, value: profileValueString + ) + .fireAndForget() + ) + } + } else if response.isProfileNotFound { + effects.append(.init(value: .createDefaultEhProfile)) + } else { + let message = "Found profile but failed in parsing value." + effects.append(loggerClient.error(message, nil).fireAndForget()) + } + } + return effects.isEmpty ? .none : .merge(effects) + + case .fetchFavoriteCategories: + guard cookieClient.didLogin else { return .none } + return FavoriteCategoriesRequest().effect.map(Action.fetchFavoriteCategoriesDone) + + case .fetchFavoriteCategoriesDone(let result): + if case .success(let categories) = result { + state.user.favoriteCategories = categories + } + return .none + + case .account(.login(.loginDone)): + return .merge( + cookieClient.removeYay().fireAndForget(), + cookieClient.fulfillAnotherHostField().fireAndForget(), + .init(value: .fetchIgneous), + .init(value: .fetchUserInfo), + .init(value: .fetchFavoriteCategories), + .init(value: .fetchEhProfileIndex) + ) + + case .account(.onLogoutConfirmButtonTapped): + state.user = User() + return .merge( + .init(value: .syncUser), + cookieClient.clearAll().fireAndForget(), + databaseClient.removeImageURLs().fireAndForget(), + libraryClient.clearWebImageDiskCache().fireAndForget() + ) + + case .account: + return .none + + case .general(.onTranslationsFilePicked(let url)): + return fileClient.importTagTranslator(url).map(Action.fetchTagTranslatorDone) + + case .general(.onRemoveCustomTranslations): + state.tagTranslator.hasCustomTranslations = false + state.tagTranslator.translations = .init() + return .init(value: .syncTagTranslator) + + case .general: + return .none + + case .appearance: + return .none + } + } + + 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) + + BindingReducer() + } +} diff --git a/EhPanda/View/Setting/SettingView.swift b/EhPanda/View/Setting/SettingView.swift index c827323b..f4d267b2 100644 --- a/EhPanda/View/Setting/SettingView.swift +++ b/EhPanda/View/Setting/SettingView.swift @@ -10,11 +10,11 @@ import SFSafeSymbols import ComposableArchitecture struct SettingView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let blurRadius: Double - init(store: Store, blurRadius: Double) { + init(store: StoreOf, blurRadius: Double) { self.store = store viewStore = ViewStore(store) self.blurRadius = blurRadius @@ -25,7 +25,7 @@ struct SettingView: View { NavigationView { ScrollView { VStack(spacing: 0) { - ForEach(SettingState.Route.allCases) { route in + ForEach(SettingReducer.Route.allCases) { route in SettingRow(rowType: route) { viewStore.send(.setNavigation($0)) } @@ -42,9 +42,9 @@ struct SettingView: View { // MARK: NavigationLinks private extension SettingView { @ViewBuilder var navigationLinks: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingState.Route.account) { _ in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingReducer.Route.account) { _ in AccountSettingView( - store: store.scope(state: \.accountSettingState, action: SettingAction.account), + store: store.scope(state: \.accountSettingState, action: SettingReducer.Action.account), galleryHost: viewStore.binding(\.$setting.galleryHost), showsNewDawnGreeting: viewStore.binding(\.$setting.showsNewDawnGreeting), bypassesSNIFiltering: viewStore.setting.bypassesSNIFiltering, @@ -52,9 +52,9 @@ private extension SettingView { ) .tint(viewStore.setting.accentColor) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingState.Route.general) { _ in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingReducer.Route.general) { _ in GeneralSettingView( - store: store.scope(state: \.generalSettingState, action: SettingAction.general), + store: store.scope(state: \.generalSettingState, action: SettingReducer.Action.general), tagTranslatorLoadingState: viewStore.tagTranslatorLoadingState, tagTranslatorEmpty: viewStore.tagTranslator.translations.isEmpty, tagTranslatorHasCustomTranslations: viewStore.tagTranslator.hasCustomTranslations, @@ -69,9 +69,9 @@ private extension SettingView { ) .tint(viewStore.setting.accentColor) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingState.Route.appearance) { _ in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingReducer.Route.appearance) { _ in AppearanceSettingView( - store: store.scope(state: \.appearanceSettingState, action: SettingAction.appearance), + store: store.scope(state: \.appearanceSettingState, action: SettingReducer.Action.appearance), preferredColorScheme: viewStore.binding(\.$setting.preferredColorScheme), accentColor: viewStore.binding(\.$setting.accentColor), appIconType: viewStore.binding(\.$setting.appIconType), @@ -82,7 +82,7 @@ private extension SettingView { ) .tint(viewStore.setting.accentColor) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingState.Route.reading) { _ in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingReducer.Route.reading) { _ in ReadingSettingView( readingDirection: viewStore.binding(\.$setting.readingDirection), prefetchLimit: viewStore.binding(\.$setting.prefetchLimit), @@ -93,14 +93,14 @@ private extension SettingView { ) .tint(viewStore.setting.accentColor) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingState.Route.laboratory) { _ in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingReducer.Route.laboratory) { _ in LaboratorySettingView( bypassesSNIFiltering: viewStore.binding(\.$setting.bypassesSNIFiltering) ) .tint(viewStore.setting.accentColor) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingState.Route.ehpanda) { _ in - EhPandaView().tint(viewStore.setting.accentColor) + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingReducer.Route.about) { _ in + AboutView().tint(viewStore.setting.accentColor) } } } @@ -110,8 +110,8 @@ private struct SettingRow: View { @Environment(\.colorScheme) private var colorScheme @State private var isPressing = false - private let rowType: SettingState.Route - private let tapAction: (SettingState.Route) -> Void + private let rowType: SettingReducer.Route + private let tapAction: (SettingReducer.Route) -> Void private var color: Color { colorScheme == .light ? Color(.darkGray) : Color(.lightGray) @@ -120,7 +120,7 @@ private struct SettingRow: View { isPressing ? color.opacity(0.1) : .clear } - init(rowType: SettingState.Route, tapAction: @escaping (SettingState.Route) -> Void) { + init(rowType: SettingReducer.Route, tapAction: @escaping (SettingReducer.Route) -> Void) { self.rowType = rowType self.tapAction = tapAction } @@ -145,7 +145,7 @@ private struct SettingRow: View { } // MARK: Definition -extension SettingState.Route { +extension SettingReducer.Route { var value: String { switch self { case .account: @@ -158,8 +158,8 @@ extension SettingState.Route { return L10n.Localizable.Enum.SettingStateRoute.Value.reading case .laboratory: return L10n.Localizable.Enum.SettingStateRoute.Value.laboratory - case .ehpanda: - return L10n.Localizable.Enum.SettingStateRoute.Value.ehPanda + case .about: + return L10n.Localizable.Enum.SettingStateRoute.Value.about } } var symbol: SFSymbol { @@ -174,7 +174,7 @@ extension SettingState.Route { return .newspaperFill case .laboratory: return .testtube2 - case .ehpanda: + case .about: return .pCircleFill } } @@ -185,22 +185,7 @@ struct SettingView_Previews: PreviewProvider { SettingView( store: .init( initialState: .init(), - reducer: settingReducer, - environment: SettingEnvironment( - dfClient: .live, - fileClient: .live, - deviceClient: .live, - loggerClient: .live, - hapticsClient: .live, - libraryClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - userDefaultsClient: .live, - uiApplicationClient: .live, - authorizationClient: .live - ) + reducer: SettingReducer() ), blurRadius: 0 ) From 5d3bac3dc877d4c4a7750cfe8eb5d30a5af5fad8 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Thu, 25 May 2023 20:25:04 +0800 Subject: [PATCH 12/29] Refactor SearchRootReducer Refactor SearchReducer Refactor FiltersReducer Refactor QuickSearchReducer --- EhPanda.xcodeproj/project.pbxproj | 32 +-- EhPanda/DataFlow/AppStore.swift | 41 +-- .../Detail/DataFlow/DetailSearchStore.swift | 47 ++-- EhPanda/View/Favorites/FavoritesStore.swift | 23 +- .../View/Home/DataFlow/FrontpageStore.swift | 22 +- EhPanda/View/Home/DataFlow/PopularStore.swift | 22 +- EhPanda/View/Home/DataFlow/WatchedStore.swift | 44 ++-- EhPanda/View/Search/SearchReducer.swift | 207 +++++++++++++++ EhPanda/View/Search/SearchRootReducer.swift | 210 +++++++++++++++ EhPanda/View/Search/SearchRootStore.swift | 248 ------------------ EhPanda/View/Search/SearchRootView.swift | 40 +-- EhPanda/View/Search/SearchStore.swift | 227 ---------------- EhPanda/View/Search/SearchView.swift | 36 +-- .../Search/Support/QuickSearchReducer.swift | 134 ++++++++++ .../Search/Support/QuickSearchStore.swift | 130 --------- .../View/Search/Support/QuickSearchView.swift | 23 +- EhPanda/View/Support/FiltersReducer.swift | 114 ++++++++ EhPanda/View/Support/FiltersStore.swift | 110 -------- EhPanda/View/Support/FiltersView.swift | 27 +- 19 files changed, 830 insertions(+), 907 deletions(-) create mode 100644 EhPanda/View/Search/SearchReducer.swift create mode 100644 EhPanda/View/Search/SearchRootReducer.swift delete mode 100644 EhPanda/View/Search/SearchRootStore.swift delete mode 100644 EhPanda/View/Search/SearchStore.swift create mode 100644 EhPanda/View/Search/Support/QuickSearchReducer.swift delete mode 100644 EhPanda/View/Search/Support/QuickSearchStore.swift create mode 100644 EhPanda/View/Support/FiltersReducer.swift delete mode 100644 EhPanda/View/Support/FiltersStore.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 496e5f3d..98038359 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -129,9 +129,9 @@ AB706F92278A6E8C0025A48A /* WatchedStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F91278A6E8C0025A48A /* WatchedStore.swift */; }; AB706F95278A75D30025A48A /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F94278A75D30025A48A /* HistoryView.swift */; }; AB706F97278A77E20025A48A /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F96278A77E20025A48A /* HistoryStore.swift */; }; - AB706F99278A820C0025A48A /* FiltersStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F98278A820C0025A48A /* FiltersStore.swift */; }; + AB706F99278A820C0025A48A /* FiltersReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F98278A820C0025A48A /* FiltersReducer.swift */; }; AB706F9B278AC5A30025A48A /* SearchRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F9A278AC5A30025A48A /* SearchRootView.swift */; }; - AB706F9D278ACCA20025A48A /* SearchRootStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F9C278ACCA20025A48A /* SearchRootStore.swift */; }; + AB706F9D278ACCA20025A48A /* SearchRootReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F9C278ACCA20025A48A /* SearchRootReducer.swift */; }; AB706F9F278AD4800025A48A /* GalleryHistoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F9E278AD4800025A48A /* GalleryHistoryCell.swift */; }; AB706FA1278BCEC60025A48A /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706FA0278BCEC60025A48A /* DetailView.swift */; }; AB706FA3278BCF2F0025A48A /* DetailStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706FA2278BCF2F0025A48A /* DetailStore.swift */; }; @@ -189,7 +189,6 @@ ABAB5B9727EF03F600198597 /* AppearanceSettingStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABAB5B9627EF03F600198597 /* AppearanceSettingStoreTests.swift */; }; ABAC82FE26BC4A96009F5026 /* OpenCC in Frameworks */ = {isa = PBXBuildFile; productRef = ABAC82FD26BC4A96009F5026 /* OpenCC */; }; ABBB2631278E6EF3007B6149 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2630278E6EF3007B6149 /* SearchView.swift */; }; - ABBB2633278E6F3B007B6149 /* SearchStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2632278E6F3B007B6149 /* SearchStore.swift */; }; ABBB2636278FB888007B6149 /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = ABBB2635278FB888007B6149 /* SwiftUINavigation */; }; ABBB2638278FBD2F007B6149 /* SwiftUINavigation_Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2637278FBD2F007B6149 /* SwiftUINavigation_Extension.swift */; }; ABBB263A2792588F007B6149 /* TTProgressHUD_Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB26392792588F007B6149 /* TTProgressHUD_Extension.swift */; }; @@ -200,7 +199,7 @@ ABBB26682797BFAA007B6149 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB26672797BFAA007B6149 /* ActivityView.swift */; }; ABBB266A2797C61F007B6149 /* TorrentsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB26692797C61F007B6149 /* TorrentsStore.swift */; }; ABBB266C2797E882007B6149 /* ClipboardClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB266B2797E882007B6149 /* ClipboardClient.swift */; }; - ABBB266E27998479007B6149 /* QuickSearchStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB266D27998479007B6149 /* QuickSearchStore.swift */; }; + ABBB266E27998479007B6149 /* QuickSearchReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB266D27998479007B6149 /* QuickSearchReducer.swift */; }; ABBB2671279AFA61007B6149 /* EnvironmentKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2670279AFA61007B6149 /* EnvironmentKeys.swift */; }; ABBB2673279B9332007B6149 /* ReadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2672279B9332007B6149 /* ReadingView.swift */; }; ABBB2675279B933D007B6149 /* ReadingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2674279B933D007B6149 /* ReadingStore.swift */; }; @@ -274,6 +273,7 @@ ABF75F3F25A19CD200544D29 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF75F3E25A19CD200544D29 /* User.swift */; }; ABF9720A26DE6E1300118887 /* GalleryDetailWithGreeting.html in Resources */ = {isa = PBXBuildFile; fileRef = ABF9720926DE6E1300118887 /* GalleryDetailWithGreeting.html */; }; EA2E2E7F2A1F7E500038A261 /* SettingReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2E2E7E2A1F7E500038A261 /* SettingReducer.swift */; }; + EA2E2E822A1FA1060038A261 /* SearchReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2E2E812A1FA1050038A261 /* SearchReducer.swift */; }; EAE63E2129E2A6330048C601 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = EAE63E2029E2A6330048C601 /* SwiftyBeaver */; }; /* End PBXBuildFile section */ @@ -429,9 +429,9 @@ AB706F93278A6F2B0025A48A /* Model 6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 6.xcdatamodel"; sourceTree = ""; }; AB706F94278A75D30025A48A /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; AB706F96278A77E20025A48A /* HistoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryStore.swift; sourceTree = ""; }; - AB706F98278A820C0025A48A /* FiltersStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersStore.swift; sourceTree = ""; }; + AB706F98278A820C0025A48A /* FiltersReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersReducer.swift; sourceTree = ""; }; AB706F9A278AC5A30025A48A /* SearchRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRootView.swift; sourceTree = ""; }; - AB706F9C278ACCA20025A48A /* SearchRootStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRootStore.swift; sourceTree = ""; }; + AB706F9C278ACCA20025A48A /* SearchRootReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRootReducer.swift; sourceTree = ""; }; AB706F9E278AD4800025A48A /* GalleryHistoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryHistoryCell.swift; sourceTree = ""; }; AB706FA0278BCEC60025A48A /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; AB706FA2278BCF2F0025A48A /* DetailStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailStore.swift; sourceTree = ""; }; @@ -493,7 +493,6 @@ ABB5013026A41EBA00B542D9 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; ABB5013126A41EBA00B542D9 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = ""; }; ABBB2630278E6EF3007B6149 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; - ABBB2632278E6F3B007B6149 /* SearchStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchStore.swift; sourceTree = ""; }; ABBB2637278FBD2F007B6149 /* SwiftUINavigation_Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUINavigation_Extension.swift; sourceTree = ""; }; ABBB26392792588F007B6149 /* TTProgressHUD_Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTProgressHUD_Extension.swift; sourceTree = ""; }; ABBB263D2793C648007B6149 /* PreviewsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsStore.swift; sourceTree = ""; }; @@ -503,7 +502,7 @@ ABBB26672797BFAA007B6149 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; ABBB26692797C61F007B6149 /* TorrentsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentsStore.swift; sourceTree = ""; }; ABBB266B2797E882007B6149 /* ClipboardClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardClient.swift; sourceTree = ""; }; - ABBB266D27998479007B6149 /* QuickSearchStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSearchStore.swift; sourceTree = ""; }; + ABBB266D27998479007B6149 /* QuickSearchReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSearchReducer.swift; sourceTree = ""; }; ABBB2670279AFA61007B6149 /* EnvironmentKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentKeys.swift; sourceTree = ""; }; ABBB2672279B9332007B6149 /* ReadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadingView.swift; sourceTree = ""; }; ABBB2674279B933D007B6149 /* ReadingStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadingStore.swift; sourceTree = ""; }; @@ -587,6 +586,7 @@ ABF75F3E25A19CD200544D29 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; ABF9720926DE6E1300118887 /* GalleryDetailWithGreeting.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = GalleryDetailWithGreeting.html; 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -670,7 +670,7 @@ isa = PBXGroup; children = ( ABF45AC125F3313D00ECB568 /* FiltersView.swift */, - AB706F98278A820C0025A48A /* FiltersStore.swift */, + AB706F98278A820C0025A48A /* FiltersReducer.swift */, ABC1FAB72642C37D00A9F352 /* NewDawnView.swift */, ABF45AC625F3313D00ECB568 /* Components */, ); @@ -715,7 +715,7 @@ isa = PBXGroup; children = ( ABE9401426FF158D0085E158 /* QuickSearchView.swift */, - ABBB266D27998479007B6149 /* QuickSearchStore.swift */, + ABBB266D27998479007B6149 /* QuickSearchReducer.swift */, ); path = Support; sourceTree = ""; @@ -1076,9 +1076,9 @@ isa = PBXGroup; children = ( AB706F9A278AC5A30025A48A /* SearchRootView.swift */, - AB706F9C278ACCA20025A48A /* SearchRootStore.swift */, + AB706F9C278ACCA20025A48A /* SearchRootReducer.swift */, ABBB2630278E6EF3007B6149 /* SearchView.swift */, - ABBB2632278E6F3B007B6149 /* SearchStore.swift */, + EA2E2E812A1FA1050038A261 /* SearchReducer.swift */, AB24C563276757C30085C33A /* Support */, ); path = Search; @@ -1713,6 +1713,7 @@ AB63EADB2699AC8200090535 /* AppEnvMO+CoreDataProperties.swift in Sources */, AB7BF2FD27ABCAD4001865A3 /* MigrationStore.swift in Sources */, AB7BF2D827AA3F61001865A3 /* UserDefaultsUtil.swift in Sources */, + EA2E2E822A1FA1060038A261 /* SearchReducer.swift in Sources */, AB0929CA278196ED00F107CA /* CookieClient.swift in Sources */, AB7BF2FB27ABCA3A001865A3 /* MigrationView.swift in Sources */, AB7BF31C27ABE028001865A3 /* NSManagedObjectModel+Compatible.swift in Sources */, @@ -1735,7 +1736,7 @@ AB0929D42781EDDC00F107CA /* UserDefaultsClient.swift in Sources */, AB0929D82782A83A00F107CA /* AuthorizationClient.swift in Sources */, ABF45AEF25F3313D00ECB568 /* TorrentsView.swift in Sources */, - AB706F99278A820C0025A48A /* FiltersStore.swift in Sources */, + AB706F99278A820C0025A48A /* FiltersReducer.swift in Sources */, AB3072D4276E19AA00EFF242 /* FrontpageView.swift in Sources */, AB3072D2276D734800EFF242 /* SubSection.swift in Sources */, ABBB26682797BFAA007B6149 /* ActivityView.swift in Sources */, @@ -1793,7 +1794,6 @@ ABBB2631278E6EF3007B6149 /* SearchView.swift in Sources */, AB706F92278A6E8C0025A48A /* WatchedStore.swift in Sources */, AB706F80278981370025A48A /* AlertKit_Extension.swift in Sources */, - ABBB2633278E6F3B007B6149 /* SearchStore.swift in Sources */, ABA9A6C228EC7BD000EE28DE /* Strings.swift in Sources */, AB58A5B22776B99000C0D285 /* AppStore.swift in Sources */, AB24C566276758E30085C33A /* GalleryCardCell.swift in Sources */, @@ -1838,7 +1838,7 @@ AB7B29F626AC741600EE1F14 /* GenericList.swift in Sources */, AB0CFBC927C07F95004BD372 /* TagSuggestionView.swift in Sources */, AB706F82278986120025A48A /* ToolbarItems.swift in Sources */, - ABBB266E27998479007B6149 /* QuickSearchStore.swift in Sources */, + ABBB266E27998479007B6149 /* QuickSearchReducer.swift in Sources */, AB0ABCB526C5406400AD970F /* LoginView.swift in Sources */, AB24C55C2767565A0085C33A /* HomeView.swift in Sources */, ABF45AF225F3313D00ECB568 /* GeneralSettingView.swift in Sources */, @@ -1862,7 +1862,7 @@ AB358315269D821D009466A5 /* DFExtensions.swift in Sources */, ABC3C7872593699B00E0C11B /* EhPandaApp.swift in Sources */, AB0929C82781938A00F107CA /* DFClient.swift in Sources */, - AB706F9D278ACCA20025A48A /* SearchRootStore.swift in Sources */, + AB706F9D278ACCA20025A48A /* SearchRootReducer.swift in Sources */, ABF313A525B1AB6600D47A2F /* Misc.swift in Sources */, ABA732DF25A852D800B3D9AB /* Filter.swift in Sources */, AB7BF31E27ABE028001865A3 /* NSPersistentStoreCoordinator+SQLite.swift in Sources */, diff --git a/EhPanda/DataFlow/AppStore.swift b/EhPanda/DataFlow/AppStore.swift index 8250b858..cd88f567 100644 --- a/EhPanda/DataFlow/AppStore.swift +++ b/EhPanda/DataFlow/AppStore.swift @@ -15,7 +15,7 @@ struct AppState: Equatable { var tabBarState = TabBarState() var homeState = HomeState() var favoritesState = FavoritesState() - var searchRootState = SearchRootState() + var searchRootState = SearchRootReducer.State() var settingState = SettingReducer.State() } @@ -31,7 +31,7 @@ enum AppAction: BindableAction { case home(HomeAction) case favorites(FavoritesAction) - case searchRoot(SearchRootAction) + case searchRoot(SearchRootReducer.Action) case setting(SettingReducer.Action) } @@ -296,26 +296,27 @@ let appReducer = Reducer.combine( uiApplicationClient: $0.uiApplicationClient ) } - ), - searchRootReducer.pullback( - state: \.searchRootState, - action: /AppAction.searchRoot, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } ) // , +// searchRootReducer.pullback( +// state: \.searchRootState, +// action: /AppAction.searchRoot, +// environment: { +// .init( +// urlClient: $0.urlClient, +// fileClient: $0.fileClient, +// imageClient: $0.imageClient, +// deviceClient: $0.deviceClient, +// hapticsClient: $0.hapticsClient, +// cookieClient: $0.cookieClient, +// databaseClient: $0.databaseClient, +// clipboardClient: $0.clipboardClient, +// appDelegateClient: $0.appDelegateClient, +// uiApplicationClient: $0.uiApplicationClient +// ) +// } +// ) +// , // settingReducer.pullback( // state: \.settingState, // action: /AppAction.setting, diff --git a/EhPanda/View/Detail/DataFlow/DetailSearchStore.swift b/EhPanda/View/Detail/DataFlow/DetailSearchStore.swift index 2daa8a0a..9e035142 100644 --- a/EhPanda/View/Detail/DataFlow/DetailSearchStore.swift +++ b/EhPanda/View/Detail/DataFlow/DetailSearchStore.swift @@ -31,8 +31,8 @@ struct DetailSearchState: Equatable { var footerLoadingState: LoadingState = .idle @Heap var detailState: DetailState! - var filtersState = FiltersState() - var quickDetailSearchState = QuickSearchState() + var filtersState = FiltersReducer.State() + var quickDetailSearchState = QuickSearchReducer.State() mutating func insertGalleries(_ galleries: [Gallery]) { galleries.forEach { gallery in @@ -55,8 +55,8 @@ enum DetailSearchAction: BindableAction { case fetchMoreGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) case detail(DetailAction) - case filters(FiltersAction) - case quickSearch(QuickSearchAction) + case filters(FiltersReducer.Action) + case quickSearch(QuickSearchReducer.Action) } struct DetailSearchEnvironment { @@ -186,23 +186,24 @@ let detailSearchReducer = Reducer) case detail(DetailAction) - case quickSearch(QuickSearchAction) + case quickSearch(QuickSearchReducer.Action) } // MARK: Environment @@ -217,14 +217,15 @@ let favoritesReducer = Reducer) - case filters(FiltersAction) + case filters(FiltersReducer.Action) case detail(DetailAction) } @@ -166,15 +166,15 @@ let frontpageReducer = Reducer) - case filters(FiltersAction) + case filters(FiltersReducer.Action) case detail(DetailAction) } @@ -116,15 +116,15 @@ let popularReducer = Reducer.co hapticsClient: \.hapticsClient ) .binding(), - filtersReducer.pullback( - state: \.filtersState, - action: /PopularAction.filters, - environment: { - .init( - databaseClient: $0.databaseClient - ) - } - ), +// filtersReducer.pullback( +// state: \.filtersState, +// action: /PopularAction.filters, +// environment: { +// .init( +// databaseClient: $0.databaseClient +// ) +// } +// ), detailReducer.pullback( state: \.detailState, action: /PopularAction.detail, diff --git a/EhPanda/View/Home/DataFlow/WatchedStore.swift b/EhPanda/View/Home/DataFlow/WatchedStore.swift index c349f1a3..4a0fbb72 100644 --- a/EhPanda/View/Home/DataFlow/WatchedStore.swift +++ b/EhPanda/View/Home/DataFlow/WatchedStore.swift @@ -29,8 +29,8 @@ struct WatchedState: Equatable { var loadingState: LoadingState = .idle var footerLoadingState: LoadingState = .idle - var filtersState = FiltersState() - var quickSearchState = QuickSearchState() + var filtersState = FiltersReducer.State() + var quickSearchState = QuickSearchReducer.State() @Heap var detailState: DetailState! mutating func insertGalleries(_ galleries: [Gallery]) { @@ -54,9 +54,9 @@ enum WatchedAction: BindableAction { case fetchMoreGalleries case fetchMoreGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) - case filters(FiltersAction) + case filters(FiltersReducer.Action) case detail(DetailAction) - case quickSearch(QuickSearchAction) + case quickSearch(QuickSearchReducer.Action) } struct WatchedEnvironment { @@ -183,24 +183,24 @@ let watchedReducer = Reducer.co hapticsClient: \.hapticsClient ) .binding(), - filtersReducer.pullback( - state: \.filtersState, - action: /WatchedAction.filters, - environment: { - .init( - databaseClient: $0.databaseClient - ) - } - ), - quickSearchReducer.pullback( - state: \.quickSearchState, - action: /WatchedAction.quickSearch, - environment: { - .init( - databaseClient: $0.databaseClient - ) - } - ), +// filtersReducer.pullback( +// state: \.filtersState, +// action: /WatchedAction.filters, +// environment: { +// .init( +// databaseClient: $0.databaseClient +// ) +// } +// ), +// quickSearchReducer.pullback( +// state: \.quickSearchState, +// action: /WatchedAction.quickSearch, +// environment: { +// .init( +// databaseClient: $0.databaseClient +// ) +// } +// ), detailReducer.pullback( state: \.detailState, action: /WatchedAction.detail, diff --git a/EhPanda/View/Search/SearchReducer.swift b/EhPanda/View/Search/SearchReducer.swift new file mode 100644 index 00000000..b4f0da65 --- /dev/null +++ b/EhPanda/View/Search/SearchReducer.swift @@ -0,0 +1,207 @@ +// +// SearchReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/12. +// + +import ComposableArchitecture + +struct SearchReducer: ReducerProtocol { + enum Route: Equatable { + case filters + case quickSearch + case detail(String) + } + + struct CancelID: Hashable { + let id = String(describing: SearchReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var keyword = "" + var lastKeyword = "" + + var galleries = [Gallery]() + var pageNumber = PageNumber() + var loadingState: LoadingState = .idle + var footerLoadingState: LoadingState = .idle + + var filtersState = FiltersReducer.State() + @Heap var detailState: DetailState! + var quickSearchState = QuickSearchReducer.State() + + init() { + _detailState = .init(.init()) + } + + mutating func insertGalleries(_ galleries: [Gallery]) { + galleries.forEach { gallery in + if !self.galleries.contains(gallery) { + self.galleries.append(gallery) + } + } + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + + case teardown + case fetchGalleries(String? = nil) + case fetchGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) + case fetchMoreGalleries + case fetchMoreGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) + + case detail(DetailAction) + case filters(FiltersReducer.Action) + case quickSearch(QuickSearchReducer.Action) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding(\.$keyword): + if !state.keyword.isEmpty { + state.lastKeyword = state.keyword + } + return .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .clearSubStates: + state.detailState = .init() + state.filtersState = .init() + state.quickSearchState = .init() + return .merge( + .init(value: .detail(.teardown)), + .init(value: .quickSearch(.teardown)) + ) + + case .teardown: + return .cancel(id: CancelID()) + + case .fetchGalleries(let keyword): + guard state.loadingState != .loading else { return .none } + if let keyword = keyword { + state.keyword = keyword + state.lastKeyword = keyword + } + state.loadingState = .loading + state.pageNumber.resetPages() + let filter = databaseClient.fetchFilterSynchronously(range: .search) + return SearchGalleriesRequest(keyword: state.lastKeyword, filter: filter).effect + .map(Action.fetchGalleriesDone) + .cancellable(id: CancelID()) + + case .fetchGalleriesDone(let result): + state.loadingState = .idle + switch result { + case .success(let (pageNumber, galleries)): + guard !galleries.isEmpty else { + state.loadingState = .failed(.notFound) + guard pageNumber.hasNextPage() else { return .none } + return .init(value: .fetchMoreGalleries) + } + state.pageNumber = pageNumber + state.galleries = galleries + return databaseClient.cacheGalleries(galleries).fireAndForget() + case .failure(let error): + state.loadingState = .failed(error) + } + return .none + + case .fetchMoreGalleries: + let pageNumber = state.pageNumber + guard pageNumber.hasNextPage(), + state.footerLoadingState != .loading, + let lastID = state.galleries.last?.id + else { return .none } + state.footerLoadingState = .loading + let filter = databaseClient.fetchFilterSynchronously(range: .search) + return MoreSearchGalleriesRequest(keyword: state.lastKeyword, filter: filter, lastID: lastID).effect + .map(Action.fetchMoreGalleriesDone) + .cancellable(id: CancelID()) + + case .fetchMoreGalleriesDone(let result): + state.footerLoadingState = .idle + switch result { + case .success(let (pageNumber, galleries)): + state.pageNumber = pageNumber + state.insertGalleries(galleries) + + var effects: [EffectTask] = [ + databaseClient.cacheGalleries(galleries).fireAndForget() + ] + if galleries.isEmpty, pageNumber.hasNextPage() { + effects.append(.init(value: .fetchMoreGalleries)) + } else if !galleries.isEmpty { + state.loadingState = .idle + } + return .merge(effects) + + case .failure(let error): + state.footerLoadingState = .failed(error) + } + return .none + + case .detail: + return .none + + case .filters: + return .none + + case .quickSearch: + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.quickSearch, + hapticsClient: hapticsClient + ) + .haptics( + unwrapping: \.route, + case: /Route.filters, + hapticsClient: hapticsClient + ) + + Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) + Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) + +// detailReducer.pullback( +// state: \.detailState, +// action: /SearchAction.detail, +// environment: { +// .init( +// urlClient: $0.urlClient, +// fileClient: $0.fileClient, +// imageClient: $0.imageClient, +// deviceClient: $0.deviceClient, +// hapticsClient: $0.hapticsClient, +// cookieClient: $0.cookieClient, +// databaseClient: $0.databaseClient, +// clipboardClient: $0.clipboardClient, +// appDelegateClient: $0.appDelegateClient, +// uiApplicationClient: $0.uiApplicationClient +// ) +// } +// ) + + BindingReducer() + } +} diff --git a/EhPanda/View/Search/SearchRootReducer.swift b/EhPanda/View/Search/SearchRootReducer.swift new file mode 100644 index 00000000..406d080b --- /dev/null +++ b/EhPanda/View/Search/SearchRootReducer.swift @@ -0,0 +1,210 @@ +// +// SearchRootReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/09. +// + +import ComposableArchitecture + +struct SearchRootReducer: ReducerProtocol { + enum Route: Equatable { + case search + case filters + case quickSearch + case detail(String) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var keyword = "" + var historyGalleries = [Gallery]() + + var historyKeywords = [String]() + var quickSearchWords = [QuickSearchWord]() + + var searchState = SearchReducer.State() + var filtersState = FiltersReducer.State() + var quickSearchState = QuickSearchReducer.State() + @Heap var detailState: DetailState! + + init() { + _detailState = .init(.init()) + } + + mutating func appendHistoryKeywords(_ keywords: [String]) { + guard !keywords.isEmpty else { return } + var historyKeywords = historyKeywords + + keywords.forEach { keyword in + guard !keyword.isEmpty else { return } + if let index = historyKeywords.firstIndex(where: { + $0.caseInsensitiveEqualsTo(keyword) + }) { + if historyKeywords.last != keyword { + historyKeywords.remove(at: index) + historyKeywords.append(keyword) + } + } else { + historyKeywords.append(keyword) + let overflow = historyKeywords.count - 20 + if overflow > 0 { + historyKeywords = Array( + historyKeywords.dropFirst(overflow) + ) + } + } + } + self.historyKeywords = historyKeywords + } + + mutating func removeHistoryKeyword(_ keyword: String) { + historyKeywords = historyKeywords.filter { $0 != keyword } + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case setKeyword(String) + case clearSubStates + + case syncHistoryKeywords + case fetchDatabaseInfos + case fetchDatabaseInfosDone(AppEnv) + case appendHistoryKeyword(String) + case removeHistoryKeyword(String) + case fetchHistoryGalleries + case fetchHistoryGalleriesDone([Gallery]) + + case search(SearchReducer.Action) + case filters(FiltersReducer.Action) + case quickSearch(QuickSearchReducer.Action) + case detail(DetailAction) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil + ? .merge( + .init(value: .clearSubStates), + .init(value: .fetchDatabaseInfos) + ) + : .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil + ? .merge( + .init(value: .clearSubStates), + .init(value: .fetchDatabaseInfos) + ) + : .none + + case .setKeyword(let keyword): + state.keyword = keyword + return .none + + case .clearSubStates: + state.searchState = .init() + state.detailState = .init() + state.filtersState = .init() + state.quickSearchState = .init() + return .merge( + .init(value: .search(.teardown)), + .init(value: .quickSearch(.teardown)), + .init(value: .detail(.teardown)) + ) + + case .syncHistoryKeywords: + return databaseClient.updateHistoryKeywords(state.historyKeywords).fireAndForget() + + case .fetchDatabaseInfos: + return databaseClient.fetchAppEnv().map(Action.fetchDatabaseInfosDone) + + case .fetchDatabaseInfosDone(let appEnv): + state.historyKeywords = appEnv.historyKeywords + state.quickSearchWords = appEnv.quickSearchWords + return .none + + case .appendHistoryKeyword(let keyword): + state.appendHistoryKeywords([keyword]) + return .init(value: .syncHistoryKeywords) + + case .removeHistoryKeyword(let keyword): + state.removeHistoryKeyword(keyword) + return .init(value: .syncHistoryKeywords) + + case .fetchHistoryGalleries: + return databaseClient.fetchHistoryGalleries(fetchLimit: 10).map(Action.fetchHistoryGalleriesDone) + + case .fetchHistoryGalleriesDone(let galleries): + state.historyGalleries = Array(galleries.prefix(min(galleries.count, 10))) + return .none + + case .search(.fetchGalleries(let keyword)): + if let keyword = keyword { + state.appendHistoryKeywords([keyword]) + } else { + state.appendHistoryKeywords([state.searchState.lastKeyword]) + } + return .init(value: .syncHistoryKeywords) + + case .search: + return .none + + case .filters: + return .none + + case .quickSearch: + return .none + + case .detail: + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.quickSearch, + hapticsClient: hapticsClient + ) + .haptics( + unwrapping: \.route, + case: /Route.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) + +// detailReducer.pullback( +// state: \.detailState, +// action: /SearchRootAction.detail, +// environment: { +// .init( +// urlClient: $0.urlClient, +// fileClient: $0.fileClient, +// imageClient: $0.imageClient, +// deviceClient: $0.deviceClient, +// hapticsClient: $0.hapticsClient, +// cookieClient: $0.cookieClient, +// databaseClient: $0.databaseClient, +// clipboardClient: $0.clipboardClient, +// appDelegateClient: $0.appDelegateClient, +// uiApplicationClient: $0.uiApplicationClient +// ) +// } +// ) + + BindingReducer() + } +} diff --git a/EhPanda/View/Search/SearchRootStore.swift b/EhPanda/View/Search/SearchRootStore.swift deleted file mode 100644 index 6a2a554d..00000000 --- a/EhPanda/View/Search/SearchRootStore.swift +++ /dev/null @@ -1,248 +0,0 @@ -// -// SearchRootStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/09. -// - -import ComposableArchitecture - -struct SearchRootState: Equatable { - enum Route: Equatable { - case search - case filters - case quickSearch - case detail(String) - } - - init() { - _detailState = .init(.init()) - } - - @BindingState var route: Route? - @BindingState var keyword = "" - var historyGalleries = [Gallery]() - - var historyKeywords = [String]() - var quickSearchWords = [QuickSearchWord]() - - var searchState = SearchState() - var filtersState = FiltersState() - var quickSearchState = QuickSearchState() - @Heap var detailState: DetailState! - - mutating func appendHistoryKeywords(_ keywords: [String]) { - guard !keywords.isEmpty else { return } - var historyKeywords = historyKeywords - - keywords.forEach { keyword in - guard !keyword.isEmpty else { return } - if let index = historyKeywords.firstIndex(where: { - $0.caseInsensitiveEqualsTo(keyword) - }) { - if historyKeywords.last != keyword { - historyKeywords.remove(at: index) - historyKeywords.append(keyword) - } - } else { - historyKeywords.append(keyword) - let overflow = historyKeywords.count - 20 - if overflow > 0 { - historyKeywords = Array( - historyKeywords.dropFirst(overflow) - ) - } - } - } - self.historyKeywords = historyKeywords - } - mutating func removeHistoryKeyword(_ keyword: String) { - historyKeywords = historyKeywords.filter { $0 != keyword } - } -} - -enum SearchRootAction: BindableAction { - case binding(BindingAction) - case setNavigation(SearchRootState.Route?) - case setKeyword(String) - case clearSubStates - - case syncHistoryKeywords - case fetchDatabaseInfos - case fetchDatabaseInfosDone(AppEnv) - case appendHistoryKeyword(String) - case removeHistoryKeyword(String) - case fetchHistoryGalleries - case fetchHistoryGalleriesDone([Gallery]) - - case search(SearchAction) - case filters(FiltersAction) - case quickSearch(QuickSearchAction) - case detail(DetailAction) -} - -struct SearchRootEnvironment { - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let uiApplicationClient: UIApplicationClient -} - -let searchRootReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil - ? .merge( - .init(value: .clearSubStates), - .init(value: .fetchDatabaseInfos) - ) - : .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil - ? .merge( - .init(value: .clearSubStates), - .init(value: .fetchDatabaseInfos) - ) - : .none - - case .setKeyword(let keyword): - state.keyword = keyword - return .none - - case .clearSubStates: - state.searchState = .init() - state.detailState = .init() - state.filtersState = .init() - state.quickSearchState = .init() - return .merge( - .init(value: .search(.teardown)), - .init(value: .quickSearch(.teardown)), - .init(value: .detail(.teardown)) - ) - - case .syncHistoryKeywords: - return environment.databaseClient.updateHistoryKeywords(state.historyKeywords).fireAndForget() - - case .fetchDatabaseInfos: - return environment.databaseClient.fetchAppEnv().map(SearchRootAction.fetchDatabaseInfosDone) - - case .fetchDatabaseInfosDone(let appEnv): - state.historyKeywords = appEnv.historyKeywords - state.quickSearchWords = appEnv.quickSearchWords - return .none - - case .appendHistoryKeyword(let keyword): - state.appendHistoryKeywords([keyword]) - return .init(value: .syncHistoryKeywords) - - case .removeHistoryKeyword(let keyword): - state.removeHistoryKeyword(keyword) - return .init(value: .syncHistoryKeywords) - - case .fetchHistoryGalleries: - return environment.databaseClient - .fetchHistoryGalleries(fetchLimit: 10).map(SearchRootAction.fetchHistoryGalleriesDone) - - case .fetchHistoryGalleriesDone(let galleries): - state.historyGalleries = Array(galleries.prefix(min(galleries.count, 10))) - return .none - - case .search(.fetchGalleries(let keyword)): - if let keyword = keyword { - state.appendHistoryKeywords([keyword]) - } else { - state.appendHistoryKeywords([state.searchState.lastKeyword]) - } - return .init(value: .syncHistoryKeywords) - - case .search: - return .none - - case .filters: - return .none - - case .quickSearch: - return .none - - case .detail: - return .none - } - } - .haptics( - unwrapping: \.route, - case: /SearchRootState.Route.quickSearch, - hapticsClient: \.hapticsClient - ) - .haptics( - unwrapping: \.route, - case: /SearchRootState.Route.filters, - hapticsClient: \.hapticsClient - ) - .binding(), - searchReducer.pullback( - state: \.searchState, - action: /SearchRootAction.search, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ), - filtersReducer.pullback( - state: \.filtersState, - action: /SearchRootAction.filters, - environment: { - .init( - databaseClient: $0.databaseClient - ) - } - ), - quickSearchReducer.pullback( - state: \.quickSearchState, - action: /SearchRootAction.quickSearch, - environment: { - .init( - databaseClient: $0.databaseClient - ) - } - ), - detailReducer.pullback( - state: \.detailState, - action: /SearchRootAction.detail, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ) -) diff --git a/EhPanda/View/Search/SearchRootView.swift b/EhPanda/View/Search/SearchRootView.swift index 7ac1e3e8..095a6041 100644 --- a/EhPanda/View/Search/SearchRootView.swift +++ b/EhPanda/View/Search/SearchRootView.swift @@ -9,15 +9,15 @@ import SwiftUI import ComposableArchitecture struct SearchRootView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double private let tagTranslator: TagTranslator init( - store: Store, + store: StoreOf, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store @@ -50,25 +50,25 @@ struct SearchRootView: View { } .sheet( unwrapping: viewStore.binding(\.$route), - case: /SearchRootState.Route.detail, + case: /SearchRootReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: SearchRootAction.detail), + store: store.scope(state: \.detailState, action: SearchRootReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /SearchRootState.Route.filters) { _ in - FiltersView(store: store.scope(state: \.filtersState, action: SearchRootAction.filters)) + .sheet(unwrapping: viewStore.binding(\.$route), case: /SearchRootReducer.Route.filters) { _ in + FiltersView(store: store.scope(state: \.filtersState, action: SearchRootReducer.Action.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /SearchRootState.Route.quickSearch) { _ in + .sheet(unwrapping: viewStore.binding(\.$route), case: /SearchRootReducer.Route.quickSearch) { _ in QuickSearchView( - store: store.scope(state: \.quickSearchState, action: SearchRootAction.quickSearch) + store: store.scope(state: \.quickSearchState, action: SearchRootReducer.Action.quickSearch) ) { keyword in viewStore.send(.setNavigation(nil)) viewStore.send(.setKeyword(keyword)) @@ -120,18 +120,18 @@ private extension SearchRootView { searchViewLink } var detailViewLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SearchRootState.Route.detail) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SearchRootReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: SearchRootAction.detail), + store: store.scope(state: \.detailState, action: SearchRootReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } } var searchViewLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SearchRootState.Route.search) { _ in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SearchRootReducer.Route.search) { _ in SearchView( - store: store.scope(state: \.searchState, action: SearchRootAction.search), + store: store.scope(state: \.searchState, action: SearchRootReducer.Action.search), keyword: viewStore.keyword, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -409,19 +409,7 @@ struct SearchRootView_Previews: PreviewProvider { SearchRootView( store: .init( initialState: .init(), - reducer: searchRootReducer, - environment: SearchRootEnvironment( - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - uiApplicationClient: .live - ) + reducer: SearchRootReducer() ), user: .init(), setting: .constant(.init()), diff --git a/EhPanda/View/Search/SearchStore.swift b/EhPanda/View/Search/SearchStore.swift deleted file mode 100644 index d526fd19..00000000 --- a/EhPanda/View/Search/SearchStore.swift +++ /dev/null @@ -1,227 +0,0 @@ -// -// SearchStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/12. -// - -import ComposableArchitecture - -struct SearchState: Equatable { - enum Route: Equatable { - case filters - case quickSearch - case detail(String) - } - struct CancelID: Hashable { - let id = String(describing: SearchState.self) - } - - init() { - _detailState = .init(.init()) - } - - @BindingState var route: Route? - @BindingState var keyword = "" - var lastKeyword = "" - - var galleries = [Gallery]() - var pageNumber = PageNumber() - var loadingState: LoadingState = .idle - var footerLoadingState: LoadingState = .idle - - var filtersState = FiltersState() - @Heap var detailState: DetailState! - var quickSearchState = QuickSearchState() - - mutating func insertGalleries(_ galleries: [Gallery]) { - galleries.forEach { gallery in - if !self.galleries.contains(gallery) { - self.galleries.append(gallery) - } - } - } -} - -enum SearchAction: BindableAction { - case binding(BindingAction) - case setNavigation(SearchState.Route?) - case clearSubStates - - case teardown - case fetchGalleries(String? = nil) - case fetchGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) - case fetchMoreGalleries - case fetchMoreGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) - - case detail(DetailAction) - case filters(FiltersAction) - case quickSearch(QuickSearchAction) -} - -struct SearchEnvironment { - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let uiApplicationClient: UIApplicationClient -} - -let searchReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding(\.$keyword): - if !state.keyword.isEmpty { - state.lastKeyword = state.keyword - } - return .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .clearSubStates: - state.detailState = .init() - state.filtersState = .init() - state.quickSearchState = .init() - return .merge( - .init(value: .detail(.teardown)), - .init(value: .quickSearch(.teardown)) - ) - - case .teardown: - return .cancel(id: SearchState.CancelID()) - - case .fetchGalleries(let keyword): - guard state.loadingState != .loading else { return .none } - if let keyword = keyword { - state.keyword = keyword - state.lastKeyword = keyword - } - state.loadingState = .loading - state.pageNumber.resetPages() - let filter = environment.databaseClient.fetchFilterSynchronously(range: .search) - return SearchGalleriesRequest(keyword: state.lastKeyword, filter: filter).effect - .map(SearchAction.fetchGalleriesDone) - .cancellable(id: SearchState.CancelID()) - - case .fetchGalleriesDone(let result): - state.loadingState = .idle - switch result { - case .success(let (pageNumber, galleries)): - guard !galleries.isEmpty else { - state.loadingState = .failed(.notFound) - guard pageNumber.hasNextPage() else { return .none } - return .init(value: .fetchMoreGalleries) - } - state.pageNumber = pageNumber - state.galleries = galleries - return environment.databaseClient.cacheGalleries(galleries).fireAndForget() - case .failure(let error): - state.loadingState = .failed(error) - } - return .none - - case .fetchMoreGalleries: - let pageNumber = state.pageNumber - guard pageNumber.hasNextPage(), - state.footerLoadingState != .loading, - let lastID = state.galleries.last?.id - else { return .none } - state.footerLoadingState = .loading - let filter = environment.databaseClient.fetchFilterSynchronously(range: .search) - return MoreSearchGalleriesRequest(keyword: state.lastKeyword, filter: filter, lastID: lastID).effect - .map(SearchAction.fetchMoreGalleriesDone) - .cancellable(id: SearchState.CancelID()) - - case .fetchMoreGalleriesDone(let result): - state.footerLoadingState = .idle - switch result { - case .success(let (pageNumber, galleries)): - state.pageNumber = pageNumber - state.insertGalleries(galleries) - - var effects: [EffectTask] = [ - environment.databaseClient.cacheGalleries(galleries).fireAndForget() - ] - if galleries.isEmpty, pageNumber.hasNextPage() { - effects.append(.init(value: .fetchMoreGalleries)) - } else if !galleries.isEmpty { - state.loadingState = .idle - } - return .merge(effects) - - case .failure(let error): - state.footerLoadingState = .failed(error) - } - return .none - - case .detail: - return .none - - case .filters: - return .none - - case .quickSearch: - return .none - } - } - .haptics( - unwrapping: \.route, - case: /SearchState.Route.quickSearch, - hapticsClient: \.hapticsClient - ) - .haptics( - unwrapping: \.route, - case: /SearchState.Route.filters, - hapticsClient: \.hapticsClient - ) - .binding(), - filtersReducer.pullback( - state: \.filtersState, - action: /SearchAction.filters, - environment: { - .init( - databaseClient: $0.databaseClient - ) - } - ), - quickSearchReducer.pullback( - state: \.quickSearchState, - action: /SearchAction.quickSearch, - environment: { - .init( - databaseClient: $0.databaseClient - ) - } - ), - detailReducer.pullback( - state: \.detailState, - action: /SearchAction.detail, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ) -) diff --git a/EhPanda/View/Search/SearchView.swift b/EhPanda/View/Search/SearchView.swift index ea8cd187..eeee29b8 100644 --- a/EhPanda/View/Search/SearchView.swift +++ b/EhPanda/View/Search/SearchView.swift @@ -9,8 +9,8 @@ import SwiftUI import ComposableArchitecture struct SearchView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let keyword: String private let user: User @Binding private var setting: Setting @@ -18,7 +18,7 @@ struct SearchView: View { private let tagTranslator: TagTranslator init( - store: Store, + store: StoreOf, keyword: String, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store @@ -46,21 +46,21 @@ struct SearchView: View { ) .sheet( unwrapping: viewStore.binding(\.$route), - case: /SearchState.Route.detail, + case: /SearchReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: SearchAction.detail), + store: store.scope(state: \.detailState, action: SearchReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /SearchState.Route.quickSearch) { _ in + .sheet(unwrapping: viewStore.binding(\.$route), case: /SearchReducer.Route.quickSearch) { _ in QuickSearchView( - store: store.scope(state: \.quickSearchState, action: SearchAction.quickSearch) + store: store.scope(state: \.quickSearchState, action: SearchReducer.Action.quickSearch) ) { keyword in viewStore.send(.setNavigation(nil)) viewStore.send(.fetchGalleries(keyword)) @@ -68,8 +68,8 @@ struct SearchView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /SearchState.Route.filters) { _ in - FiltersView(store: store.scope(state: \.filtersState, action: SearchAction.filters)) + .sheet(unwrapping: viewStore.binding(\.$route), case: /SearchReducer.Route.filters) { _ in + FiltersView(store: store.scope(state: \.filtersState, action: SearchReducer.Action.filters)) .accentColor(setting.accentColor).autoBlur(radius: blurRadius) } .searchable(text: viewStore.binding(\.$keyword)) { @@ -95,9 +95,9 @@ struct SearchView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SearchState.Route.detail) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SearchReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: SearchAction.detail), + store: store.scope(state: \.detailState, action: SearchReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -123,19 +123,7 @@ struct SearchView_Previews: PreviewProvider { SearchView( store: .init( initialState: .init(), - reducer: searchReducer, - environment: SearchEnvironment( - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - uiApplicationClient: .live - ) + reducer: SearchReducer() ), keyword: .init(), user: .init(), diff --git a/EhPanda/View/Search/Support/QuickSearchReducer.swift b/EhPanda/View/Search/Support/QuickSearchReducer.swift new file mode 100644 index 00000000..ab3c13b3 --- /dev/null +++ b/EhPanda/View/Search/Support/QuickSearchReducer.swift @@ -0,0 +1,134 @@ +// +// QuickSearchReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/20. +// + +import SwiftUI +import ComposableArchitecture + +struct QuickSearchReducer: ReducerProtocol { + enum Route: Equatable { + case newWord + case editWord + case deleteWord(QuickSearchWord) + } + + enum FocusField { + case name + case content + } + + struct CancelID: Hashable { + let id = String(describing: QuickSearchReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var focusedField: FocusField? + @BindingState var editingWord: QuickSearchWord = .empty + @BindingState var listEditMode: EditMode = .inactive + var isListEditing: Bool { + get { listEditMode == .active } + set { listEditMode = newValue ? .active : .inactive } + } + + var loadingState: LoadingState = .idle + var quickSearchWords = [QuickSearchWord]() + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + + case syncQuickSearchWords + + case toggleListEditing + case setEditingWord(QuickSearchWord) + + case appendWord + case editWord + case deleteWord(QuickSearchWord) + case deleteWordWithOffsets(IndexSet) + case moveWord(IndexSet, Int) + + case teardown + case fetchQuickSearchWords + case fetchQuickSearchWordsDone([QuickSearchWord]) + } + + @Dependency(\.databaseClient) private var databaseClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .clearSubStates: + state.focusedField = nil + state.editingWord = .empty + return .none + + case .syncQuickSearchWords: + return databaseClient.updateQuickSearchWords(state.quickSearchWords).fireAndForget() + + case .toggleListEditing: + state.isListEditing.toggle() + return .none + + case .setEditingWord(let word): + state.editingWord = word + return .none + + case .appendWord: + state.quickSearchWords.append(state.editingWord) + return .init(value: .syncQuickSearchWords) + + case .editWord: + if let index = state.quickSearchWords.firstIndex(where: { $0.id == state.editingWord.id }) { + state.quickSearchWords[index] = state.editingWord + return .init(value: .syncQuickSearchWords) + } + return .none + + case .deleteWord(let word): + state.quickSearchWords = state.quickSearchWords.filter({ $0 != word }) + return .init(value: .syncQuickSearchWords) + + case .deleteWordWithOffsets(let offsets): + state.quickSearchWords.remove(atOffsets: offsets) + return .init(value: .syncQuickSearchWords) + + case .moveWord(let source, let destination): + state.quickSearchWords.move(fromOffsets: source, toOffset: destination) + return .init(value: .syncQuickSearchWords) + + case .teardown: + return .cancel(id: CancelID()) + + case .fetchQuickSearchWords: + state.loadingState = .loading + return databaseClient.fetchQuickSearchWords() + .map(Action.fetchQuickSearchWordsDone) + .cancellable(id: CancelID()) + + case .fetchQuickSearchWordsDone(let words): + state.loadingState = .idle + state.quickSearchWords = words + return .none + } + } + + BindingReducer() + } +} diff --git a/EhPanda/View/Search/Support/QuickSearchStore.swift b/EhPanda/View/Search/Support/QuickSearchStore.swift deleted file mode 100644 index aa32108b..00000000 --- a/EhPanda/View/Search/Support/QuickSearchStore.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// QuickSearchStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/20. -// - -import SwiftUI -import ComposableArchitecture - -struct QuickSearchState: Equatable { - enum Route: Equatable { - case newWord - case editWord - case deleteWord(QuickSearchWord) - } - enum FocusField { - case name - case content - } - struct CancelID: Hashable { - let id = String(describing: QuickSearchState.self) - } - - @BindingState var route: Route? - @BindingState var focusedField: FocusField? - @BindingState var editingWord: QuickSearchWord = .empty - @BindingState var listEditMode: EditMode = .inactive - var isListEditing: Bool { - get { listEditMode == .active } - set { listEditMode = newValue ? .active : .inactive } - } - - var loadingState: LoadingState = .idle - var quickSearchWords = [QuickSearchWord]() -} - -enum QuickSearchAction: BindableAction { - case binding(BindingAction) - case setNavigation(QuickSearchState.Route?) - case clearSubStates - - case syncQuickSearchWords - - case toggleListEditing - case setEditingWord(QuickSearchWord) - - case appendWord - case editWord - case deleteWord(QuickSearchWord) - case deleteWordWithOffsets(IndexSet) - case moveWord(IndexSet, Int) - - case teardown - case fetchQuickSearchWords - case fetchQuickSearchWordsDone([QuickSearchWord]) -} - -struct QuickSearchEnvironment { - let databaseClient: DatabaseClient -} - -let quickSearchReducer = Reducer -{ state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .clearSubStates: - state.focusedField = nil - state.editingWord = .empty - return .none - - case .syncQuickSearchWords: - return environment.databaseClient.updateQuickSearchWords(state.quickSearchWords).fireAndForget() - - case .toggleListEditing: - state.isListEditing.toggle() - return .none - - case .setEditingWord(let word): - state.editingWord = word - return .none - - case .appendWord: - state.quickSearchWords.append(state.editingWord) - return .init(value: .syncQuickSearchWords) - - case .editWord: - if let index = state.quickSearchWords.firstIndex(where: { $0.id == state.editingWord.id }) { - state.quickSearchWords[index] = state.editingWord - return .init(value: .syncQuickSearchWords) - } - return .none - - case .deleteWord(let word): - state.quickSearchWords = state.quickSearchWords.filter({ $0 != word }) - return .init(value: .syncQuickSearchWords) - - case .deleteWordWithOffsets(let offsets): - state.quickSearchWords.remove(atOffsets: offsets) - return .init(value: .syncQuickSearchWords) - - case .moveWord(let source, let destination): - state.quickSearchWords.move(fromOffsets: source, toOffset: destination) - return .init(value: .syncQuickSearchWords) - - case .teardown: - return .cancel(id: QuickSearchState.CancelID()) - - case .fetchQuickSearchWords: - state.loadingState = .loading - return environment.databaseClient - .fetchQuickSearchWords().map(QuickSearchAction.fetchQuickSearchWordsDone) - .cancellable(id: QuickSearchState.CancelID()) - - case .fetchQuickSearchWordsDone(let words): - state.loadingState = .idle - state.quickSearchWords = words - return .none - } -} -.binding() diff --git a/EhPanda/View/Search/Support/QuickSearchView.swift b/EhPanda/View/Search/Support/QuickSearchView.swift index 26aaa632..62e8e591 100644 --- a/EhPanda/View/Search/Support/QuickSearchView.swift +++ b/EhPanda/View/Search/Support/QuickSearchView.swift @@ -9,13 +9,13 @@ import SwiftUI import ComposableArchitecture struct QuickSearchView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let searchAction: (String) -> Void - @FocusState private var focusedField: QuickSearchState.FocusField? + @FocusState private var focusedField: QuickSearchReducer.FocusField? - init(store: Store, searchAction: @escaping (String) -> Void) { + init(store: StoreOf, searchAction: @escaping (String) -> Void) { self.store = store viewStore = ViewStore(store) self.searchAction = searchAction @@ -55,7 +55,7 @@ struct QuickSearchView: View { .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.delete, unwrapping: viewStore.binding(\.$route), - case: /QuickSearchState.Route.deleteWord, + case: /QuickSearchReducer.Route.deleteWord, matching: word ) { route in Button(L10n.Localizable.ConfirmationDialog.Button.delete, role: .destructive) { @@ -123,7 +123,7 @@ struct QuickSearchView: View { } } @ViewBuilder private var navigationLinks: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /QuickSearchState.Route.newWord) { _ in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /QuickSearchReducer.Route.newWord) { _ in EditWordView( title: L10n.Localizable.QuickSearchView.Title.newWord, word: viewStore.binding(\.$editingWord), @@ -135,7 +135,7 @@ struct QuickSearchView: View { } ) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /QuickSearchState.Route.editWord) { _ in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /QuickSearchReducer.Route.editWord) { _ in EditWordView( title: L10n.Localizable.QuickSearchView.Title.editWord, word: viewStore.binding(\.$editingWord), @@ -155,13 +155,13 @@ extension QuickSearchView { struct EditWordView: View { private let title: String @Binding private var word: QuickSearchWord - private let focusedField: FocusState.Binding + private let focusedField: FocusState.Binding private let submitAction: () -> Void private let confirmAction: () -> Void init( title: String, word: Binding, - focusedField: FocusState.Binding, + focusedField: FocusState.Binding, submitAction: @escaping () -> Void, confirmAction: @escaping () -> Void ) { self.title = title @@ -204,10 +204,7 @@ struct QuickSearchView_Previews: PreviewProvider { QuickSearchView( store: .init( initialState: .init(), - reducer: quickSearchReducer, - environment: QuickSearchEnvironment( - databaseClient: .live - ) + reducer: QuickSearchReducer() ), searchAction: { _ in } ) diff --git a/EhPanda/View/Support/FiltersReducer.swift b/EhPanda/View/Support/FiltersReducer.swift new file mode 100644 index 00000000..cde3c73a --- /dev/null +++ b/EhPanda/View/Support/FiltersReducer.swift @@ -0,0 +1,114 @@ +// +// FiltersReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/09. +// + +import ComposableArchitecture + +struct FiltersReducer: ReducerProtocol { + enum Route { + case resetFilters + } + + enum FocusedBound { + case lower + case upper + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var filterRange: FilterRange = .search + @BindingState var focusedBound: FocusedBound? + + @BindingState var searchFilter = Filter() + @BindingState var globalFilter = Filter() + @BindingState var watchedFilter = Filter() + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case setNavigation(Route?) + case onTextFieldSubmitted + + case syncFilter(FilterRange) + case resetFilters + case fetchFilters + case fetchFiltersDone(AppEnv) + } + + @Dependency(\.databaseClient) private var databaseClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$searchFilter): + state.searchFilter.fixInvalidData() + return .init(value: .syncFilter(.search)) + + case .binding(\.$globalFilter): + state.globalFilter.fixInvalidData() + return .init(value: .syncFilter(.global)) + + case .binding(\.$watchedFilter): + state.watchedFilter.fixInvalidData() + return .init(value: .syncFilter(.watched)) + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return .none + + case .onTextFieldSubmitted: + switch state.focusedBound { + case .lower: + state.focusedBound = .upper + case .upper: + state.focusedBound = nil + default: + break + } + return .none + + case .syncFilter(let range): + let filter: Filter + switch range { + case .search: + filter = state.searchFilter + case .global: + filter = state.globalFilter + case .watched: + filter = state.watchedFilter + } + return databaseClient.updateFilter(filter, range: range).fireAndForget() + + case .resetFilters: + switch state.filterRange { + case .search: + state.searchFilter = .init() + return .init(value: .syncFilter(.search)) + case .global: + state.globalFilter = .init() + return .init(value: .syncFilter(.global)) + case .watched: + state.watchedFilter = .init() + return .init(value: .syncFilter(.watched)) + } + + case .fetchFilters: + return databaseClient.fetchAppEnv().map(Action.fetchFiltersDone) + + case .fetchFiltersDone(let appEnv): + state.searchFilter = appEnv.searchFilter + state.globalFilter = appEnv.globalFilter + state.watchedFilter = appEnv.watchedFilter + return .none + } + } + + BindingReducer() + } +} diff --git a/EhPanda/View/Support/FiltersStore.swift b/EhPanda/View/Support/FiltersStore.swift deleted file mode 100644 index bfd8dfcb..00000000 --- a/EhPanda/View/Support/FiltersStore.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// FiltersStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/09. -// - -import ComposableArchitecture - -struct FiltersState: Equatable { - enum Route { - case resetFilters - } - enum FocusedBound { - case lower - case upper - } - - @BindingState var route: Route? - @BindingState var filterRange: FilterRange = .search - @BindingState var focusedBound: FocusedBound? - - @BindingState var searchFilter = Filter() - @BindingState var globalFilter = Filter() - @BindingState var watchedFilter = Filter() -} - -enum FiltersAction: BindableAction { - case binding(BindingAction) - case setNavigation(FiltersState.Route?) - case onTextFieldSubmitted - - case syncFilter(FilterRange) - case resetFilters - case fetchFilters - case fetchFiltersDone(AppEnv) -} - -struct FiltersEnvironment { - let databaseClient: DatabaseClient -} - -let filtersReducer = Reducer { state, action, environment in - switch action { - case .binding(\.$searchFilter): - state.searchFilter.fixInvalidData() - return .init(value: .syncFilter(.search)) - - case .binding(\.$globalFilter): - state.globalFilter.fixInvalidData() - return .init(value: .syncFilter(.global)) - - case .binding(\.$watchedFilter): - state.watchedFilter.fixInvalidData() - return .init(value: .syncFilter(.watched)) - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return .none - - case .onTextFieldSubmitted: - switch state.focusedBound { - case .lower: - state.focusedBound = .upper - case .upper: - state.focusedBound = nil - default: - break - } - return .none - - case .syncFilter(let range): - let filter: Filter - switch range { - case .search: - filter = state.searchFilter - case .global: - filter = state.globalFilter - case .watched: - filter = state.watchedFilter - } - return environment.databaseClient.updateFilter(filter, range: range).fireAndForget() - - case .resetFilters: - switch state.filterRange { - case .search: - state.searchFilter = .init() - return .init(value: .syncFilter(.search)) - case .global: - state.globalFilter = .init() - return .init(value: .syncFilter(.global)) - case .watched: - state.watchedFilter = .init() - return .init(value: .syncFilter(.watched)) - } - - case .fetchFilters: - return environment.databaseClient.fetchAppEnv().map(FiltersAction.fetchFiltersDone) - - case .fetchFiltersDone(let appEnv): - state.searchFilter = appEnv.searchFilter - state.globalFilter = appEnv.globalFilter - state.watchedFilter = appEnv.watchedFilter - return .none - } -} -.binding() diff --git a/EhPanda/View/Support/FiltersView.swift b/EhPanda/View/Support/FiltersView.swift index d98af131..5ac42d18 100644 --- a/EhPanda/View/Support/FiltersView.swift +++ b/EhPanda/View/Support/FiltersView.swift @@ -9,12 +9,12 @@ import SwiftUI import ComposableArchitecture struct FiltersView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf - @FocusState private var focusedBound: FiltersState.FocusedBound? + @FocusState private var focusedBound: FiltersReducer.FocusedBound? - init(store: Store) { + init(store: StoreOf) { self.store = store viewStore = ViewStore(store) } @@ -54,7 +54,7 @@ struct FiltersView: View { // MARK: BasicSection private struct BasicSection: View { - @Binding private var route: FiltersState.Route? + @Binding private var route: FiltersReducer.Route? @Binding private var filter: Filter @Binding private var filterRange: FilterRange private let resetFiltersAction: () -> Void @@ -65,7 +65,7 @@ private struct BasicSection: View { ] } init( - route: Binding, filter: Binding, filterRange: Binding, + route: Binding, filter: Binding, filterRange: Binding, resetFiltersAction: @escaping () -> Void, resetFiltersDialogAction: @escaping () -> Void ) { _route = route @@ -89,7 +89,7 @@ private struct BasicSection: View { } .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.reset, - unwrapping: $route, case: /FiltersState.Route.resetFilters + unwrapping: $route, case: /FiltersReducer.Route.resetFilters ) { Button( L10n.Localizable.ConfirmationDialog.Button.reset, @@ -104,12 +104,12 @@ private struct BasicSection: View { // MARK: AdvancedSection private struct AdvancedSection: View { @Binding private var filter: Filter - private let focusedBound: FocusState.Binding + private let focusedBound: FocusState.Binding private let submitAction: () -> Void init( filter: Binding, - focusedBound: FocusState.Binding, + focusedBound: FocusState.Binding, submitAction: @escaping () -> Void ) { _filter = filter @@ -178,13 +178,13 @@ private struct MinimumRatingSetter: View { private struct PagesRangeSetter: View { @Binding private var lowerBound: String @Binding private var upperBound: String - private let focusedBound: FocusState.Binding + private let focusedBound: FocusState.Binding private let submitAction: () -> Void init( lowerBound: Binding, upperBound: Binding, - focusedBound: FocusState.Binding, + focusedBound: FocusState.Binding, submitAction: @escaping () -> Void ) { _lowerBound = lowerBound @@ -242,10 +242,7 @@ struct FiltersView_Previews: PreviewProvider { FiltersView( store: .init( initialState: .init(), - reducer: filtersReducer, - environment: FiltersEnvironment( - databaseClient: .live - ) + reducer: FiltersReducer() ) ) } From 824b7ea2d1b642e98aeba20b582956e92824592d Mon Sep 17 00:00:00 2001 From: Chihchy Date: Fri, 26 May 2023 12:28:47 +0800 Subject: [PATCH 13/29] Refactor DetailReducer Refactor ReadingReducer Refactor ArchivesReducer Refactor TorrentsReducer Refactor PreviewsReducer Refactor CommentsReducer Refactor GalleryInfosReducer Refactor DetailSearchReducer --- EhPanda.xcodeproj/project.pbxproj | 138 ++-- .../Tools/Extensions/Reducer_Extension.swift | 18 + EhPanda/DataFlow/AppRouteStore.swift | 43 +- .../Detail/Archives/ArchivesReducer.swift | 147 +++++ .../Detail/{ => Archives}/ArchivesView.swift | 17 +- .../Detail/Comments/CommentsReducer.swift | 226 +++++++ .../Detail/{ => Comments}/CommentsView.swift | 28 +- .../{Support => Components}/LinkedText.swift | 0 .../PostCommentView.swift | 0 .../{Support => Components}/RatingView.swift | 0 .../TagDetailView.swift | 0 .../View/Detail/DataFlow/ArchivesStore.swift | 139 ---- .../View/Detail/DataFlow/CommentsStore.swift | 227 ------- .../Detail/DataFlow/DetailSearchStore.swift | 209 ------ .../View/Detail/DataFlow/DetailStore.swift | 524 ---------------- .../Detail/DataFlow/GalleryInfosStore.swift | 44 -- .../View/Detail/DataFlow/PreviewsStore.swift | 161 ----- .../View/Detail/DataFlow/TorrentsStore.swift | 108 ---- EhPanda/View/Detail/DetailReducer.swift | 420 +++++++++++++ .../DetailSearch/DetailSearchReducer.swift | 187 ++++++ .../{ => DetailSearch}/DetailSearchView.swift | 36 +- EhPanda/View/Detail/DetailView.swift | 56 +- .../GalleryInfos/GalleryInfosReducer.swift | 46 ++ .../{ => GalleryInfos}/GalleryInfosView.swift | 14 +- .../Detail/Previews/PreviewsReducer.swift | 143 +++++ .../Detail/{ => Previews}/PreviewsView.swift | 22 +- .../Detail/Torrents/TorrentsReducer.swift | 112 ++++ .../Detail/{ => Torrents}/TorrentsView.swift | 17 +- EhPanda/View/Favorites/FavoritesStore.swift | 43 +- .../View/Home/DataFlow/FrontpageStore.swift | 43 +- EhPanda/View/Home/DataFlow/HistoryStore.swift | 43 +- EhPanda/View/Home/DataFlow/HomeStore.swift | 41 +- EhPanda/View/Home/DataFlow/PopularStore.swift | 43 +- .../View/Home/DataFlow/ToplistsStore.swift | 43 +- EhPanda/View/Home/DataFlow/WatchedStore.swift | 43 +- EhPanda/View/Reading/ReadingReducer.swift | 593 ++++++++++++++++++ EhPanda/View/Reading/ReadingStore.swift | 587 ----------------- EhPanda/View/Reading/ReadingView.swift | 24 +- EhPanda/View/Search/SearchReducer.swift | 24 +- EhPanda/View/Search/SearchRootReducer.swift | 24 +- 40 files changed, 2234 insertions(+), 2399 deletions(-) create mode 100644 EhPanda/View/Detail/Archives/ArchivesReducer.swift rename EhPanda/View/Detail/{ => Archives}/ArchivesView.swift (93%) create mode 100644 EhPanda/View/Detail/Comments/CommentsReducer.swift rename EhPanda/View/Detail/{ => Comments}/CommentsView.swift (91%) rename EhPanda/View/Detail/{Support => Components}/LinkedText.swift (100%) rename EhPanda/View/Detail/{Support => Components}/PostCommentView.swift (100%) rename EhPanda/View/Detail/{Support => Components}/RatingView.swift (100%) rename EhPanda/View/Detail/{Support => Components}/TagDetailView.swift (100%) delete mode 100644 EhPanda/View/Detail/DataFlow/ArchivesStore.swift delete mode 100644 EhPanda/View/Detail/DataFlow/CommentsStore.swift delete mode 100644 EhPanda/View/Detail/DataFlow/DetailSearchStore.swift delete mode 100644 EhPanda/View/Detail/DataFlow/DetailStore.swift delete mode 100644 EhPanda/View/Detail/DataFlow/GalleryInfosStore.swift delete mode 100644 EhPanda/View/Detail/DataFlow/PreviewsStore.swift delete mode 100644 EhPanda/View/Detail/DataFlow/TorrentsStore.swift create mode 100644 EhPanda/View/Detail/DetailReducer.swift create mode 100644 EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift rename EhPanda/View/Detail/{ => DetailSearch}/DetailSearchView.swift (80%) create mode 100644 EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift rename EhPanda/View/Detail/{ => GalleryInfos}/GalleryInfosView.swift (90%) create mode 100644 EhPanda/View/Detail/Previews/PreviewsReducer.swift rename EhPanda/View/Detail/{ => Previews}/PreviewsView.swift (81%) create mode 100644 EhPanda/View/Detail/Torrents/TorrentsReducer.swift rename EhPanda/View/Detail/{ => Torrents}/TorrentsView.swift (87%) create mode 100644 EhPanda/View/Reading/ReadingReducer.swift delete mode 100644 EhPanda/View/Reading/ReadingStore.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 98038359..42e5ef6d 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -134,7 +134,7 @@ AB706F9D278ACCA20025A48A /* SearchRootReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F9C278ACCA20025A48A /* SearchRootReducer.swift */; }; AB706F9F278AD4800025A48A /* GalleryHistoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F9E278AD4800025A48A /* GalleryHistoryCell.swift */; }; AB706FA1278BCEC60025A48A /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706FA0278BCEC60025A48A /* DetailView.swift */; }; - AB706FA3278BCF2F0025A48A /* DetailStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706FA2278BCF2F0025A48A /* DetailStore.swift */; }; + AB706FA3278BCF2F0025A48A /* DetailReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706FA2278BCF2F0025A48A /* DetailReducer.swift */; }; AB706FA5278C3DDE0025A48A /* PreviewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706FA4278C3DDE0025A48A /* PreviewsView.swift */; }; AB7B29F226AC471E00EE1F14 /* Model5toModel6MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7B29F126AC471E00EE1F14 /* Model5toModel6MigrationPolicy.swift */; }; AB7B29F626AC741600EE1F14 /* GenericList.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7B29F526AC741600EE1F14 /* GenericList.swift */; }; @@ -192,19 +192,19 @@ ABBB2636278FB888007B6149 /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = ABBB2635278FB888007B6149 /* SwiftUINavigation */; }; ABBB2638278FBD2F007B6149 /* SwiftUINavigation_Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2637278FBD2F007B6149 /* SwiftUINavigation_Extension.swift */; }; ABBB263A2792588F007B6149 /* TTProgressHUD_Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB26392792588F007B6149 /* TTProgressHUD_Extension.swift */; }; - ABBB263E2793C648007B6149 /* PreviewsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB263D2793C648007B6149 /* PreviewsStore.swift */; }; - ABBB2640279417EC007B6149 /* CommentsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB263F279417EC007B6149 /* CommentsStore.swift */; }; + ABBB263E2793C648007B6149 /* PreviewsReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB263D2793C648007B6149 /* PreviewsReducer.swift */; }; + ABBB2640279417EC007B6149 /* CommentsReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB263F279417EC007B6149 /* CommentsReducer.swift */; }; ABBB264227942B74007B6149 /* URLClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB264127942B74007B6149 /* URLClient.swift */; }; - ABBB266627977C2A007B6149 /* ArchivesStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB266527977C2A007B6149 /* ArchivesStore.swift */; }; + ABBB266627977C2A007B6149 /* ArchivesReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB266527977C2A007B6149 /* ArchivesReducer.swift */; }; ABBB26682797BFAA007B6149 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB26672797BFAA007B6149 /* ActivityView.swift */; }; - ABBB266A2797C61F007B6149 /* TorrentsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB26692797C61F007B6149 /* TorrentsStore.swift */; }; + ABBB266A2797C61F007B6149 /* TorrentsReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB26692797C61F007B6149 /* TorrentsReducer.swift */; }; ABBB266C2797E882007B6149 /* ClipboardClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB266B2797E882007B6149 /* ClipboardClient.swift */; }; ABBB266E27998479007B6149 /* QuickSearchReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB266D27998479007B6149 /* QuickSearchReducer.swift */; }; ABBB2671279AFA61007B6149 /* EnvironmentKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2670279AFA61007B6149 /* EnvironmentKeys.swift */; }; ABBB2673279B9332007B6149 /* ReadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2672279B9332007B6149 /* ReadingView.swift */; }; - ABBB2675279B933D007B6149 /* ReadingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2674279B933D007B6149 /* ReadingStore.swift */; }; + ABBB2675279B933D007B6149 /* ReadingReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2674279B933D007B6149 /* ReadingReducer.swift */; }; ABBB2677279CDBB0007B6149 /* ImageClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2676279CDBB0007B6149 /* ImageClient.swift */; }; - ABBB2679279D454C007B6149 /* GalleryInfosStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2678279D454C007B6149 /* GalleryInfosStore.swift */; }; + ABBB2679279D454C007B6149 /* GalleryInfosReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2678279D454C007B6149 /* GalleryInfosReducer.swift */; }; ABBC332826BE31AE0084A331 /* EhSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBC332726BE31AE0084A331 /* EhSettingView.swift */; }; ABBC332A26BE7C940084A331 /* SettingTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBC332926BE7C940084A331 /* SettingTextField.swift */; }; ABBCCC9026C95F6E007D8A36 /* GalleryInfosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBCCC8F26C95F6E007D8A36 /* GalleryInfosView.swift */; }; @@ -221,7 +221,7 @@ ABC732C527B9024500D47DA9 /* LiveText.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC732C427B9024500D47DA9 /* LiveText.swift */; }; ABC732C727B90F0900D47DA9 /* LiveTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC732C627B90F0900D47DA9 /* LiveTextView.swift */; }; ABC8355D27B118330091DCDB /* DetailSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC8355C27B118330091DCDB /* DetailSearchView.swift */; }; - ABC8355F27B118370091DCDB /* DetailSearchStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC8355E27B118370091DCDB /* DetailSearchStore.swift */; }; + ABC8355F27B118370091DCDB /* DetailSearchReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC8355E27B118370091DCDB /* DetailSearchReducer.swift */; }; ABC8356127B357C50091DCDB /* GestureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC8356027B357C50091DCDB /* GestureHandler.swift */; }; ABC8356327B366760091DCDB /* PageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC8356227B366760091DCDB /* PageHandler.swift */; }; ABC8356527B36E550091DCDB /* AutoPlayHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC8356427B36E550091DCDB /* AutoPlayHandler.swift */; }; @@ -434,7 +434,7 @@ AB706F9C278ACCA20025A48A /* SearchRootReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRootReducer.swift; sourceTree = ""; }; AB706F9E278AD4800025A48A /* GalleryHistoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryHistoryCell.swift; sourceTree = ""; }; AB706FA0278BCEC60025A48A /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; - AB706FA2278BCF2F0025A48A /* DetailStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailStore.swift; sourceTree = ""; }; + AB706FA2278BCF2F0025A48A /* DetailReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailReducer.swift; sourceTree = ""; }; AB706FA4278C3DDE0025A48A /* PreviewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsView.swift; sourceTree = ""; }; AB7B29F126AC471E00EE1F14 /* Model5toModel6MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model5toModel6MigrationPolicy.swift; sourceTree = ""; }; AB7B29F526AC741600EE1F14 /* GenericList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericList.swift; sourceTree = ""; }; @@ -495,19 +495,19 @@ ABBB2630278E6EF3007B6149 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; ABBB2637278FBD2F007B6149 /* SwiftUINavigation_Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUINavigation_Extension.swift; sourceTree = ""; }; ABBB26392792588F007B6149 /* TTProgressHUD_Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTProgressHUD_Extension.swift; sourceTree = ""; }; - ABBB263D2793C648007B6149 /* PreviewsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsStore.swift; sourceTree = ""; }; - ABBB263F279417EC007B6149 /* CommentsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsStore.swift; sourceTree = ""; }; + ABBB263D2793C648007B6149 /* PreviewsReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewsReducer.swift; sourceTree = ""; }; + ABBB263F279417EC007B6149 /* CommentsReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsReducer.swift; sourceTree = ""; }; ABBB264127942B74007B6149 /* URLClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLClient.swift; sourceTree = ""; }; - ABBB266527977C2A007B6149 /* ArchivesStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchivesStore.swift; sourceTree = ""; }; + ABBB266527977C2A007B6149 /* ArchivesReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchivesReducer.swift; sourceTree = ""; }; ABBB26672797BFAA007B6149 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; - ABBB26692797C61F007B6149 /* TorrentsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentsStore.swift; sourceTree = ""; }; + ABBB26692797C61F007B6149 /* TorrentsReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentsReducer.swift; sourceTree = ""; }; ABBB266B2797E882007B6149 /* ClipboardClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardClient.swift; sourceTree = ""; }; ABBB266D27998479007B6149 /* QuickSearchReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSearchReducer.swift; sourceTree = ""; }; ABBB2670279AFA61007B6149 /* EnvironmentKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentKeys.swift; sourceTree = ""; }; ABBB2672279B9332007B6149 /* ReadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadingView.swift; sourceTree = ""; }; - ABBB2674279B933D007B6149 /* ReadingStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadingStore.swift; sourceTree = ""; }; + ABBB2674279B933D007B6149 /* ReadingReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadingReducer.swift; sourceTree = ""; }; ABBB2676279CDBB0007B6149 /* ImageClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageClient.swift; sourceTree = ""; }; - ABBB2678279D454C007B6149 /* GalleryInfosStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryInfosStore.swift; sourceTree = ""; }; + ABBB2678279D454C007B6149 /* GalleryInfosReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryInfosReducer.swift; sourceTree = ""; }; ABBC332726BE31AE0084A331 /* EhSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EhSettingView.swift; sourceTree = ""; }; ABBC332926BE7C940084A331 /* SettingTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingTextField.swift; sourceTree = ""; }; ABBCCC8F26C95F6E007D8A36 /* GalleryInfosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryInfosView.swift; sourceTree = ""; }; @@ -526,7 +526,7 @@ ABC732C427B9024500D47DA9 /* LiveText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveText.swift; sourceTree = ""; }; ABC732C627B90F0900D47DA9 /* LiveTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTextView.swift; sourceTree = ""; }; ABC8355C27B118330091DCDB /* DetailSearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailSearchView.swift; sourceTree = ""; }; - ABC8355E27B118370091DCDB /* DetailSearchStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailSearchStore.swift; sourceTree = ""; }; + ABC8355E27B118370091DCDB /* DetailSearchReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailSearchReducer.swift; sourceTree = ""; }; ABC8356027B357C50091DCDB /* GestureHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureHandler.swift; sourceTree = ""; }; ABC8356227B366760091DCDB /* PageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHandler.swift; sourceTree = ""; }; ABC8356427B36E550091DCDB /* AutoPlayHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoPlayHandler.swift; sourceTree = ""; }; @@ -700,7 +700,7 @@ path = Support; sourceTree = ""; }; - AB24C562276757B00085C33A /* Support */ = { + AB24C562276757B00085C33A /* Components */ = { isa = PBXGroup; children = ( ABF45ACB25F3313D00ECB568 /* LinkedText.swift */, @@ -708,7 +708,7 @@ AB0CFBD627C3B2D0004BD372 /* TagDetailView.swift */, ABF45AC825F3313D00ECB568 /* PostCommentView.swift */, ); - path = Support; + path = Components; sourceTree = ""; }; AB24C563276757C30085C33A /* Support */ = { @@ -1119,20 +1119,6 @@ path = Generated; sourceTree = ""; }; - ABBB263C2793C580007B6149 /* DataFlow */ = { - isa = PBXGroup; - children = ( - AB706FA2278BCF2F0025A48A /* DetailStore.swift */, - ABC8355E27B118370091DCDB /* DetailSearchStore.swift */, - ABBB266527977C2A007B6149 /* ArchivesStore.swift */, - ABBB26692797C61F007B6149 /* TorrentsStore.swift */, - ABBB263D2793C648007B6149 /* PreviewsStore.swift */, - ABBB263F279417EC007B6149 /* CommentsStore.swift */, - ABBB2678279D454C007B6149 /* GalleryInfosStore.swift */, - ); - path = DataFlow; - sourceTree = ""; - }; ABC3C74B2593696C00E0C11B = { isa = PBXGroup; children = ( @@ -1319,7 +1305,7 @@ isa = PBXGroup; children = ( ABBB2672279B9332007B6149 /* ReadingView.swift */, - ABBB2674279B933D007B6149 /* ReadingStore.swift */, + ABBB2674279B933D007B6149 /* ReadingReducer.swift */, AB24C561276757A30085C33A /* Support */, ); path = Reading; @@ -1328,15 +1314,15 @@ ABF45AD125F3313D00ECB568 /* Detail */ = { isa = PBXGroup; children = ( + EA2E2E852A20E40B0038A261 /* Torrents */, + EA2E2E842A20E1840038A261 /* Archives */, + EA2E2E862A20E52C0038A261 /* Previews */, + EA2E2E872A20E6C90038A261 /* Comments */, + EA2E2E882A20E9C50038A261 /* GalleryInfos */, + EA2E2E892A20EA460038A261 /* DetailSearch */, + AB24C562276757B00085C33A /* Components */, AB706FA0278BCEC60025A48A /* DetailView.swift */, - ABC8355C27B118330091DCDB /* DetailSearchView.swift */, - ABF45AD325F3313D00ECB568 /* ArchivesView.swift */, - ABF45AD425F3313D00ECB568 /* TorrentsView.swift */, - AB706FA4278C3DDE0025A48A /* PreviewsView.swift */, - ABF45AD525F3313D00ECB568 /* CommentsView.swift */, - ABBCCC8F26C95F6E007D8A36 /* GalleryInfosView.swift */, - ABBB263C2793C580007B6149 /* DataFlow */, - AB24C562276757B00085C33A /* Support */, + AB706FA2278BCF2F0025A48A /* DetailReducer.swift */, ); path = Detail; sourceTree = ""; @@ -1413,6 +1399,60 @@ path = Components; sourceTree = ""; }; + EA2E2E842A20E1840038A261 /* Archives */ = { + isa = PBXGroup; + children = ( + ABF45AD325F3313D00ECB568 /* ArchivesView.swift */, + ABBB266527977C2A007B6149 /* ArchivesReducer.swift */, + ); + path = Archives; + sourceTree = ""; + }; + EA2E2E852A20E40B0038A261 /* Torrents */ = { + isa = PBXGroup; + children = ( + ABF45AD425F3313D00ECB568 /* TorrentsView.swift */, + ABBB26692797C61F007B6149 /* TorrentsReducer.swift */, + ); + path = Torrents; + sourceTree = ""; + }; + EA2E2E862A20E52C0038A261 /* Previews */ = { + isa = PBXGroup; + children = ( + AB706FA4278C3DDE0025A48A /* PreviewsView.swift */, + ABBB263D2793C648007B6149 /* PreviewsReducer.swift */, + ); + path = Previews; + sourceTree = ""; + }; + EA2E2E872A20E6C90038A261 /* Comments */ = { + isa = PBXGroup; + children = ( + ABF45AD525F3313D00ECB568 /* CommentsView.swift */, + ABBB263F279417EC007B6149 /* CommentsReducer.swift */, + ); + path = Comments; + sourceTree = ""; + }; + EA2E2E882A20E9C50038A261 /* GalleryInfos */ = { + isa = PBXGroup; + children = ( + ABBCCC8F26C95F6E007D8A36 /* GalleryInfosView.swift */, + ABBB2678279D454C007B6149 /* GalleryInfosReducer.swift */, + ); + path = GalleryInfos; + sourceTree = ""; + }; + EA2E2E892A20EA460038A261 /* DetailSearch */ = { + isa = PBXGroup; + children = ( + ABC8355C27B118330091DCDB /* DetailSearchView.swift */, + ABC8355E27B118370091DCDB /* DetailSearchReducer.swift */, + ); + path = DetailSearch; + sourceTree = ""; + }; EAEC870B2A1F74D500E1A97A /* EhSetting */ = { isa = PBXGroup; children = ( @@ -1761,7 +1801,7 @@ AB8C821926BF801700E8C5E6 /* EhSetting.swift in Sources */, AB86AC1327856F2700E61E6A /* AppLockStore.swift in Sources */, AB58A5AC2776B2BC00C0D285 /* AppDelegateStore.swift in Sources */, - ABBB263E2793C648007B6149 /* PreviewsStore.swift in Sources */, + ABBB263E2793C648007B6149 /* PreviewsReducer.swift in Sources */, ABBC332A26BE7C940084A331 /* SettingTextField.swift in Sources */, AB358317269D826B009466A5 /* DFStreamHandler.swift in Sources */, AB7BF2CA27A969F4001865A3 /* GalleryState.swift in Sources */, @@ -1788,8 +1828,8 @@ ABF45AEA25F3313D00ECB568 /* Placeholder.swift in Sources */, ABD4032826B7967F00001B8C /* CategoryView.swift in Sources */, ABC681F326898D46007BBD69 /* Model.xcdatamodeld in Sources */, - ABBB266627977C2A007B6149 /* ArchivesStore.swift in Sources */, - ABBB2640279417EC007B6149 /* CommentsStore.swift in Sources */, + ABBB266627977C2A007B6149 /* ArchivesReducer.swift in Sources */, + ABBB2640279417EC007B6149 /* CommentsReducer.swift in Sources */, AB0929C027805A8200F107CA /* LoginReducer.swift in Sources */, ABBB2631278E6EF3007B6149 /* SearchView.swift in Sources */, AB706F92278A6E8C0025A48A /* WatchedStore.swift in Sources */, @@ -1797,9 +1837,9 @@ ABA9A6C228EC7BD000EE28DE /* Strings.swift in Sources */, AB58A5B22776B99000C0D285 /* AppStore.swift in Sources */, AB24C566276758E30085C33A /* GalleryCardCell.swift in Sources */, - ABBB2679279D454C007B6149 /* GalleryInfosStore.swift in Sources */, + ABBB2679279D454C007B6149 /* GalleryInfosReducer.swift in Sources */, AB7BF2B727A9652F001865A3 /* Greeting.swift in Sources */, - ABC8355F27B118370091DCDB /* DetailSearchStore.swift in Sources */, + ABC8355F27B118370091DCDB /* DetailSearchReducer.swift in Sources */, AB4FD2C1268AB83300A95968 /* GalleryDetailMO+CoreDataProperties.swift in Sources */, AB26F59427ACC6CD00AB3468 /* TagTranslator.swift in Sources */, AB0929CE2781AADA00F107CA /* DatabaseClient.swift in Sources */, @@ -1824,7 +1864,7 @@ AB7BF2D627AA3F4C001865A3 /* FileUtil.swift in Sources */, AB0ABCB726C541A400AD970F /* WaveForm.swift in Sources */, AB0929D62782A65F00F107CA /* GeneralSettingReducer.swift in Sources */, - AB706FA3278BCF2F0025A48A /* DetailStore.swift in Sources */, + AB706FA3278BCF2F0025A48A /* DetailReducer.swift in Sources */, ABBCCC9026C95F6E007D8A36 /* GalleryInfosView.swift in Sources */, AB7BF2A927A63C89001865A3 /* Language.swift in Sources */, AB86AC0A2782FAFA00E61E6A /* AppearanceSettingReducer.swift in Sources */, @@ -1833,7 +1873,7 @@ AB0CFBD727C3B2D0004BD372 /* TagDetailView.swift in Sources */, AB38A0CB25CA993D00764D64 /* ColorCodable.swift in Sources */, ABC732C127B8962000D47DA9 /* LiveTextHandler.swift in Sources */, - ABBB2675279B933D007B6149 /* ReadingStore.swift in Sources */, + ABBB2675279B933D007B6149 /* ReadingReducer.swift in Sources */, ABF45AF425F3313D00ECB568 /* WebView.swift in Sources */, AB7B29F626AC741600EE1F14 /* GenericList.swift in Sources */, AB0CFBC927C07F95004BD372 /* TagSuggestionView.swift in Sources */, @@ -1878,7 +1918,7 @@ ABF45AE525F3313D00ECB568 /* PostCommentView.swift in Sources */, AB358313269D7E89009466A5 /* DFRequest.swift in Sources */, AB706F90278A5F680025A48A /* AppDelegateClient.swift in Sources */, - ABBB266A2797C61F007B6149 /* TorrentsStore.swift in Sources */, + ABBB266A2797C61F007B6149 /* TorrentsReducer.swift in Sources */, ABF75F3F25A19CD200544D29 /* User.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift index 732d915a..6e2361dc 100644 --- a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift +++ b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift @@ -76,3 +76,21 @@ extension ReducerProtocol { } } } + +// MARK: Recurse +struct RecurseReducer: ReducerProtocol +where State == Base.State, Action == Base.Action { + let base: (Reduce) -> Base + + public init(@ReducerBuilder base: @escaping (Reduce) -> Base) { + self.base = base + } + + public var body: some ReducerProtocol { + var `self`: Reduce! + self = Reduce { state, action in + base(self).reduce(into: &state, action: action) + } + return self + } +} diff --git a/EhPanda/DataFlow/AppRouteStore.swift b/EhPanda/DataFlow/AppRouteStore.swift index d5e4f986..d8c4111e 100644 --- a/EhPanda/DataFlow/AppRouteStore.swift +++ b/EhPanda/DataFlow/AppRouteStore.swift @@ -24,7 +24,7 @@ struct AppRouteState: Equatable { @BindingState var route: Route? var hudConfig: TTProgressHUDConfig = .loading - @Heap var detailState: DetailState! + @Heap var detailState: DetailReducer.State! } enum AppRouteAction: BindableAction { @@ -43,7 +43,7 @@ enum AppRouteAction: BindableAction { case fetchGalleryDone(URL, Result) case fetchGreetingDone(Result) - case detail(DetailAction) + case detail(DetailReducer.Action) } struct AppRouteEnvironment { @@ -181,23 +181,24 @@ let appRouteReducer = Reducer) + case setNavigation(Route?) + + case syncGalleryFunds(String, String) + + case teardown + case fetchArchive(String, URL, URL) + case fetchArchiveDone(String, URL, Result<(GalleryArchive, String?, String?), AppError>) + case fetchArchiveFunds(String, URL) + case fetchArchiveFundsDone(Result<(String, String), AppError>) + case fetchDownloadResponse(URL) + case fetchDownloadResponseDone(Result) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + @Dependency(\.cookieClient) private var cookieClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return .none + + case .syncGalleryFunds(let galleryPoints, let credits): + return databaseClient + .updateGalleryFunds(galleryPoints: galleryPoints, credits: credits).fireAndForget() + + case .teardown: + return .cancel(id: CancelID()) + + case .fetchArchive(let gid, let galleryURL, let archiveURL): + guard state.loadingState != .loading else { return .none } + state.loadingState = .loading + return GalleryArchiveRequest(archiveURL: archiveURL) + .effect.map({ Action.fetchArchiveDone(gid, galleryURL, $0) }) + .cancellable(id: CancelID()) + + case .fetchArchiveDone(let gid, let galleryURL, let result): + state.loadingState = .idle + switch result { + case .success(let (archive, galleryPoints, credits)): + guard !archive.hathArchives.isEmpty else { + state.loadingState = .failed(.notFound) + return .none + } + state.hathArchives = archive.hathArchives + if let galleryPoints = galleryPoints, let credits = credits { + return .init(value: .syncGalleryFunds(galleryPoints, credits)) + } else if cookieClient.isSameAccount { + return .init(value: .fetchArchiveFunds(gid, galleryURL)) + } else { + return .none + } + case .failure(let error): + state.loadingState = .failed(error) + } + return .none + + case .fetchArchiveFunds(let gid, let galleryURL): + guard let galleryURL = galleryURL.replaceHost(to: Defaults.URL.ehentai.host) else { return .none } + return GalleryArchiveFundsRequest(gid: gid, galleryURL: galleryURL) + .effect.map(Action.fetchArchiveFundsDone).cancellable(id: CancelID()) + + case .fetchArchiveFundsDone(let result): + if case .success(let (galleryPoints, credits)) = result { + return .init(value: .syncGalleryFunds(galleryPoints, credits)) + } + return .none + + case .fetchDownloadResponse(let archiveURL): + guard let selectedArchive = state.selectedArchive, + state.route != .communicatingHUD + else { return .none } + state.route = .communicatingHUD + return SendDownloadCommandRequest( + archiveURL: archiveURL, resolution: selectedArchive.resolution.parameter + ) + .effect.map(Action.fetchDownloadResponseDone).cancellable(id: CancelID()) + + case .fetchDownloadResponseDone(let result): + state.route = .messageHUD + let isSuccess: Bool + switch result { + case .success(let response): + switch response { + case L10n.Constant.Website.Response.hathClientNotFound: + state.messageHUDConfig = .error(caption: L10n.Localizable.Website.Response.hathClientNotFound) + isSuccess = false + case L10n.Constant.Website.Response.hathClientNotOnline: + state.messageHUDConfig = .error(caption: L10n.Localizable.Website.Response.hathClientNotOnline) + isSuccess = false + case L10n.Constant.Website.Response.invalidResolution: + state.messageHUDConfig = .error(caption: L10n.Localizable.Website.Response.invalidResolution) + isSuccess = false + default: + state.messageHUDConfig = .success(caption: response) + isSuccess = true + } + case .failure: + state.messageHUDConfig = .error + isSuccess = false + } + return hapticsClient.generateNotificationFeedback(isSuccess ? .success : .error).fireAndForget() + } + } + + BindingReducer() + } +} diff --git a/EhPanda/View/Detail/ArchivesView.swift b/EhPanda/View/Detail/Archives/ArchivesView.swift similarity index 93% rename from EhPanda/View/Detail/ArchivesView.swift rename to EhPanda/View/Detail/Archives/ArchivesView.swift index deb8ef03..9ae731f3 100644 --- a/EhPanda/View/Detail/ArchivesView.swift +++ b/EhPanda/View/Detail/Archives/ArchivesView.swift @@ -9,15 +9,15 @@ import SwiftUI import ComposableArchitecture struct ArchivesView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let gid: String private let user: User private let galleryURL: URL private let archiveURL: URL init( - store: Store, + store: StoreOf, gid: String, user: User, galleryURL: URL, archiveURL: URL ) { self.store = store @@ -56,12 +56,12 @@ struct ArchivesView: View { .progressHUD( config: viewStore.communicatingHUDConfig, unwrapping: viewStore.binding(\.$route), - case: /ArchivesState.Route.communicatingHUD + case: /ArchivesReducer.Route.communicatingHUD ) .progressHUD( config: viewStore.messageHUDConfig, unwrapping: viewStore.binding(\.$route), - case: /ArchivesState.Route.messageHUD + case: /ArchivesReducer.Route.messageHUD ) .animation(.default, value: viewStore.hathArchives) .animation(.default, value: user.galleryPoints) @@ -223,12 +223,7 @@ struct ArchivesView_Previews: PreviewProvider { ArchivesView( store: .init( initialState: .init(), - reducer: archivesReducer, - environment: ArchivesEnvironment( - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live - ) + reducer: ArchivesReducer() ), gid: .init(), user: .init(), diff --git a/EhPanda/View/Detail/Comments/CommentsReducer.swift b/EhPanda/View/Detail/Comments/CommentsReducer.swift new file mode 100644 index 00000000..2f94d736 --- /dev/null +++ b/EhPanda/View/Detail/Comments/CommentsReducer.swift @@ -0,0 +1,226 @@ +// +// CommentsReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/16. +// + +import Foundation +import TTProgressHUD +import ComposableArchitecture + +struct CommentsReducer: ReducerProtocol { + enum Route: Equatable { + case hud + case detail(String) + case postComment(String) + } + + struct CancelID: Hashable { + let id = String(describing: CommentsReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var commentContent = "" + @BindingState var postCommentFocused = false + + var hudConfig: TTProgressHUDConfig = .loading + var scrollCommentID: String? + var scrollRowOpacity: Double = 1 + + @Heap var detailState: DetailReducer.State! + + init() { + _detailState = .init(.init()) + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + case clearScrollCommentID + + case setHUDConfig(TTProgressHUDConfig) + case setPostCommentFocused(Bool) + case setScrollRowOpacity(Double) + case setCommentContent(String) + case performScrollOpacityEffect + case handleCommentLink(URL) + case handleGalleryLink(URL) + case onPostCommentAppear + case onAppear + + case updateReadingProgress(String, Int) + + case teardown + case postComment(URL, String? = nil) + case voteComment(String, String, String, String, Int) + case performCommentActionDone(Result) + case fetchGallery(URL, Bool) + case fetchGalleryDone(URL, Result) + + case detail(DetailReducer.Action) + } + + @Dependency(\.uiApplicationClient) private var uiApplicationClient + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + @Dependency(\.cookieClient) private var cookieClient + @Dependency(\.urlClient) private var urlClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .clearSubStates: + state.detailState = .init() + state.commentContent = .init() + state.postCommentFocused = false + return .init(value: .detail(.teardown)) + + case .clearScrollCommentID: + state.scrollCommentID = nil + return .none + + case .setHUDConfig(let config): + state.hudConfig = config + return .none + + case .setPostCommentFocused(let isFocused): + state.postCommentFocused = isFocused + return .none + + case .setScrollRowOpacity(let opacity): + state.scrollRowOpacity = opacity + return .none + + case .setCommentContent(let content): + state.commentContent = content + return .none + + case .performScrollOpacityEffect: + return .merge( + .init(value: .setScrollRowOpacity(0.25)) + .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect(), + .init(value: .setScrollRowOpacity(1)) + .delay(for: .milliseconds(1250), scheduler: DispatchQueue.main).eraseToEffect(), + .init(value: .clearScrollCommentID) + .delay(for: .milliseconds(2000), scheduler: DispatchQueue.main).eraseToEffect() + ) + + case .handleCommentLink(let url): + guard urlClient.checkIfHandleable(url) else { + return uiApplicationClient.openURL(url).fireAndForget() + } + let (isGalleryImageURL, _, _) = urlClient.analyzeURL(url) + let gid = urlClient.parseGalleryID(url) + guard databaseClient.fetchGallery(gid: gid) == nil else { + return .init(value: .handleGalleryLink(url)) + } + return .init(value: .fetchGallery(url, isGalleryImageURL)) + + case .handleGalleryLink(let url): + let (_, pageIndex, commentID) = urlClient.analyzeURL(url) + let gid = urlClient.parseGalleryID(url) + var effects = [EffectTask]() + if let pageIndex = pageIndex { + effects.append(.init(value: .updateReadingProgress(gid, pageIndex))) + effects.append( + .init(value: .detail(.setNavigation(.reading))) + .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect() + ) + } else if let commentID = commentID { + state.detailState.commentsState?.scrollCommentID = commentID + effects.append( + .init(value: .detail(.setNavigation(.comments(url)))) + .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect() + ) + } + effects.append(.init(value: .setNavigation(.detail(gid)))) + return .merge(effects) + + case .onPostCommentAppear: + return .init(value: .setPostCommentFocused(true)) + .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect() + + case .onAppear: + if state.detailState == nil { + state.detailState = .init() + } + return state.scrollCommentID != nil ? .init(value: .performScrollOpacityEffect) : .none + + case .updateReadingProgress(let gid, let progress): + guard !gid.isEmpty else { return .none } + return databaseClient + .updateReadingProgress(gid: gid, progress: progress).fireAndForget() + + case .teardown: + return .cancel(id: CancelID()) + + case .postComment(let galleryURL, let commentID): + guard !state.commentContent.isEmpty else { return .none } + if let commentID = commentID { + return EditGalleryCommentRequest( + commentID: commentID, content: state.commentContent, galleryURL: galleryURL + ) + .effect.map(Action.performCommentActionDone).cancellable(id: CancelID()) + } else { + return CommentGalleryRequest(content: state.commentContent, galleryURL: galleryURL) + .effect.map(Action.performCommentActionDone).cancellable(id: CancelID()) + } + + case .voteComment(let gid, let token, let apiKey, let commentID, let vote): + guard let gid = Int(gid), let commentID = Int(commentID), + let apiuid = Int(cookieClient.apiuid) + else { return .none } + return VoteGalleryCommentRequest( + apiuid: apiuid, apikey: apiKey, gid: gid, token: token, + commentID: commentID, commentVote: vote + ) + .effect.map(Action.performCommentActionDone).cancellable(id: CancelID()) + + case .performCommentActionDone: + return .none + + case .fetchGallery(let url, let isGalleryImageURL): + state.route = .hud + return GalleryReverseRequest(url: url, isGalleryImageURL: isGalleryImageURL) + .effect.map({ Action.fetchGalleryDone(url, $0) }).cancellable(id: CancelID()) + + case .fetchGalleryDone(let url, let result): + state.route = nil + switch result { + case .success(let gallery): + return .merge( + databaseClient.cacheGalleries([gallery]).fireAndForget(), + .init(value: .handleGalleryLink(url)) + ) + case .failure: + return .init(value: .setHUDConfig(.error)) + .delay(for: .milliseconds(500), scheduler: DispatchQueue.main).eraseToEffect() + } + + case .detail: + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.postComment, + hapticsClient: hapticsClient + ) + + BindingReducer() + } +} diff --git a/EhPanda/View/Detail/CommentsView.swift b/EhPanda/View/Detail/Comments/CommentsView.swift similarity index 91% rename from EhPanda/View/Detail/CommentsView.swift rename to EhPanda/View/Detail/Comments/CommentsView.swift index 2c86baa6..27534aab 100644 --- a/EhPanda/View/Detail/CommentsView.swift +++ b/EhPanda/View/Detail/Comments/CommentsView.swift @@ -10,8 +10,8 @@ import Kingfisher import ComposableArchitecture struct CommentsView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let gid: String private let token: String private let apiKey: String @@ -23,7 +23,7 @@ struct CommentsView: View { private let tagTranslator: TagTranslator init( - store: Store, + store: StoreOf, gid: String, token: String, apiKey: String, galleryURL: URL, comments: [GalleryComment], user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator @@ -92,7 +92,7 @@ struct CommentsView: View { } } } - .sheet(unwrapping: viewStore.binding(\.$route), case: /CommentsState.Route.postComment) { route in + .sheet(unwrapping: viewStore.binding(\.$route), case: /CommentsReducer.Route.postComment) { route in let hasCommentID = !route.wrappedValue.isEmpty PostCommentView( title: hasCommentID @@ -117,7 +117,7 @@ struct CommentsView: View { .progressHUD( config: viewStore.hudConfig, unwrapping: viewStore.binding(\.$route), - case: /CommentsState.Route.hud + case: /CommentsReducer.Route.hud ) .animation(.default, value: viewStore.scrollRowOpacity) .onAppear { @@ -143,9 +143,9 @@ struct CommentsView: View { // MARK: NavigationLinks private extension CommentsView { @ViewBuilder var navigationLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /CommentsState.Route.detail) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /CommentsReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: CommentsAction.detail), + store: store.scope(state: \.detailState, action: CommentsReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -274,19 +274,7 @@ struct CommentsView_Previews: PreviewProvider { CommentsView( store: .init( initialState: .init(), - reducer: commentsReducer, - environment: CommentsEnvironment( - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - uiApplicationClient: .live - ) + reducer: CommentsReducer() ), gid: .init(), token: .init(), diff --git a/EhPanda/View/Detail/Support/LinkedText.swift b/EhPanda/View/Detail/Components/LinkedText.swift similarity index 100% rename from EhPanda/View/Detail/Support/LinkedText.swift rename to EhPanda/View/Detail/Components/LinkedText.swift diff --git a/EhPanda/View/Detail/Support/PostCommentView.swift b/EhPanda/View/Detail/Components/PostCommentView.swift similarity index 100% rename from EhPanda/View/Detail/Support/PostCommentView.swift rename to EhPanda/View/Detail/Components/PostCommentView.swift diff --git a/EhPanda/View/Detail/Support/RatingView.swift b/EhPanda/View/Detail/Components/RatingView.swift similarity index 100% rename from EhPanda/View/Detail/Support/RatingView.swift rename to EhPanda/View/Detail/Components/RatingView.swift diff --git a/EhPanda/View/Detail/Support/TagDetailView.swift b/EhPanda/View/Detail/Components/TagDetailView.swift similarity index 100% rename from EhPanda/View/Detail/Support/TagDetailView.swift rename to EhPanda/View/Detail/Components/TagDetailView.swift diff --git a/EhPanda/View/Detail/DataFlow/ArchivesStore.swift b/EhPanda/View/Detail/DataFlow/ArchivesStore.swift deleted file mode 100644 index fedf0a3b..00000000 --- a/EhPanda/View/Detail/DataFlow/ArchivesStore.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// ArchivesStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/19. -// - -import Foundation -import TTProgressHUD -import ComposableArchitecture - -struct ArchivesState: Equatable { - enum Route { - case messageHUD - case communicatingHUD - } - struct CancelID: Hashable { - let id = String(describing: ArchivesState.self) - } - - @BindingState var route: Route? - @BindingState var selectedArchive: GalleryArchive.HathArchive? - - var loadingState: LoadingState = .idle - var hathArchives = [GalleryArchive.HathArchive]() - - var messageHUDConfig = TTProgressHUDConfig() - var communicatingHUDConfig: TTProgressHUDConfig = .communicating -} - -enum ArchivesAction: BindableAction { - case binding(BindingAction) - case setNavigation(ArchivesState.Route?) - - case syncGalleryFunds(String, String) - - case teardown - case fetchArchive(String, URL, URL) - case fetchArchiveDone(String, URL, Result<(GalleryArchive, String?, String?), AppError>) - case fetchArchiveFunds(String, URL) - case fetchArchiveFundsDone(Result<(String, String), AppError>) - case fetchDownloadResponse(URL) - case fetchDownloadResponseDone(Result) -} - -struct ArchivesEnvironment { - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient -} - -let archivesReducer = Reducer { state, action, environment in - switch action { - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return .none - - case .syncGalleryFunds(let galleryPoints, let credits): - return environment.databaseClient - .updateGalleryFunds(galleryPoints: galleryPoints, credits: credits).fireAndForget() - - case .teardown: - return .cancel(id: ArchivesState.CancelID()) - - case .fetchArchive(let gid, let galleryURL, let archiveURL): - guard state.loadingState != .loading else { return .none } - state.loadingState = .loading - return GalleryArchiveRequest(archiveURL: archiveURL) - .effect.map({ ArchivesAction.fetchArchiveDone(gid, galleryURL, $0) }) - .cancellable(id: ArchivesState.CancelID()) - - case .fetchArchiveDone(let gid, let galleryURL, let result): - state.loadingState = .idle - switch result { - case .success(let (archive, galleryPoints, credits)): - guard !archive.hathArchives.isEmpty else { - state.loadingState = .failed(.notFound) - return .none - } - state.hathArchives = archive.hathArchives - if let galleryPoints = galleryPoints, let credits = credits { - return .init(value: .syncGalleryFunds(galleryPoints, credits)) - } else if environment.cookieClient.isSameAccount { - return .init(value: .fetchArchiveFunds(gid, galleryURL)) - } else { - return .none - } - case .failure(let error): - state.loadingState = .failed(error) - } - return .none - - case .fetchArchiveFunds(let gid, let galleryURL): - guard let galleryURL = galleryURL.replaceHost(to: Defaults.URL.ehentai.host) else { return .none } - return GalleryArchiveFundsRequest(gid: gid, galleryURL: galleryURL) - .effect.map(ArchivesAction.fetchArchiveFundsDone).cancellable(id: ArchivesState.CancelID()) - - case .fetchArchiveFundsDone(let result): - if case .success(let (galleryPoints, credits)) = result { - return .init(value: .syncGalleryFunds(galleryPoints, credits)) - } - return .none - - case .fetchDownloadResponse(let archiveURL): - guard let selectedArchive = state.selectedArchive, state.route != .communicatingHUD else { return .none } - state.route = .communicatingHUD - return SendDownloadCommandRequest(archiveURL: archiveURL, resolution: selectedArchive.resolution.parameter) - .effect.map(ArchivesAction.fetchDownloadResponseDone).cancellable(id: ArchivesState.CancelID()) - - case .fetchDownloadResponseDone(let result): - state.route = .messageHUD - let isSuccess: Bool - switch result { - case .success(let response): - switch response { - case L10n.Constant.Website.Response.hathClientNotFound: - state.messageHUDConfig = .error(caption: L10n.Localizable.Website.Response.hathClientNotFound) - isSuccess = false - case L10n.Constant.Website.Response.hathClientNotOnline: - state.messageHUDConfig = .error(caption: L10n.Localizable.Website.Response.hathClientNotOnline) - isSuccess = false - case L10n.Constant.Website.Response.invalidResolution: - state.messageHUDConfig = .error(caption: L10n.Localizable.Website.Response.invalidResolution) - isSuccess = false - default: - state.messageHUDConfig = .success(caption: response) - isSuccess = true - } - case .failure: - state.messageHUDConfig = .error - isSuccess = false - } - return environment.hapticsClient.generateNotificationFeedback(isSuccess ? .success : .error).fireAndForget() - } -} -.binding() diff --git a/EhPanda/View/Detail/DataFlow/CommentsStore.swift b/EhPanda/View/Detail/DataFlow/CommentsStore.swift deleted file mode 100644 index 5db05dec..00000000 --- a/EhPanda/View/Detail/DataFlow/CommentsStore.swift +++ /dev/null @@ -1,227 +0,0 @@ -// -// CommentsStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/16. -// - -import Foundation -import TTProgressHUD -import ComposableArchitecture - -struct CommentsState: Equatable { - enum Route: Equatable { - case hud - case detail(String) - case postComment(String) - } - struct CancelID: Hashable { - let id = String(describing: CommentsState.self) - } - - init() { - _detailState = .init(.init()) - } - - @BindingState var route: Route? - @BindingState var commentContent = "" - @BindingState var postCommentFocused = false - - var hudConfig: TTProgressHUDConfig = .loading - var scrollCommentID: String? - var scrollRowOpacity: Double = 1 - - @Heap var detailState: DetailState! -} - -enum CommentsAction: BindableAction { - case binding(BindingAction) - case setNavigation(CommentsState.Route?) - case clearSubStates - case clearScrollCommentID - - case setHUDConfig(TTProgressHUDConfig) - case setPostCommentFocused(Bool) - case setScrollRowOpacity(Double) - case setCommentContent(String) - case performScrollOpacityEffect - case handleCommentLink(URL) - case handleGalleryLink(URL) - case onPostCommentAppear - case onAppear - - case updateReadingProgress(String, Int) - - case teardown - case postComment(URL, String? = nil) - case voteComment(String, String, String, String, Int) - case performCommentActionDone(Result) - case fetchGallery(URL, Bool) - case fetchGalleryDone(URL, Result) - - case detail(DetailAction) -} - -struct CommentsEnvironment { - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let uiApplicationClient: UIApplicationClient -} - -let commentsReducer = Reducer { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .clearSubStates: - state.detailState = .init() - state.commentContent = .init() - state.postCommentFocused = false - return .init(value: .detail(.teardown)) - - case .clearScrollCommentID: - state.scrollCommentID = nil - return .none - - case .setHUDConfig(let config): - state.hudConfig = config - return .none - - case .setPostCommentFocused(let isFocused): - state.postCommentFocused = isFocused - return .none - - case .setScrollRowOpacity(let opacity): - state.scrollRowOpacity = opacity - return .none - - case .setCommentContent(let content): - state.commentContent = content - return .none - - case .performScrollOpacityEffect: - return .merge( - .init(value: .setScrollRowOpacity(0.25)) - .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect(), - .init(value: .setScrollRowOpacity(1)) - .delay(for: .milliseconds(1250), scheduler: DispatchQueue.main).eraseToEffect(), - .init(value: .clearScrollCommentID) - .delay(for: .milliseconds(2000), scheduler: DispatchQueue.main).eraseToEffect() - ) - - case .handleCommentLink(let url): - guard environment.urlClient.checkIfHandleable(url) else { - return environment.uiApplicationClient.openURL(url).fireAndForget() - } - let (isGalleryImageURL, _, _) = environment.urlClient.analyzeURL(url) - let gid = environment.urlClient.parseGalleryID(url) - guard environment.databaseClient.fetchGallery(gid: gid) == nil else { - return .init(value: .handleGalleryLink(url)) - } - return .init(value: .fetchGallery(url, isGalleryImageURL)) - - case .handleGalleryLink(let url): - let (_, pageIndex, commentID) = environment.urlClient.analyzeURL(url) - let gid = environment.urlClient.parseGalleryID(url) - var effects = [EffectTask]() - if let pageIndex = pageIndex { - effects.append(.init(value: .updateReadingProgress(gid, pageIndex))) - effects.append( - .init(value: .detail(.setNavigation(.reading))) - .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect() - ) - } else if let commentID = commentID { - state.detailState.commentsState?.scrollCommentID = commentID - effects.append( - .init(value: .detail(.setNavigation(.comments(url)))) - .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect() - ) - } - effects.append(.init(value: .setNavigation(.detail(gid)))) - return .merge(effects) - - case .onPostCommentAppear: - return .init(value: .setPostCommentFocused(true)) - .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect() - - case .onAppear: - if state.detailState == nil { - state.detailState = .init() - } - return state.scrollCommentID != nil ? .init(value: .performScrollOpacityEffect) : .none - - case .updateReadingProgress(let gid, let progress): - guard !gid.isEmpty else { return .none } - return environment.databaseClient - .updateReadingProgress(gid: gid, progress: progress).fireAndForget() - - case .teardown: - return .cancel(id: CommentsState.CancelID()) - - case .postComment(let galleryURL, let commentID): - guard !state.commentContent.isEmpty else { return .none } - if let commentID = commentID { - return EditGalleryCommentRequest( - commentID: commentID, content: state.commentContent, galleryURL: galleryURL - ) - .effect.map(CommentsAction.performCommentActionDone).cancellable(id: CommentsState.CancelID()) - } else { - return CommentGalleryRequest(content: state.commentContent, galleryURL: galleryURL) - .effect.map(CommentsAction.performCommentActionDone).cancellable(id: CommentsState.CancelID()) - } - - case .voteComment(let gid, let token, let apiKey, let commentID, let vote): - guard let gid = Int(gid), let commentID = Int(commentID), - let apiuid = Int(environment.cookieClient.apiuid) - else { return .none } - return VoteGalleryCommentRequest( - apiuid: apiuid, apikey: apiKey, gid: gid, token: token, - commentID: commentID, commentVote: vote - ) - .effect.map(CommentsAction.performCommentActionDone).cancellable(id: CommentsState.CancelID()) - - case .performCommentActionDone: - return .none - - case .fetchGallery(let url, let isGalleryImageURL): - state.route = .hud - return GalleryReverseRequest(url: url, isGalleryImageURL: isGalleryImageURL) - .effect.map({ CommentsAction.fetchGalleryDone(url, $0) }).cancellable(id: CommentsState.CancelID()) - - case .fetchGalleryDone(let url, let result): - state.route = nil - switch result { - case .success(let gallery): - return .merge( - environment.databaseClient.cacheGalleries([gallery]).fireAndForget(), - .init(value: .handleGalleryLink(url)) - ) - case .failure: - return .init(value: .setHUDConfig(.error)) - .delay(for: .milliseconds(500), scheduler: DispatchQueue.main).eraseToEffect() - } - - case .detail: - return .none - } -} -.haptics( - unwrapping: \.route, - case: /CommentsState.Route.postComment, - hapticsClient: \.hapticsClient -) -.binding() diff --git a/EhPanda/View/Detail/DataFlow/DetailSearchStore.swift b/EhPanda/View/Detail/DataFlow/DetailSearchStore.swift deleted file mode 100644 index 9e035142..00000000 --- a/EhPanda/View/Detail/DataFlow/DetailSearchStore.swift +++ /dev/null @@ -1,209 +0,0 @@ -// -// SearchStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/12. -// - -import ComposableArchitecture - -struct DetailSearchState: Equatable { - enum Route: Equatable { - case filters - case quickSearch - case detail(String) - } - struct CancelID: Hashable { - let id = String(describing: DetailSearchState.self) - } - - init() { - _detailState = .init(.init()) - } - - @BindingState var route: Route? - @BindingState var keyword = "" - var lastKeyword = "" - - var galleries = [Gallery]() - var pageNumber = PageNumber() - var loadingState: LoadingState = .idle - var footerLoadingState: LoadingState = .idle - - @Heap var detailState: DetailState! - var filtersState = FiltersReducer.State() - var quickDetailSearchState = QuickSearchReducer.State() - - mutating func insertGalleries(_ galleries: [Gallery]) { - galleries.forEach { gallery in - if !self.galleries.contains(gallery) { - self.galleries.append(gallery) - } - } - } -} - -enum DetailSearchAction: BindableAction { - case binding(BindingAction) - case setNavigation(DetailSearchState.Route?) - case clearSubStates - - case teardown - case fetchGalleries(String? = nil) - case fetchGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) - case fetchMoreGalleries - case fetchMoreGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) - - case detail(DetailAction) - case filters(FiltersReducer.Action) - case quickSearch(QuickSearchReducer.Action) -} - -struct DetailSearchEnvironment { - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let uiApplicationClient: UIApplicationClient -} - -let detailSearchReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding(\.$keyword): - if !state.keyword.isEmpty { - state.lastKeyword = state.keyword - } - return .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .clearSubStates: - state.detailState = .init() - state.filtersState = .init() - state.quickDetailSearchState = .init() - return .merge( - .init(value: .detail(.teardown)), - .init(value: .quickSearch(.teardown)) - ) - - case .teardown: - return .cancel(id: DetailSearchState.CancelID()) - - case .fetchGalleries(let keyword): - guard state.loadingState != .loading else { return .none } - if let keyword = keyword { - state.keyword = keyword - state.lastKeyword = keyword - } - state.loadingState = .loading - state.pageNumber.resetPages() - let filter = environment.databaseClient.fetchFilterSynchronously(range: .search) - return SearchGalleriesRequest(keyword: state.lastKeyword, filter: filter).effect - .map(DetailSearchAction.fetchGalleriesDone).cancellable(id: DetailSearchState.CancelID()) - - case .fetchGalleriesDone(let result): - state.loadingState = .idle - switch result { - case .success(let (pageNumber, galleries)): - guard !galleries.isEmpty else { - state.loadingState = .failed(.notFound) - guard pageNumber.hasNextPage() else { return .none } - return .init(value: .fetchMoreGalleries) - } - state.pageNumber = pageNumber - state.galleries = galleries - return environment.databaseClient.cacheGalleries(galleries).fireAndForget() - case .failure(let error): - state.loadingState = .failed(error) - } - return .none - - case .fetchMoreGalleries: - let pageNumber = state.pageNumber - guard pageNumber.hasNextPage(), - state.footerLoadingState != .loading, - let lastID = state.galleries.last?.id - else { return .none } - state.footerLoadingState = .loading - let filter = environment.databaseClient.fetchFilterSynchronously(range: .search) - return MoreSearchGalleriesRequest(keyword: state.lastKeyword, filter: filter, lastID: lastID).effect - .map(DetailSearchAction.fetchMoreGalleriesDone) - .cancellable(id: DetailSearchState.CancelID()) - - case .fetchMoreGalleriesDone(let result): - state.footerLoadingState = .idle - switch result { - case .success(let (pageNumber, galleries)): - state.pageNumber = pageNumber - state.insertGalleries(galleries) - - var effects: [EffectTask] = [ - environment.databaseClient.cacheGalleries(galleries).fireAndForget() - ] - if galleries.isEmpty, pageNumber.hasNextPage() { - effects.append(.init(value: .fetchMoreGalleries)) - } else if !galleries.isEmpty { - state.loadingState = .idle - } - return .merge(effects) - - case .failure(let error): - state.footerLoadingState = .failed(error) - } - return .none - - case .detail: - return .none - - case .filters: - return .none - - case .quickSearch: - return .none - } - } - .haptics( - unwrapping: \.route, - case: /DetailSearchState.Route.quickSearch, - hapticsClient: \.hapticsClient - ) - .haptics( - unwrapping: \.route, - case: /DetailSearchState.Route.filters, - hapticsClient: \.hapticsClient - ) - .binding() -// , -// filtersReducer.pullback( -// state: \.filtersState, -// action: /DetailSearchAction.filters, -// environment: { -// .init( -// databaseClient: $0.databaseClient -// ) -// } -// ), -// quickSearchReducer.pullback( -// state: \.quickDetailSearchState, -// action: /DetailSearchAction.quickSearch, -// environment: { -// .init( -// databaseClient: $0.databaseClient -// ) -// } -// ) -) diff --git a/EhPanda/View/Detail/DataFlow/DetailStore.swift b/EhPanda/View/Detail/DataFlow/DetailStore.swift deleted file mode 100644 index 1593191f..00000000 --- a/EhPanda/View/Detail/DataFlow/DetailStore.swift +++ /dev/null @@ -1,524 +0,0 @@ -// -// DetailStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/10. -// - -import SwiftUI -import Foundation -import ComposableArchitecture - -struct DetailState: Equatable { - enum Route: Equatable { - case reading - case archives(URL, URL) - case torrents - case previews - case comments(URL) - case share(URL) - case postComment - case newDawn(Greeting) - case detailSearch(String) - case tagDetail(TagDetail) - case galleryInfos(Gallery, GalleryDetail) - } - struct CancelID: Hashable { - let id = String(describing: DetailState.self) - } - - init() { - _commentsState = .init(nil) - _detailSearchState = .init(nil) - } - - @BindingState var route: Route? - @BindingState var commentContent = "" - @BindingState var postCommentFocused = false - - var showsNewDawnGreeting = false - var showsUserRating = false - var showsFullTitle = false - var userRating = 0 - - var apiKey = "" - var loadingState: LoadingState = .idle - var gallery: Gallery = .empty - var galleryDetail: GalleryDetail? - var galleryTags = [GalleryTag]() - var galleryPreviewURLs = [Int: URL]() - var galleryComments = [GalleryComment]() - - var readingState = ReadingState() - var archivesState = ArchivesState() - var torrentsState = TorrentsState() - var previewsState = PreviewsState() - @Heap var commentsState: CommentsState? - var galleryInfosState = GalleryInfosState() - @Heap var detailSearchState: DetailSearchState? - - mutating func updateRating(value: DragGesture.Value) { - let rating = Int(value.location.x / 31 * 2) + 1 - userRating = min(max(rating, 1), 10) - } -} - -indirect enum DetailAction: BindableAction { - case binding(BindingAction) - case setNavigation(DetailState.Route?) - case clearSubStates - case onPostCommentAppear - case onAppear(String, Bool) - - case toggleShowFullTitle - case toggleShowUserRating - case setCommentContent(String) - case setPostCommentFocused(Bool) - case updateRating(DragGesture.Value) - case confirmRating(DragGesture.Value) - case confirmRatingDone - - case syncGalleryTags - case syncGalleryDetail - case syncGalleryPreviewURLs - case syncGalleryComments - case syncGreeting(Greeting) - case syncPreviewConfig(PreviewConfig) - case saveGalleryHistory - case updateReadingProgress(Int) - - case teardown - case fetchDatabaseInfos(String) - case fetchDatabaseInfosDone(GalleryState) - case fetchGalleryDetail - case fetchGalleryDetailDone(Result<(GalleryDetail, GalleryState, String, Greeting?), AppError>) - - case rateGallery - case favorGallery(Int) - case unfavorGallery - case postComment(URL) - case voteTag(String, Int) - case anyGalleryOpsDone(Result) - - case reading(ReadingAction) - case archives(ArchivesAction) - case torrents(TorrentsAction) - case previews(PreviewsAction) - case comments(CommentsAction) - case galleryInfos(GalleryInfosAction) - case detailSearch(DetailSearchAction) -} - -struct DetailEnvironment { - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let uiApplicationClient: UIApplicationClient -} - -extension Reducer { - static func recurse( - _ reducer: @escaping - (Reducer) - -> Reducer - ) - -> Reducer { - var `self`: Reducer! - self = Reducer { state, action, environment in - reducer(self).run(&state, action, environment) - } - return self - } -} - -let detailReducer = Reducer.recurse { (self) in - Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .clearSubStates: - state.readingState = .init() - state.archivesState = .init() - state.torrentsState = .init() - state.previewsState = .init() - state.commentsState = .init() - state.commentContent = .init() - state.postCommentFocused = false - state.galleryInfosState = .init() - state.detailSearchState = .init() - return .merge( - .init(value: .reading(.teardown)), - .init(value: .archives(.teardown)), - .init(value: .torrents(.teardown)), - .init(value: .previews(.teardown)), - .init(value: .comments(.teardown)), - .init(value: .detailSearch(.teardown)) - ) - - case .onPostCommentAppear: - return .init(value: .setPostCommentFocused(true)) - .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect() - - case .onAppear(let gid, let showsNewDawnGreeting): - state.showsNewDawnGreeting = showsNewDawnGreeting - if state.detailSearchState == nil { - state.detailSearchState = .init() - } - if state.commentsState == nil { - state.commentsState = .init() - } - return .init(value: .fetchDatabaseInfos(gid)) - - case .toggleShowFullTitle: - state.showsFullTitle.toggle() - return environment.hapticsClient.generateFeedback(.soft).fireAndForget() - - case .toggleShowUserRating: - state.showsUserRating.toggle() - return environment.hapticsClient.generateFeedback(.soft).fireAndForget() - - 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( - .init(value: .rateGallery), - environment.hapticsClient.generateFeedback(.soft).fireAndForget(), - .init(value: .confirmRatingDone).delay(for: 1, scheduler: DispatchQueue.main).eraseToEffect() - ) - - case .confirmRatingDone: - state.showsUserRating = false - return .none - - case .syncGalleryTags: - return environment.databaseClient - .updateGalleryTags(gid: state.gallery.id, tags: state.galleryTags).fireAndForget() - - case .syncGalleryDetail: - guard let detail = state.galleryDetail else { return .none } - return environment.databaseClient.cacheGalleryDetail(detail).fireAndForget() - - case .syncGalleryPreviewURLs: - return environment.databaseClient - .updatePreviewURLs(gid: state.gallery.id, previewURLs: state.galleryPreviewURLs).fireAndForget() - - case .syncGalleryComments: - return environment.databaseClient - .updateComments(gid: state.gallery.id, comments: state.galleryComments).fireAndForget() - - case .syncGreeting(let greeting): - return environment.databaseClient.updateGreeting(greeting).fireAndForget() - - case .syncPreviewConfig(let config): - return environment.databaseClient - .updatePreviewConfig(gid: state.gallery.id, config: config).fireAndForget() - - case .saveGalleryHistory: - return environment.databaseClient.updateLastOpenDate(gid: state.gallery.id).fireAndForget() - - case .updateReadingProgress(let progress): - return environment.databaseClient - .updateReadingProgress(gid: state.gallery.id, progress: progress).fireAndForget() - - case .teardown: - return .cancel(id: DetailState.CancelID()) - - case .fetchDatabaseInfos(let gid): - guard let gallery = environment.databaseClient.fetchGallery(gid: gid) else { return .none } - state.gallery = gallery - if let detail = environment.databaseClient.fetchGalleryDetail(gid: gid) { - state.galleryDetail = detail - } - return .merge( - .init(value: .saveGalleryHistory), - environment.databaseClient.fetchGalleryState(gid: state.gallery.id) - .map(DetailAction.fetchDatabaseInfosDone).cancellable(id: DetailState.CancelID()) - ) - - case .fetchDatabaseInfosDone(let galleryState): - state.galleryTags = galleryState.tags - state.galleryPreviewURLs = galleryState.previewURLs - state.galleryComments = galleryState.comments - return .init(value: .fetchGalleryDetail) - - case .fetchGalleryDetail: - guard state.loadingState != .loading, - let galleryURL = state.gallery.galleryURL - else { return .none } - state.loadingState = .loading - return GalleryDetailRequest(gid: state.gallery.id, galleryURL: galleryURL) - .effect.map(DetailAction.fetchGalleryDetailDone).cancellable(id: DetailState.CancelID()) - - case .fetchGalleryDetailDone(let result): - state.loadingState = .idle - switch result { - case .success(let (galleryDetail, galleryState, apiKey, greeting)): - var effects: [EffectTask] = [ - .init(value: .syncGalleryTags), - .init(value: .syncGalleryDetail), - .init(value: .syncGalleryPreviewURLs), - .init(value: .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(.init(value: .syncGreeting(greeting))) - if !greeting.gainedNothing && state.showsNewDawnGreeting { - effects.append(.init(value: .setNavigation(.newDawn(greeting)))) - } - } - if let config = galleryState.previewConfig { - effects.append(.init(value: .syncPreviewConfig(config))) - } - return .merge(effects) - case .failure(let error): - state.loadingState = .failed(error) - } - return .none - - case .rateGallery: - guard let apiuid = Int(environment.cookieClient.apiuid), let gid = Int(state.gallery.id) - else { return .none } - return RateGalleryRequest( - apiuid: apiuid, apikey: state.apiKey, gid: gid, - token: state.gallery.token, rating: state.userRating - ) - .effect.map(DetailAction.anyGalleryOpsDone).cancellable(id: DetailState.CancelID()) - - case .favorGallery(let favIndex): - return FavorGalleryRequest(gid: state.gallery.id, token: state.gallery.token, favIndex: favIndex) - .effect.map(DetailAction.anyGalleryOpsDone).cancellable(id: DetailState.CancelID()) - - case .unfavorGallery: - return UnfavorGalleryRequest(gid: state.gallery.id).effect.map(DetailAction.anyGalleryOpsDone) - .cancellable(id: DetailState.CancelID()) - - case .postComment(let galleryURL): - guard !state.commentContent.isEmpty else { return .none } - return CommentGalleryRequest(content: state.commentContent, galleryURL: galleryURL) - .effect.map(DetailAction.anyGalleryOpsDone).cancellable(id: DetailState.CancelID()) - - case .voteTag(let tag, let vote): - guard let apiuid = Int(environment.cookieClient.apiuid), let gid = Int(state.gallery.id) - else { return .none } - return VoteGalleryTagRequest( - apiuid: apiuid, apikey: state.apiKey, gid: gid, token: state.gallery.token, tag: tag, vote: vote - ) - .effect.map(DetailAction.anyGalleryOpsDone).cancellable(id: DetailState.CancelID()) - - case .anyGalleryOpsDone(let result): - if case .success = result { - return .merge( - .init(value: .fetchGalleryDetail), - environment.hapticsClient.generateNotificationFeedback(.success).fireAndForget() - ) - } - return environment.hapticsClient.generateNotificationFeedback(.error).fireAndForget() - - case .reading(.onPerformDismiss): - return .init(value: .setNavigation(nil)) - - case .reading: - return .none - - case .archives: - return .none - - case .torrents: - return .none - - case .previews: - return .none - - case .comments(.performCommentActionDone(let result)): - return .init(value: .anyGalleryOpsDone(result)) - - case .comments(.detail(let recursiveAction)): - guard state.commentsState != nil else { return .none } - return self.run(&state.commentsState!.detailState, recursiveAction, environment) - .map({ DetailAction.comments(.detail($0)) }) - - case .comments: - return .none - - case .galleryInfos: - return .none - - case .detailSearch(.detail(let recursiveAction)): - guard state.detailSearchState != nil else { return .none } - return self.run(&state.detailSearchState!.detailState, recursiveAction, environment) - .map({ DetailAction.detailSearch(.detail($0)) }) - - case .detailSearch: - return .none - } - } - .haptics( - unwrapping: \.route, - case: /DetailState.Route.detailSearch, - hapticsClient: \.hapticsClient, - style: .soft - ) - .haptics( - unwrapping: \.route, - case: /DetailState.Route.postComment, - hapticsClient: \.hapticsClient - ) - .haptics( - unwrapping: \.route, - case: /DetailState.Route.tagDetail, - hapticsClient: \.hapticsClient - ) - .haptics( - unwrapping: \.route, - case: /DetailState.Route.torrents, - hapticsClient: \.hapticsClient - ) - .haptics( - unwrapping: \.route, - case: /DetailState.Route.archives, - hapticsClient: \.hapticsClient - ) - .haptics( - unwrapping: \.route, - case: /DetailState.Route.reading, - hapticsClient: \.hapticsClient - ) - .haptics( - unwrapping: \.route, - case: /DetailState.Route.share, - hapticsClient: \.hapticsClient - ) - .binding(), - readingReducer.pullback( - state: \.readingState, - action: /DetailAction.reading, - environment: { - .init( - urlClient: $0.urlClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient - ) - } - ), - archivesReducer.pullback( - state: \.archivesState, - action: /DetailAction.archives, - environment: { - .init( - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient - ) - } - ), - torrentsReducer.pullback( - state: \.torrentsState, - action: /DetailAction.torrents, - environment: { - .init( - fileClient: $0.fileClient, - hapticsClient: $0.hapticsClient, - clipboardClient: $0.clipboardClient - ) - } - ), - previewsReducer.pullback( - state: \.previewsState, - action: /DetailAction.previews, - environment: { - .init( - urlClient: $0.urlClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient - ) - } - ), - commentsReducer.optional().pullback( - state: \.commentsState, - action: /DetailAction.comments, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ), - detailSearchReducer.optional().pullback( - state: \.detailSearchState, - action: /DetailAction.detailSearch, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ), - galleryInfosReducer.pullback( - state: \.galleryInfosState, - action: /DetailAction.galleryInfos, - environment: { - .init( - hapticsClient: $0.hapticsClient, - clipboardClient: $0.clipboardClient - ) - } - ) - ) -} diff --git a/EhPanda/View/Detail/DataFlow/GalleryInfosStore.swift b/EhPanda/View/Detail/DataFlow/GalleryInfosStore.swift deleted file mode 100644 index 9f75496a..00000000 --- a/EhPanda/View/Detail/DataFlow/GalleryInfosStore.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// GalleryInfosStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/23. -// - -import TTProgressHUD -import ComposableArchitecture - -struct GalleryInfosState: Equatable { - enum Route { - case hud - } - - @BindingState var route: Route? - var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded -} - -enum GalleryInfosAction: BindableAction { - case binding(BindingAction) - case copyText(String) -} - -struct GalleryInfosEnvironment { - let hapticsClient: HapticsClient - let clipboardClient: ClipboardClient -} - -let galleryInfosReducer = Reducer -{ state, action, environment in - switch action { - case .binding: - return .none - - case .copyText(let text): - state.route = .hud - return .merge( - environment.clipboardClient.saveText(text).fireAndForget(), - environment.hapticsClient.generateNotificationFeedback(.success).fireAndForget() - ) - } -} -.binding() diff --git a/EhPanda/View/Detail/DataFlow/PreviewsStore.swift b/EhPanda/View/Detail/DataFlow/PreviewsStore.swift deleted file mode 100644 index df1d94ce..00000000 --- a/EhPanda/View/Detail/DataFlow/PreviewsStore.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// PreviewsStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/16. -// - -import Foundation -import ComposableArchitecture - -struct PreviewsState: Equatable { - enum Route { - case reading - } - struct CancelID: Hashable { - let id = String(describing: PreviewsState.self) - } - - @BindingState var route: Route? - - var gallery: Gallery = .empty - var loadingState: LoadingState = .idle - var databaseLoadingState: LoadingState = .loading - - var previewURLs = [Int: URL]() - var previewConfig: PreviewConfig = .normal(rows: 4) - - var readingState = ReadingState() - - mutating func updatePreviewURLs(_ previewURLs: [Int: URL]) { - self.previewURLs = self.previewURLs.merging( - previewURLs, uniquingKeysWith: { stored, _ in stored } - ) - } -} - -enum PreviewsAction: BindableAction { - case binding(BindingAction) - case setNavigation(PreviewsState.Route?) - case clearSubStates - - case syncPreviewURLs([Int: URL]) - case updateReadingProgress(Int) - - case teardown - case fetchDatabaseInfos(String) - case fetchDatabaseInfosDone(GalleryState) - case fetchPreviewURLs(Int) - case fetchPreviewURLsDone(Result<[Int: URL], AppError>) - - case reading(ReadingAction) -} - -struct PreviewsEnvironment { - let urlClient: URLClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient -} - -let previewsReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .clearSubStates: - state.readingState = .init() - return .init(value: .reading(.teardown)) - - case .syncPreviewURLs(let previewURLs): - return environment.databaseClient - .updatePreviewURLs(gid: state.gallery.id, previewURLs: previewURLs).fireAndForget() - - case .updateReadingProgress(let progress): - return environment.databaseClient - .updateReadingProgress(gid: state.gallery.id, progress: progress).fireAndForget() - - case .teardown: - return .cancel(id: PreviewsState.CancelID()) - - case .fetchDatabaseInfos(let gid): - guard let gallery = environment.databaseClient.fetchGallery(gid: gid) else { return .none } - state.gallery = gallery - return environment.databaseClient.fetchGalleryState(gid: state.gallery.id) - .map(PreviewsAction.fetchDatabaseInfosDone).cancellable(id: PreviewsState.CancelID()) - - case .fetchDatabaseInfosDone(let galleryState): - if let previewConfig = galleryState.previewConfig { - state.previewConfig = previewConfig - } - state.previewURLs = galleryState.previewURLs - state.databaseLoadingState = .idle - return .none - - case .fetchPreviewURLs(let index): - guard state.loadingState != .loading, - let galleryURL = state.gallery.galleryURL - else { return .none } - state.loadingState = .loading - let pageNum = state.previewConfig.pageNumber(index: index) - return GalleryPreviewURLsRequest(galleryURL: galleryURL, pageNum: pageNum) - .effect.map(PreviewsAction.fetchPreviewURLsDone).cancellable(id: PreviewsState.CancelID()) - - case .fetchPreviewURLsDone(let result): - state.loadingState = .idle - - switch result { - case .success(let previewURLs): - guard !previewURLs.isEmpty else { - state.loadingState = .failed(.notFound) - return .none - } - state.updatePreviewURLs(previewURLs) - return .init(value: .syncPreviewURLs(previewURLs)) - case .failure(let error): - state.loadingState = .failed(error) - } - return .none - - case .reading(.onPerformDismiss): - return .init(value: .setNavigation(nil)) - - case .reading: - return .none - } - } - .haptics( - unwrapping: \.route, - case: /PreviewsState.Route.reading, - hapticsClient: \.hapticsClient - ) - .binding(), - readingReducer.pullback( - state: \.readingState, - action: /PreviewsAction.reading, - environment: { - .init( - urlClient: $0.urlClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient - ) - } - ) -) diff --git a/EhPanda/View/Detail/DataFlow/TorrentsStore.swift b/EhPanda/View/Detail/DataFlow/TorrentsStore.swift deleted file mode 100644 index eaa4e481..00000000 --- a/EhPanda/View/Detail/DataFlow/TorrentsStore.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// TorrentsStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/19. -// - -import Foundation -import TTProgressHUD -import ComposableArchitecture - -struct TorrentsState: Equatable { - enum Route: Equatable { - case hud - case share(URL) - } - struct CancelID: Hashable { - let id = String(describing: TorrentsState.self) - } - - @BindingState var route: Route? - var torrents = [GalleryTorrent]() - var loadingState: LoadingState = .idle - var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded -} - -enum TorrentsAction: BindableAction { - case binding(BindingAction) - case setNavigation(TorrentsState.Route) - - case copyText(String) - case presentTorrentActivity(String, Data) - - case teardown - case fetchTorrent(String, URL) - case fetchTorrentDone(String, Result) - case fetchGalleryTorrents(String, String) - case fetchGalleryTorrentsDone(Result<[GalleryTorrent], AppError>) -} - -struct TorrentsEnvironment { - let fileClient: FileClient - let hapticsClient: HapticsClient - let clipboardClient: ClipboardClient -} - -let torrentsReducer = Reducer { state, action, environment in - switch action { - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return .none - - case .copyText(let magnetURL): - state.route = .hud - return .merge( - environment.clipboardClient.saveText(magnetURL).fireAndForget(), - environment.hapticsClient.generateNotificationFeedback(.success).fireAndForget() - ) - - case .presentTorrentActivity(let hash, let data): - if let url = environment.fileClient.saveTorrent(hash: hash, data: data) { - return .init(value: .setNavigation(.share(url))) - } - return .none - - case .fetchTorrent(let hash, let torrentURL): - return DataRequest(url: torrentURL).effect.map({ TorrentsAction.fetchTorrentDone(hash, $0) }) - .cancellable(id: TorrentsState.CancelID()) - - case .teardown: - return .cancel(id: TorrentsState.CancelID()) - - case .fetchTorrentDone(let hash, let result): - if case .success(let data) = result, !data.isEmpty { - return .init(value: .presentTorrentActivity(hash, data)) - } - return .none - - case .fetchGalleryTorrents(let gid, let token): - guard state.loadingState != .loading else { return .none } - state.loadingState = .loading - return GalleryTorrentsRequest(gid: gid, token: token) - .effect.map(TorrentsAction.fetchGalleryTorrentsDone).cancellable(id: TorrentsState.CancelID()) - - case .fetchGalleryTorrentsDone(let result): - state.loadingState = .idle - switch result { - case .success(let torrents): - guard !torrents.isEmpty else { - state.loadingState = .failed(.notFound) - return .none - } - state.torrents = torrents - case .failure(let error): - state.loadingState = .failed(error) - } - return .none - } -} -.haptics( - unwrapping: \.route, - case: /TorrentsState.Route.share, - hapticsClient: \.hapticsClient -) -.binding() diff --git a/EhPanda/View/Detail/DetailReducer.swift b/EhPanda/View/Detail/DetailReducer.swift new file mode 100644 index 00000000..658f5bf7 --- /dev/null +++ b/EhPanda/View/Detail/DetailReducer.swift @@ -0,0 +1,420 @@ +// +// DetailReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/10. +// + +import SwiftUI +import Foundation +import ComposableArchitecture + +struct DetailReducer: ReducerProtocol { + enum Route: Equatable { + case reading + case archives(URL, URL) + case torrents + case previews + case comments(URL) + case share(URL) + case postComment + case newDawn(Greeting) + case detailSearch(String) + case tagDetail(TagDetail) + case galleryInfos(Gallery, GalleryDetail) + } + + struct CancelID: Hashable { + let id = String(describing: DetailReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var commentContent = "" + @BindingState var postCommentFocused = false + + var showsNewDawnGreeting = false + var showsUserRating = false + var showsFullTitle = false + var userRating = 0 + + var apiKey = "" + var loadingState: LoadingState = .idle + var gallery: Gallery = .empty + var galleryDetail: GalleryDetail? + var galleryTags = [GalleryTag]() + var galleryPreviewURLs = [Int: URL]() + var galleryComments = [GalleryComment]() + + var readingState = ReadingReducer.State() + var archivesState = ArchivesReducer.State() + var torrentsState = TorrentsReducer.State() + var previewsState = PreviewsReducer.State() + @Heap var commentsState: CommentsReducer.State? + var galleryInfosState = GalleryInfosReducer.State() + @Heap var detailSearchState: DetailSearchReducer.State? + + init() { + _commentsState = .init(nil) + _detailSearchState = .init(nil) + } + + mutating func updateRating(value: DragGesture.Value) { + let rating = Int(value.location.x / 31 * 2) + 1 + userRating = min(max(rating, 1), 10) + } + } + + indirect enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + case onPostCommentAppear + case onAppear(String, Bool) + + case toggleShowFullTitle + case toggleShowUserRating + case setCommentContent(String) + case setPostCommentFocused(Bool) + case updateRating(DragGesture.Value) + case confirmRating(DragGesture.Value) + case confirmRatingDone + + case syncGalleryTags + case syncGalleryDetail + case syncGalleryPreviewURLs + case syncGalleryComments + case syncGreeting(Greeting) + case syncPreviewConfig(PreviewConfig) + case saveGalleryHistory + case updateReadingProgress(Int) + + case teardown + case fetchDatabaseInfos(String) + case fetchDatabaseInfosDone(GalleryState) + case fetchGalleryDetail + case fetchGalleryDetailDone(Result<(GalleryDetail, GalleryState, String, Greeting?), AppError>) + + case rateGallery + case favorGallery(Int) + case unfavorGallery + case postComment(URL) + case voteTag(String, Int) + case anyGalleryOpsDone(Result) + + case reading(ReadingReducer.Action) + case archives(ArchivesReducer.Action) + case torrents(TorrentsReducer.Action) + case previews(PreviewsReducer.Action) + case comments(CommentsReducer.Action) + case galleryInfos(GalleryInfosReducer.Action) + case detailSearch(DetailSearchReducer.Action) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + @Dependency(\.cookieClient) private var cookieClient + + var body: some ReducerProtocol { + RecurseReducer { (self) in + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .clearSubStates: + state.readingState = .init() + state.archivesState = .init() + state.torrentsState = .init() + state.previewsState = .init() + state.commentsState = .init() + state.commentContent = .init() + state.postCommentFocused = false + state.galleryInfosState = .init() + state.detailSearchState = .init() + return .merge( + .init(value: .reading(.teardown)), + .init(value: .archives(.teardown)), + .init(value: .torrents(.teardown)), + .init(value: .previews(.teardown)), + .init(value: .comments(.teardown)), + .init(value: .detailSearch(.teardown)) + ) + + case .onPostCommentAppear: + return .init(value: .setPostCommentFocused(true)) + .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect() + + case .onAppear(let gid, let showsNewDawnGreeting): + state.showsNewDawnGreeting = showsNewDawnGreeting + if state.detailSearchState == nil { + state.detailSearchState = .init() + } + if state.commentsState == nil { + state.commentsState = .init() + } + return .init(value: .fetchDatabaseInfos(gid)) + + case .toggleShowFullTitle: + state.showsFullTitle.toggle() + return hapticsClient.generateFeedback(.soft).fireAndForget() + + case .toggleShowUserRating: + state.showsUserRating.toggle() + return hapticsClient.generateFeedback(.soft).fireAndForget() + + 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( + .init(value: .rateGallery), + hapticsClient.generateFeedback(.soft).fireAndForget(), + .init(value: .confirmRatingDone).delay(for: 1, scheduler: DispatchQueue.main).eraseToEffect() + ) + + case .confirmRatingDone: + state.showsUserRating = false + return .none + + case .syncGalleryTags: + return databaseClient + .updateGalleryTags(gid: state.gallery.id, tags: state.galleryTags).fireAndForget() + + case .syncGalleryDetail: + guard let detail = state.galleryDetail else { return .none } + return databaseClient.cacheGalleryDetail(detail).fireAndForget() + + case .syncGalleryPreviewURLs: + return databaseClient + .updatePreviewURLs(gid: state.gallery.id, previewURLs: state.galleryPreviewURLs).fireAndForget() + + case .syncGalleryComments: + return databaseClient + .updateComments(gid: state.gallery.id, comments: state.galleryComments).fireAndForget() + + case .syncGreeting(let greeting): + return databaseClient.updateGreeting(greeting).fireAndForget() + + case .syncPreviewConfig(let config): + return databaseClient + .updatePreviewConfig(gid: state.gallery.id, config: config).fireAndForget() + + case .saveGalleryHistory: + return databaseClient.updateLastOpenDate(gid: state.gallery.id).fireAndForget() + + case .updateReadingProgress(let progress): + return databaseClient + .updateReadingProgress(gid: state.gallery.id, progress: progress).fireAndForget() + + case .teardown: + return .cancel(id: CancelID()) + + 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( + .init(value: .saveGalleryHistory), + databaseClient.fetchGalleryState(gid: state.gallery.id) + .map(Action.fetchDatabaseInfosDone).cancellable(id: CancelID()) + ) + + case .fetchDatabaseInfosDone(let galleryState): + state.galleryTags = galleryState.tags + state.galleryPreviewURLs = galleryState.previewURLs + state.galleryComments = galleryState.comments + return .init(value: .fetchGalleryDetail) + + case .fetchGalleryDetail: + guard state.loadingState != .loading, + let galleryURL = state.gallery.galleryURL + else { return .none } + state.loadingState = .loading + return GalleryDetailRequest(gid: state.gallery.id, galleryURL: galleryURL) + .effect.map(Action.fetchGalleryDetailDone).cancellable(id: CancelID()) + + case .fetchGalleryDetailDone(let result): + state.loadingState = .idle + switch result { + case .success(let (galleryDetail, galleryState, apiKey, greeting)): + var effects: [EffectTask] = [ + .init(value: .syncGalleryTags), + .init(value: .syncGalleryDetail), + .init(value: .syncGalleryPreviewURLs), + .init(value: .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(.init(value: .syncGreeting(greeting))) + if !greeting.gainedNothing && state.showsNewDawnGreeting { + effects.append(.init(value: .setNavigation(.newDawn(greeting)))) + } + } + if let config = galleryState.previewConfig { + effects.append(.init(value: .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 RateGalleryRequest( + apiuid: apiuid, apikey: state.apiKey, gid: gid, + token: state.gallery.token, rating: state.userRating + ) + .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID()) + + case .favorGallery(let favIndex): + return FavorGalleryRequest(gid: state.gallery.id, token: state.gallery.token, favIndex: favIndex) + .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID()) + + case .unfavorGallery: + return UnfavorGalleryRequest(gid: state.gallery.id).effect.map(Action.anyGalleryOpsDone) + .cancellable(id: CancelID()) + + case .postComment(let galleryURL): + guard !state.commentContent.isEmpty else { return .none } + return CommentGalleryRequest(content: state.commentContent, galleryURL: galleryURL) + .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID()) + + case .voteTag(let tag, let vote): + guard let apiuid = Int(cookieClient.apiuid), let gid = Int(state.gallery.id) + else { return .none } + return VoteGalleryTagRequest( + apiuid: apiuid, apikey: state.apiKey, gid: gid, token: state.gallery.token, tag: tag, vote: vote + ) + .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID()) + + case .anyGalleryOpsDone(let result): + if case .success = result { + return .merge( + .init(value: .fetchGalleryDetail), + hapticsClient.generateNotificationFeedback(.success).fireAndForget() + ) + } + return hapticsClient.generateNotificationFeedback(.error).fireAndForget() + + case .reading(.onPerformDismiss): + return .init(value: .setNavigation(nil)) + + case .reading: + return .none + + case .archives: + return .none + + case .torrents: + return .none + + case .previews: + return .none + + case .comments(.performCommentActionDone(let result)): + return .init(value: .anyGalleryOpsDone(result)) + + case .comments(.detail(let recursiveAction)): + guard state.commentsState != nil else { return .none } + return self.reduce(into: &state.commentsState!.detailState, action: recursiveAction) + .map({ Action.comments(.detail($0)) }) + + case .comments: + return .none + + case .galleryInfos: + return .none + + case .detailSearch(.detail(let recursiveAction)): + guard state.detailSearchState != nil else { return .none } + return self.reduce(into: &state.detailSearchState!.detailState, action: recursiveAction) + .map({ Action.detailSearch(.detail($0)) }) + + case .detailSearch: + return .none + } + } + } + .ifLet( + \.commentsState, + action: /Action.comments, + then: CommentsReducer.init + ) + .ifLet( + \.detailSearchState, + action: /Action.detailSearch, + then: DetailSearchReducer.init + ) + .haptics( + unwrapping: \.route, + case: /Route.detailSearch, + hapticsClient: hapticsClient, + style: .soft + ) + .haptics( + unwrapping: \.route, + case: /Route.postComment, + hapticsClient: hapticsClient + ) + .haptics( + unwrapping: \.route, + case: /Route.tagDetail, + hapticsClient: hapticsClient + ) + .haptics( + unwrapping: \.route, + case: /Route.torrents, + hapticsClient: hapticsClient + ) + .haptics( + unwrapping: \.route, + case: /Route.archives, + hapticsClient: hapticsClient + ) + .haptics( + unwrapping: \.route, + case: /Route.reading, + hapticsClient: hapticsClient + ) + .haptics( + unwrapping: \.route, + case: /Route.share, + hapticsClient: hapticsClient + ) + + 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) + + BindingReducer() + } +} diff --git a/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift b/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift new file mode 100644 index 00000000..d07fa724 --- /dev/null +++ b/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift @@ -0,0 +1,187 @@ +// +// DetailSearchReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/12. +// + +import ComposableArchitecture + +struct DetailSearchReducer: ReducerProtocol { + enum Route: Equatable { + case filters + case quickSearch + case detail(String) + } + + struct CancelID: Hashable { + let id = String(describing: DetailSearchReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var keyword = "" + var lastKeyword = "" + + var galleries = [Gallery]() + var pageNumber = PageNumber() + var loadingState: LoadingState = .idle + var footerLoadingState: LoadingState = .idle + + @Heap var detailState: DetailReducer.State! + var filtersState = FiltersReducer.State() + var quickDetailSearchState = QuickSearchReducer.State() + + init() { + _detailState = .init(.init()) + } + + mutating func insertGalleries(_ galleries: [Gallery]) { + galleries.forEach { gallery in + if !self.galleries.contains(gallery) { + self.galleries.append(gallery) + } + } + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + + case teardown + case fetchGalleries(String? = nil) + case fetchGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) + case fetchMoreGalleries + case fetchMoreGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) + + case detail(DetailReducer.Action) + case filters(FiltersReducer.Action) + case quickSearch(QuickSearchReducer.Action) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding(\.$keyword): + if !state.keyword.isEmpty { + state.lastKeyword = state.keyword + } + return .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .clearSubStates: + state.detailState = .init() + state.filtersState = .init() + state.quickDetailSearchState = .init() + return .merge( + .init(value: .detail(.teardown)), + .init(value: .quickSearch(.teardown)) + ) + + case .teardown: + return .cancel(id: CancelID()) + + case .fetchGalleries(let keyword): + guard state.loadingState != .loading else { return .none } + if let keyword = keyword { + state.keyword = keyword + state.lastKeyword = keyword + } + state.loadingState = .loading + state.pageNumber.resetPages() + let filter = databaseClient.fetchFilterSynchronously(range: .search) + return SearchGalleriesRequest(keyword: state.lastKeyword, filter: filter).effect + .map(Action.fetchGalleriesDone).cancellable(id: CancelID()) + + case .fetchGalleriesDone(let result): + state.loadingState = .idle + switch result { + case .success(let (pageNumber, galleries)): + guard !galleries.isEmpty else { + state.loadingState = .failed(.notFound) + guard pageNumber.hasNextPage() else { return .none } + return .init(value: .fetchMoreGalleries) + } + state.pageNumber = pageNumber + state.galleries = galleries + return databaseClient.cacheGalleries(galleries).fireAndForget() + case .failure(let error): + state.loadingState = .failed(error) + } + return .none + + case .fetchMoreGalleries: + let pageNumber = state.pageNumber + guard pageNumber.hasNextPage(), + state.footerLoadingState != .loading, + let lastID = state.galleries.last?.id + else { return .none } + state.footerLoadingState = .loading + let filter = databaseClient.fetchFilterSynchronously(range: .search) + return MoreSearchGalleriesRequest(keyword: state.lastKeyword, filter: filter, lastID: lastID).effect + .map(Action.fetchMoreGalleriesDone) + .cancellable(id: CancelID()) + + case .fetchMoreGalleriesDone(let result): + state.footerLoadingState = .idle + switch result { + case .success(let (pageNumber, galleries)): + state.pageNumber = pageNumber + state.insertGalleries(galleries) + + var effects: [EffectTask] = [ + databaseClient.cacheGalleries(galleries).fireAndForget() + ] + if galleries.isEmpty, pageNumber.hasNextPage() { + effects.append(.init(value: .fetchMoreGalleries)) + } else if !galleries.isEmpty { + state.loadingState = .idle + } + return .merge(effects) + + case .failure(let error): + state.footerLoadingState = .failed(error) + } + return .none + + case .detail: + return .none + + case .filters: + return .none + + case .quickSearch: + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.quickSearch, + hapticsClient: hapticsClient + ) + .haptics( + unwrapping: \.route, + case: /Route.filters, + hapticsClient: hapticsClient + ) + + Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) + Scope(state: \.quickDetailSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) + + BindingReducer() + } +} diff --git a/EhPanda/View/Detail/DetailSearchView.swift b/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift similarity index 80% rename from EhPanda/View/Detail/DetailSearchView.swift rename to EhPanda/View/Detail/DetailSearch/DetailSearchView.swift index 5009dad2..8f535b6c 100644 --- a/EhPanda/View/Detail/DetailSearchView.swift +++ b/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift @@ -9,8 +9,8 @@ import SwiftUI import ComposableArchitecture struct DetailSearchView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let keyword: String private let user: User @Binding private var setting: Setting @@ -18,7 +18,7 @@ struct DetailSearchView: View { private let tagTranslator: TagTranslator init( - store: Store, + store: StoreOf, keyword: String, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store @@ -46,21 +46,21 @@ struct DetailSearchView: View { ) .sheet( unwrapping: viewStore.binding(\.$route), - case: /DetailSearchState.Route.detail, + case: /DetailSearchReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: DetailSearchAction.detail), + store: store.scope(state: \.detailState, action: DetailSearchReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailSearchState.Route.quickSearch) { _ in + .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailSearchReducer.Route.quickSearch) { _ in QuickSearchView( - store: store.scope(state: \.quickDetailSearchState, action: DetailSearchAction.quickSearch) + store: store.scope(state: \.quickDetailSearchState, action: DetailSearchReducer.Action.quickSearch) ) { keyword in viewStore.send(.setNavigation(nil)) viewStore.send(.fetchGalleries(keyword)) @@ -68,8 +68,8 @@ struct DetailSearchView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailSearchState.Route.filters) { _ in - FiltersView(store: store.scope(state: \.filtersState, action: DetailSearchAction.filters)) + .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailSearchReducer.Route.filters) { _ in + FiltersView(store: store.scope(state: \.filtersState, action: DetailSearchReducer.Action.filters)) .accentColor(setting.accentColor).autoBlur(radius: blurRadius) } .searchable(text: viewStore.binding(\.$keyword)) { @@ -95,9 +95,9 @@ struct DetailSearchView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailSearchState.Route.detail) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailSearchReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: DetailSearchAction.detail), + store: store.scope(state: \.detailState, action: DetailSearchReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -123,19 +123,7 @@ struct DetailSearchView_Previews: PreviewProvider { DetailSearchView( store: .init( initialState: .init(), - reducer: detailSearchReducer, - environment: DetailSearchEnvironment( - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - uiApplicationClient: .live - ) + reducer: DetailSearchReducer() ), keyword: .init(), user: .init(), diff --git a/EhPanda/View/Detail/DetailView.swift b/EhPanda/View/Detail/DetailView.swift index 55bae0aa..a5946b28 100644 --- a/EhPanda/View/Detail/DetailView.swift +++ b/EhPanda/View/Detail/DetailView.swift @@ -11,8 +11,8 @@ import ComposableArchitecture import CommonMark struct DetailView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let gid: String private let user: User @Binding private var setting: Setting @@ -20,7 +20,7 @@ struct DetailView: View { private let tagTranslator: TagTranslator init( - store: Store, gid: String, + store: StoreOf, gid: String, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store @@ -121,36 +121,36 @@ struct DetailView: View { ErrorView(error: error ?? .unknown, action: error?.isRetryable != false ? retryAction : nil) .opacity(viewStore.galleryDetail == nil && error != nil ? 1 : 0) } - .fullScreenCover(unwrapping: viewStore.binding(\.$route), case: /DetailState.Route.reading) { _ in + .fullScreenCover(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.reading) { _ in ReadingView( - store: store.scope(state: \.readingState, action: DetailAction.reading), + store: store.scope(state: \.readingState, action: DetailReducer.Action.reading), gid: gid, setting: $setting, blurRadius: blurRadius ) .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailState.Route.archives) { route in + .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.archives) { route in let (galleryURL, archiveURL) = route.wrappedValue ArchivesView( - store: store.scope(state: \.archivesState, action: DetailAction.archives), + store: store.scope(state: \.archivesState, action: DetailReducer.Action.archives), gid: gid, user: user, galleryURL: galleryURL, archiveURL: archiveURL ) .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailState.Route.torrents) { _ in + .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.torrents) { _ in TorrentsView( - store: store.scope(state: \.torrentsState, action: DetailAction.torrents), + store: store.scope(state: \.torrentsState, action: DetailReducer.Action.torrents), gid: gid, token: viewStore.gallery.token, blurRadius: blurRadius ) .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailState.Route.share) { route in + .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.share) { route in ActivityView(activityItems: [route.wrappedValue]) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailState.Route.postComment) { _ in + .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.postComment) { _ in PostCommentView( title: L10n.Localizable.PostCommentView.Title.postComment, content: viewStore.binding(\.$commentContent), @@ -167,10 +167,10 @@ struct DetailView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailState.Route.newDawn) { route in + .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.newDawn) { route in NewDawnView(greeting: route.wrappedValue).autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailState.Route.tagDetail) { route in + .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.tagDetail) { route in TagDetailView(detail: route.wrappedValue).autoBlur(radius: blurRadius) } .animation(.default, value: viewStore.showsUserRating) @@ -189,14 +189,14 @@ struct DetailView: View { // MARK: NavigationLinks private extension DetailView { @ViewBuilder var navigationLinks: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailState.Route.previews) { _ in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.previews) { _ in PreviewsView( - store: store.scope(state: \.previewsState, action: DetailAction.previews), + store: store.scope(state: \.previewsState, action: DetailReducer.Action.previews), gid: gid, setting: $setting, blurRadius: blurRadius ) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailState.Route.comments) { route in - IfLetStore(store.scope(state: \.commentsState, action: DetailAction.comments)) { store in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.comments) { route in + IfLetStore(store.scope(state: \.commentsState, action: DetailReducer.Action.comments)) { store in CommentsView( store: store, gid: gid, token: viewStore.gallery.token, apiKey: viewStore.apiKey, galleryURL: route.wrappedValue, comments: viewStore.galleryComments, user: user, @@ -205,18 +205,18 @@ private extension DetailView { ) } } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailState.Route.detailSearch) { route in - IfLetStore(store.scope(state: \.detailSearchState, action: DetailAction.detailSearch)) { store in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.detailSearch) { route in + IfLetStore(store.scope(state: \.detailSearchState, action: DetailReducer.Action.detailSearch)) { store in DetailSearchView( store: store, keyword: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailState.Route.galleryInfos) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.galleryInfos) { route in let (gallery, galleryDetail) = route.wrappedValue GalleryInfosView( - store: store.scope(state: \.galleryInfosState, action: DetailAction.galleryInfos), + store: store.scope(state: \.galleryInfosState, action: DetailReducer.Action.galleryInfos), gallery: gallery, galleryDetail: galleryDetail ) } @@ -849,19 +849,7 @@ struct DetailView_Previews: PreviewProvider { DetailView( store: .init( initialState: .init(), - reducer: detailReducer, - environment: DetailEnvironment( - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - uiApplicationClient: .live - ) + reducer: DetailReducer() ), gid: .init(), user: .init(), diff --git a/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift b/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift new file mode 100644 index 00000000..d7cbb2d2 --- /dev/null +++ b/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift @@ -0,0 +1,46 @@ +// +// GalleryInfosReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/23. +// + +import TTProgressHUD +import ComposableArchitecture + +struct GalleryInfosReducer: ReducerProtocol { + enum Route { + case hud + } + + struct State: Equatable { + @BindingState var route: Route? + var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case copyText(String) + } + + @Dependency(\.clipboardClient) private var clipboardClient + @Dependency(\.hapticsClient) private var hapticsClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding: + return .none + + case .copyText(let text): + state.route = .hud + return .merge( + clipboardClient.saveText(text).fireAndForget(), + hapticsClient.generateNotificationFeedback(.success).fireAndForget() + ) + } + } + + BindingReducer() + } +} diff --git a/EhPanda/View/Detail/GalleryInfosView.swift b/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift similarity index 90% rename from EhPanda/View/Detail/GalleryInfosView.swift rename to EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift index bae486c4..09500be6 100644 --- a/EhPanda/View/Detail/GalleryInfosView.swift +++ b/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift @@ -9,12 +9,12 @@ import SwiftUI import ComposableArchitecture struct GalleryInfosView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let gallery: Gallery private let galleryDetail: GalleryDetail - init(store: Store, gallery: Gallery, galleryDetail: GalleryDetail) { + init(store: StoreOf, gallery: Gallery, galleryDetail: GalleryDetail) { self.store = store viewStore = ViewStore(store) self.gallery = gallery @@ -119,7 +119,7 @@ struct GalleryInfosView: View { .progressHUD( config: viewStore.hudConfig, unwrapping: viewStore.binding(\.$route), - case: /GalleryInfosState.Route.hud + case: /GalleryInfosReducer.Route.hud ) .navigationTitle(L10n.Localizable.GalleryInfosView.Title.galleryInfos) } @@ -137,11 +137,7 @@ struct GalleryInfosView_Previews: PreviewProvider { GalleryInfosView( store: .init( initialState: .init(), - reducer: galleryInfosReducer, - environment: GalleryInfosEnvironment( - hapticsClient: .live, - clipboardClient: .live - ) + reducer: GalleryInfosReducer() ), gallery: .preview, galleryDetail: .preview diff --git a/EhPanda/View/Detail/Previews/PreviewsReducer.swift b/EhPanda/View/Detail/Previews/PreviewsReducer.swift new file mode 100644 index 00000000..90ca28db --- /dev/null +++ b/EhPanda/View/Detail/Previews/PreviewsReducer.swift @@ -0,0 +1,143 @@ +// +// PreviewsReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/16. +// + +import Foundation +import ComposableArchitecture + +struct PreviewsReducer: ReducerProtocol { + enum Route { + case reading + } + + struct CancelID: Hashable { + let id = String(describing: PreviewsReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + + var gallery: Gallery = .empty + var loadingState: LoadingState = .idle + var databaseLoadingState: LoadingState = .loading + + var previewURLs = [Int: URL]() + var previewConfig: PreviewConfig = .normal(rows: 4) + + var readingState = ReadingReducer.State() + + mutating func updatePreviewURLs(_ previewURLs: [Int: URL]) { + self.previewURLs = self.previewURLs.merging( + previewURLs, uniquingKeysWith: { stored, _ in stored } + ) + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + + case syncPreviewURLs([Int: URL]) + case updateReadingProgress(Int) + + case teardown + case fetchDatabaseInfos(String) + case fetchDatabaseInfosDone(GalleryState) + case fetchPreviewURLs(Int) + case fetchPreviewURLsDone(Result<[Int: URL], AppError>) + + case reading(ReadingReducer.Action) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .clearSubStates: + state.readingState = .init() + return .init(value: .reading(.teardown)) + + case .syncPreviewURLs(let previewURLs): + return databaseClient + .updatePreviewURLs(gid: state.gallery.id, previewURLs: previewURLs).fireAndForget() + + case .updateReadingProgress(let progress): + return databaseClient + .updateReadingProgress(gid: state.gallery.id, progress: progress).fireAndForget() + + case .teardown: + return .cancel(id: CancelID()) + + case .fetchDatabaseInfos(let gid): + guard let gallery = databaseClient.fetchGallery(gid: gid) else { return .none } + state.gallery = gallery + return databaseClient.fetchGalleryState(gid: state.gallery.id) + .map(Action.fetchDatabaseInfosDone).cancellable(id: CancelID()) + + case .fetchDatabaseInfosDone(let galleryState): + if let previewConfig = galleryState.previewConfig { + state.previewConfig = previewConfig + } + state.previewURLs = galleryState.previewURLs + state.databaseLoadingState = .idle + return .none + + case .fetchPreviewURLs(let index): + guard state.loadingState != .loading, + let galleryURL = state.gallery.galleryURL + else { return .none } + state.loadingState = .loading + let pageNum = state.previewConfig.pageNumber(index: index) + return GalleryPreviewURLsRequest(galleryURL: galleryURL, pageNum: pageNum) + .effect.map(Action.fetchPreviewURLsDone).cancellable(id: CancelID()) + + case .fetchPreviewURLsDone(let result): + state.loadingState = .idle + + switch result { + case .success(let previewURLs): + guard !previewURLs.isEmpty else { + state.loadingState = .failed(.notFound) + return .none + } + state.updatePreviewURLs(previewURLs) + return .init(value: .syncPreviewURLs(previewURLs)) + case .failure(let error): + state.loadingState = .failed(error) + } + return .none + + case .reading(.onPerformDismiss): + return .init(value: .setNavigation(nil)) + + case .reading: + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.reading, + hapticsClient: hapticsClient + ) + + Scope(state: \.readingState, action: /Action.reading, child: ReadingReducer.init) + + BindingReducer() + } +} diff --git a/EhPanda/View/Detail/PreviewsView.swift b/EhPanda/View/Detail/Previews/PreviewsView.swift similarity index 81% rename from EhPanda/View/Detail/PreviewsView.swift rename to EhPanda/View/Detail/Previews/PreviewsView.swift index b1dfd36f..1a255c68 100644 --- a/EhPanda/View/Detail/PreviewsView.swift +++ b/EhPanda/View/Detail/Previews/PreviewsView.swift @@ -10,14 +10,14 @@ import Kingfisher import ComposableArchitecture struct PreviewsView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let gid: String @Binding private var setting: Setting private let blurRadius: Double init( - store: Store, + store: StoreOf, gid: String, setting: Binding, blurRadius: Double ) { self.store = store @@ -68,9 +68,9 @@ struct PreviewsView: View { .padding(.bottom) .id(viewStore.databaseLoadingState) } - .fullScreenCover(unwrapping: viewStore.binding(\.$route), case: /PreviewsState.Route.reading) { _ in + .fullScreenCover(unwrapping: viewStore.binding(\.$route), case: /PreviewsReducer.Route.reading) { _ in ReadingView( - store: store.scope(state: \.readingState, action: PreviewsAction.reading), + store: store.scope(state: \.readingState, action: PreviewsReducer.Action.reading), gid: gid, setting: $setting, blurRadius: blurRadius ) .accentColor(setting.accentColor) @@ -89,17 +89,7 @@ struct PreviewsView_Previews: PreviewProvider { PreviewsView( store: .init( initialState: .init(gallery: .preview), - reducer: previewsReducer, - environment: PreviewsEnvironment( - urlClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live - ) + reducer: PreviewsReducer() ), gid: .init(), setting: .constant(.init()), diff --git a/EhPanda/View/Detail/Torrents/TorrentsReducer.swift b/EhPanda/View/Detail/Torrents/TorrentsReducer.swift new file mode 100644 index 00000000..b05a2faf --- /dev/null +++ b/EhPanda/View/Detail/Torrents/TorrentsReducer.swift @@ -0,0 +1,112 @@ +// +// TorrentsReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/19. +// + +import Foundation +import TTProgressHUD +import ComposableArchitecture + +struct TorrentsReducer: ReducerProtocol { + enum Route: Equatable { + case hud + case share(URL) + } + + struct CancelID: Hashable { + let id = String(describing: TorrentsReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + var torrents = [GalleryTorrent]() + var loadingState: LoadingState = .idle + var hudConfig: TTProgressHUDConfig = .copiedToClipboardSucceeded + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case setNavigation(Route) + + case copyText(String) + case presentTorrentActivity(String, Data) + + case teardown + case fetchTorrent(String, URL) + case fetchTorrentDone(String, Result) + case fetchGalleryTorrents(String, String) + case fetchGalleryTorrentsDone(Result<[GalleryTorrent], AppError>) + } + + @Dependency(\.clipboardClient) private var clipboardClient + @Dependency(\.hapticsClient) private var hapticsClient + @Dependency(\.fileClient) private var fileClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return .none + + case .copyText(let magnetURL): + state.route = .hud + return .merge( + clipboardClient.saveText(magnetURL).fireAndForget(), + hapticsClient.generateNotificationFeedback(.success).fireAndForget() + ) + + case .presentTorrentActivity(let hash, let data): + if let url = fileClient.saveTorrent(hash: hash, data: data) { + return .init(value: .setNavigation(.share(url))) + } + return .none + + case .fetchTorrent(let hash, let torrentURL): + return DataRequest(url: torrentURL).effect.map({ Action.fetchTorrentDone(hash, $0) }) + .cancellable(id: CancelID()) + + case .teardown: + return .cancel(id: CancelID()) + + case .fetchTorrentDone(let hash, let result): + if case .success(let data) = result, !data.isEmpty { + return .init(value: .presentTorrentActivity(hash, data)) + } + return .none + + case .fetchGalleryTorrents(let gid, let token): + guard state.loadingState != .loading else { return .none } + state.loadingState = .loading + return GalleryTorrentsRequest(gid: gid, token: token) + .effect.map(Action.fetchGalleryTorrentsDone).cancellable(id: CancelID()) + + case .fetchGalleryTorrentsDone(let result): + state.loadingState = .idle + switch result { + case .success(let torrents): + guard !torrents.isEmpty else { + state.loadingState = .failed(.notFound) + return .none + } + state.torrents = torrents + case .failure(let error): + state.loadingState = .failed(error) + } + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.share, + hapticsClient: hapticsClient + ) + + BindingReducer() + } +} diff --git a/EhPanda/View/Detail/TorrentsView.swift b/EhPanda/View/Detail/Torrents/TorrentsView.swift similarity index 87% rename from EhPanda/View/Detail/TorrentsView.swift rename to EhPanda/View/Detail/Torrents/TorrentsView.swift index 10b11c07..5a8e1dc3 100644 --- a/EhPanda/View/Detail/TorrentsView.swift +++ b/EhPanda/View/Detail/Torrents/TorrentsView.swift @@ -9,13 +9,13 @@ import SwiftUI import ComposableArchitecture struct TorrentsView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let gid: String private let token: String private let blurRadius: Double - init(store: Store, gid: String, token: String, blurRadius: Double) { + init(store: StoreOf, gid: String, token: String, blurRadius: Double) { self.store = store viewStore = ViewStore(store) self.gid = gid @@ -45,14 +45,14 @@ struct TorrentsView: View { } .opacity(error != nil && viewStore.torrents.isEmpty ? 1 : 0) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /TorrentsState.Route.share) { route in + .sheet(unwrapping: viewStore.binding(\.$route), case: /TorrentsReducer.Route.share) { route in ActivityView(activityItems: [route.wrappedValue]) .autoBlur(radius: blurRadius) } .progressHUD( config: viewStore.hudConfig, unwrapping: viewStore.binding(\.$route), - case: /TorrentsState.Route.hud + case: /TorrentsReducer.Route.hud ) .animation(.default, value: viewStore.torrents) .onAppear { @@ -120,12 +120,7 @@ struct TorrentsView_Previews: PreviewProvider { TorrentsView( store: .init( initialState: .init(), - reducer: torrentsReducer, - environment: TorrentsEnvironment( - fileClient: .live, - hapticsClient: .live, - clipboardClient: .live - ) + reducer: TorrentsReducer() ), gid: .init(), token: .init(), diff --git a/EhPanda/View/Favorites/FavoritesStore.swift b/EhPanda/View/Favorites/FavoritesStore.swift index 585d9630..1c12f256 100644 --- a/EhPanda/View/Favorites/FavoritesStore.swift +++ b/EhPanda/View/Favorites/FavoritesStore.swift @@ -44,7 +44,7 @@ struct FavoritesState: Equatable { rawFooterLoadingState[index] } - @Heap var detailState: DetailState! + @Heap var detailState: DetailReducer.State! var quickSearchState = QuickSearchReducer.State() mutating func insertGalleries(index: Int, galleries: [Gallery]) { @@ -69,7 +69,7 @@ enum FavoritesAction: BindableAction { case fetchMoreGalleries case fetchMoreGalleriesDone(Int, Result<(PageNumber, FavoritesSortOrder?, [Gallery]), AppError>) - case detail(DetailAction) + case detail(DetailReducer.Action) case quickSearch(QuickSearchReducer.Action) } @@ -199,25 +199,26 @@ let favoritesReducer = Reducer) case filters(FiltersReducer.Action) - case detail(DetailAction) + case detail(DetailReducer.Action) } struct FrontpageEnvironment { @@ -165,7 +165,8 @@ let frontpageReducer = Reducer.co return .none } } - .binding(), - detailReducer.pullback( - state: \.detailState, - action: /HistoryAction.detail, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ) + .binding() +// , +// detailReducer.pullback( +// state: \.detailState, +// action: /HistoryAction.detail, +// environment: { +// .init( +// urlClient: $0.urlClient, +// fileClient: $0.fileClient, +// imageClient: $0.imageClient, +// deviceClient: $0.deviceClient, +// hapticsClient: $0.hapticsClient, +// cookieClient: $0.cookieClient, +// databaseClient: $0.databaseClient, +// clipboardClient: $0.clipboardClient, +// appDelegateClient: $0.appDelegateClient, +// uiApplicationClient: $0.uiApplicationClient +// ) +// } +// ) ) diff --git a/EhPanda/View/Home/DataFlow/HomeStore.swift b/EhPanda/View/Home/DataFlow/HomeStore.swift index 7ba2968d..bdd0631f 100644 --- a/EhPanda/View/Home/DataFlow/HomeStore.swift +++ b/EhPanda/View/Home/DataFlow/HomeStore.swift @@ -42,7 +42,7 @@ struct HomeState: Equatable { var popularState = PopularState() var watchedState = WatchedState() var historyState = HistoryState() - @Heap var detailState: DetailState! + @Heap var detailState: DetailReducer.State! mutating func setPopularGalleries(_ galleries: [Gallery]) { let sortedGalleries = galleries.sorted { lhs, rhs in @@ -85,7 +85,7 @@ enum HomeAction: BindableAction { case popular(PopularAction) case watched(WatchedAction) case history(HistoryAction) - case detail(DetailAction) + case detail(DetailReducer.Action) } struct HomeEnvironment { @@ -345,23 +345,24 @@ let homeReducer = Reducer.combine( uiApplicationClient: $0.uiApplicationClient ) } - ), - detailReducer.pullback( - state: \.detailState, - action: /HomeAction.detail, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } ) +// , +// detailReducer.pullback( +// state: \.detailState, +// action: /HomeAction.detail, +// environment: { +// .init( +// urlClient: $0.urlClient, +// fileClient: $0.fileClient, +// imageClient: $0.imageClient, +// deviceClient: $0.deviceClient, +// hapticsClient: $0.hapticsClient, +// cookieClient: $0.cookieClient, +// databaseClient: $0.databaseClient, +// clipboardClient: $0.clipboardClient, +// appDelegateClient: $0.appDelegateClient, +// uiApplicationClient: $0.uiApplicationClient +// ) +// } +// ) ) diff --git a/EhPanda/View/Home/DataFlow/PopularStore.swift b/EhPanda/View/Home/DataFlow/PopularStore.swift index 5f0c5dad..f54be131 100644 --- a/EhPanda/View/Home/DataFlow/PopularStore.swift +++ b/EhPanda/View/Home/DataFlow/PopularStore.swift @@ -31,7 +31,7 @@ struct PopularState: Equatable { var loadingState: LoadingState = .idle var filtersState = FiltersReducer.State() - @Heap var detailState: DetailState! + @Heap var detailState: DetailReducer.State! } enum PopularAction: BindableAction { @@ -44,7 +44,7 @@ enum PopularAction: BindableAction { case fetchGalleriesDone(Result<[Gallery], AppError>) case filters(FiltersReducer.Action) - case detail(DetailAction) + case detail(DetailReducer.Action) } struct PopularEnvironment { @@ -115,7 +115,8 @@ let popularReducer = Reducer.co case: /PopularState.Route.filters, hapticsClient: \.hapticsClient ) - .binding(), + .binding() +// , // filtersReducer.pullback( // state: \.filtersState, // action: /PopularAction.filters, @@ -125,22 +126,22 @@ let popularReducer = Reducer.co // ) // } // ), - detailReducer.pullback( - state: \.detailState, - action: /PopularAction.detail, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ) +// detailReducer.pullback( +// state: \.detailState, +// action: /PopularAction.detail, +// environment: { +// .init( +// urlClient: $0.urlClient, +// fileClient: $0.fileClient, +// imageClient: $0.imageClient, +// deviceClient: $0.deviceClient, +// hapticsClient: $0.hapticsClient, +// cookieClient: $0.cookieClient, +// databaseClient: $0.databaseClient, +// clipboardClient: $0.clipboardClient, +// appDelegateClient: $0.appDelegateClient, +// uiApplicationClient: $0.uiApplicationClient +// ) +// } +// ) ) diff --git a/EhPanda/View/Home/DataFlow/ToplistsStore.swift b/EhPanda/View/Home/DataFlow/ToplistsStore.swift index 1afafe03..4e516539 100644 --- a/EhPanda/View/Home/DataFlow/ToplistsStore.swift +++ b/EhPanda/View/Home/DataFlow/ToplistsStore.swift @@ -50,7 +50,7 @@ struct ToplistsState: Equatable { rawFooterLoadingState[type] } - @Heap var detailState: DetailState! + @Heap var detailState: DetailReducer.State! mutating func insertGalleries(type: ToplistsType, galleries: [Gallery]) { galleries.forEach { gallery in @@ -77,7 +77,7 @@ enum ToplistsAction: BindableAction { case fetchMoreGalleries case fetchMoreGalleriesDone(ToplistsType, Result<(PageNumber, [Gallery]), AppError>) - case detail(DetailAction) + case detail(DetailReducer.Action) } struct ToplistsEnvironment { @@ -207,23 +207,24 @@ let toplistsReducer = Reducer) case filters(FiltersReducer.Action) - case detail(DetailAction) + case detail(DetailReducer.Action) case quickSearch(QuickSearchReducer.Action) } @@ -182,7 +182,8 @@ let watchedReducer = Reducer.co case: /WatchedState.Route.filters, hapticsClient: \.hapticsClient ) - .binding(), + .binding() +// , // filtersReducer.pullback( // state: \.filtersState, // action: /WatchedAction.filters, @@ -201,22 +202,22 @@ let watchedReducer = Reducer.co // ) // } // ), - detailReducer.pullback( - state: \.detailState, - action: /WatchedAction.detail, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ) +// detailReducer.pullback( +// state: \.detailState, +// action: /WatchedAction.detail, +// environment: { +// .init( +// urlClient: $0.urlClient, +// fileClient: $0.fileClient, +// imageClient: $0.imageClient, +// deviceClient: $0.deviceClient, +// hapticsClient: $0.hapticsClient, +// cookieClient: $0.cookieClient, +// databaseClient: $0.databaseClient, +// clipboardClient: $0.clipboardClient, +// appDelegateClient: $0.appDelegateClient, +// uiApplicationClient: $0.uiApplicationClient +// ) +// } +// ) ) diff --git a/EhPanda/View/Reading/ReadingReducer.swift b/EhPanda/View/Reading/ReadingReducer.swift new file mode 100644 index 00000000..c30d96dd --- /dev/null +++ b/EhPanda/View/Reading/ReadingReducer.swift @@ -0,0 +1,593 @@ +// +// ReadingReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/22. +// + +import SwiftUI +import TTProgressHUD +import ComposableArchitecture + +struct ReadingReducer: ReducerProtocol { + enum Route: Equatable { + case hud + case share(ShareItem) + case readingSetting + } + + enum ShareItem: Equatable { + var associatedValue: Any { + switch self { + case .data(let data): + return data + case .image(let image): + return image + } + } + case data(Data) + case image(UIImage) + } + + enum ImageAction { + case copy(Bool) + case save(Bool) + case share(Bool) + } + + struct CancelID: Hashable { + let id = String(describing: ReadingReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + var gallery: Gallery = .empty + var galleryDetail: GalleryDetail? + + var readingProgress: Int = .zero + var forceRefreshID: UUID = .init() + var hudConfig: TTProgressHUDConfig = .loading + + var webImageLoadSuccessIndices = Set() + var imageURLLoadingStates = [Int: LoadingState]() + var previewLoadingStates = [Int: LoadingState]() + var databaseLoadingState: LoadingState = .loading + var previewConfig: PreviewConfig = .normal(rows: 4) + + var previewURLs = [Int: URL]() + + var thumbnailURLs = [Int: URL]() + var imageURLs = [Int: URL]() + var originalImageURLs = [Int: URL]() + + var mpvKey: String? + var mpvImageKeys = [Int: String]() + var mpvSkipServerIdentifiers = [Int: String]() + + @BindingState var showsPanel = false + @BindingState var showsSliderPreview = false + + // Update + func update(stored: inout [Int: T], new: [Int: T], replaceExisting: Bool = true) { + guard !new.isEmpty else { return } + stored = stored.merging(new, uniquingKeysWith: { stored, new in replaceExisting ? new : stored }) + } + mutating func updatePreviewURLs(_ previewURLs: [Int: URL]) { + update(stored: &self.previewURLs, new: previewURLs) + } + mutating func updateThumbnailURLs(_ thumbnailURLs: [Int: URL]) { + update(stored: &self.thumbnailURLs, new: thumbnailURLs) + } + mutating func updateImageURLs(_ imageURLs: [Int: URL], _ originalImageURLs: [Int: URL]) { + update(stored: &self.imageURLs, new: imageURLs) + update(stored: &self.originalImageURLs, new: originalImageURLs) + } + + // Image + func containerDataSource(setting: Setting, isLandscape: Bool = DeviceUtil.isLandscape) -> [Int] { + let defaultData = Array(1...gallery.pageCount) + guard isLandscape && setting.enablesDualPageMode + && setting.readingDirection != .vertical + else { return defaultData } + + let data = setting.exceptCover + ? [1] + Array(stride(from: 2, through: gallery.pageCount, by: 2)) + : Array(stride(from: 1, through: gallery.pageCount, by: 2)) + + return data + } + func imageContainerConfigs( + index: Int, setting: Setting, isLandscape: Bool = DeviceUtil.isLandscape + ) -> ImageStackConfig { + let direction = setting.readingDirection + let isReversed = direction == .rightToLeft + let isFirstSingle = setting.exceptCover + let isFirstPageAndSingle = index == 1 && isFirstSingle + let isDualPage = isLandscape && setting.enablesDualPageMode && direction != .vertical + let firstIndex = isDualPage && isReversed && !isFirstPageAndSingle ? index + 1 : index + let secondIndex = firstIndex + (isReversed ? -1 : 1) + let isValidFirstRange = firstIndex >= 1 && firstIndex <= gallery.pageCount + let isValidSecondRange = isFirstSingle + ? secondIndex >= 2 && secondIndex <= gallery.pageCount + : secondIndex >= 1 && secondIndex <= gallery.pageCount + return .init( + firstIndex: firstIndex, secondIndex: secondIndex, isFirstAvailable: isValidFirstRange, + isSecondAvailable: !isFirstPageAndSingle && isValidSecondRange && isDualPage + ) + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + + case toggleShowsPanel + case setOrientationPortrait(Bool) + case onPerformDismiss + case onAppear(String, Bool) + + case onWebImageRetry(Int) + case onWebImageSucceeded(Int) + case onWebImageFailed(Int) + case reloadAllWebImages + case retryAllFailedWebImages + + case copyImage(URL) + case saveImage(URL) + case saveImageDone(Bool) + case shareImage(URL) + case fetchImage(ImageAction, URL) + case fetchImageDone(ImageAction, Result) + + case syncReadingProgress(Int) + case syncPreviewURLs([Int: URL]) + case syncThumbnailURLs([Int: URL]) + case syncImageURLs([Int: URL], [Int: URL]) + + case teardown + case fetchDatabaseInfos(String) + case fetchDatabaseInfosDone(GalleryState) + + case fetchPreviewURLs(Int) + case fetchPreviewURLsDone(Int, Result<[Int: URL], AppError>) + + case fetchImageURLs(Int) + case refetchImageURLs(Int) + case prefetchImages(Int, Int) + + case fetchThumbnailURLs(Int) + case fetchThumbnailURLsDone(Int, Result<[Int: URL], AppError>) + case fetchNormalImageURLs(Int, [Int: URL]) + case fetchNormalImageURLsDone(Int, Result<([Int: URL], [Int: URL]), AppError>) + case refetchNormalImageURLs(Int) + case refetchNormalImageURLsDone(Int, Result<([Int: URL], HTTPURLResponse?), AppError>) + + case fetchMPVKeys(Int, URL) + case fetchMPVKeysDone(Int, Result<(String, [Int: String]), AppError>) + case fetchMPVImageURL(Int, Bool) + case fetchMPVImageURLDone(Int, Result<(URL, URL?, String), AppError>) + } + + @Dependency(\.appDelegateClient) private var appDelegateClient + @Dependency(\.clipboardClient) private var clipboardClient + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + @Dependency(\.cookieClient) private var cookieClient + @Dependency(\.deviceClient) private var deviceClient + @Dependency(\.imageClient) private var imageClient + @Dependency(\.urlClient) private var urlClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$showsSliderPreview): + return hapticsClient.generateFeedback(.soft).fireAndForget() + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return .none + + case .toggleShowsPanel: + state.showsPanel.toggle() + return .none + + case .setOrientationPortrait(let isPortrait): + var effects = [EffectTask]() + if isPortrait { + effects.append(appDelegateClient.setPortraitOrientationMask().fireAndForget()) + effects.append(appDelegateClient.setPortraitOrientation().fireAndForget()) + } else { + effects.append(appDelegateClient.setAllOrientationMask().fireAndForget()) + } + return .merge(effects) + + case .onPerformDismiss: + return hapticsClient.generateFeedback(.light).fireAndForget() + + case .onAppear(let gid, let enablesLandscape): + var effects: [EffectTask] = [ + .init(value: .fetchDatabaseInfos(gid)) + ] + if enablesLandscape { + effects.append(.init(value: .setOrientationPortrait(false))) + } + return .merge(effects) + + case .onWebImageRetry(let index): + state.imageURLLoadingStates[index] = .idle + return .none + + case .onWebImageSucceeded(let index): + state.imageURLLoadingStates[index] = .idle + state.webImageLoadSuccessIndices.insert(index) + return .none + + case .onWebImageFailed(let index): + state.imageURLLoadingStates[index] = .failed(.webImageFailed) + return .none + + case .reloadAllWebImages: + state.previewURLs = .init() + state.thumbnailURLs = .init() + state.imageURLs = .init() + state.originalImageURLs = .init() + state.mpvKey = nil + state.mpvImageKeys = .init() + state.mpvSkipServerIdentifiers = .init() + state.forceRefreshID = .init() + return databaseClient.removeImageURLs(gid: state.gallery.id).fireAndForget() + + case .retryAllFailedWebImages: + state.imageURLLoadingStates.forEach { (index, loadingState) in + if case .failed = loadingState { + state.imageURLLoadingStates[index] = .idle + } + } + state.previewLoadingStates.forEach { (index, loadingState) in + if case .failed = loadingState { + state.previewLoadingStates[index] = .idle + } + } + return .none + + case .copyImage(let imageURL): + return .init(value: .fetchImage(.copy(imageURL.isGIF), imageURL)) + + case .saveImage(let imageURL): + return .init(value: .fetchImage(.save(imageURL.isGIF), imageURL)) + + case .saveImageDone(let isSucceeded): + state.hudConfig = isSucceeded ? .savedToPhotoLibrary : .error + return .init(value: .setNavigation(.hud)) + + case .shareImage(let imageURL): + return .init(value: .fetchImage(.share(imageURL.isGIF), imageURL)) + + case .fetchImage(let action, let imageURL): + return imageClient.fetchImage(url: imageURL) + .map({ Action.fetchImageDone(action, $0) }) + .cancellable(id: CancelID()) + + case .fetchImageDone(let action, let result): + if case .success(let image) = result { + switch action { + case .copy(let isAnimated): + state.hudConfig = .copiedToClipboardSucceeded + return .merge( + .init(value: .setNavigation(.hud)), + clipboardClient.saveImage(image, isAnimated).fireAndForget() + ) + case .save(let isAnimated): + return imageClient + .saveImageToPhotoLibrary(image, isAnimated).map(Action.saveImageDone) + case .share(let isAnimated): + if isAnimated, let data = image.kf.data(format: .GIF) { + return .init(value: .setNavigation(.share(.data(data)))) + } else { + return .init(value: .setNavigation(.share(.image(image)))) + } + } + } else { + state.hudConfig = .error + return .init(value: .setNavigation(.hud)) + } + + case .syncReadingProgress(let progress): + return databaseClient + .updateReadingProgress(gid: state.gallery.id, progress: progress).fireAndForget() + + case .syncPreviewURLs(let previewURLs): + return databaseClient + .updatePreviewURLs(gid: state.gallery.id, previewURLs: previewURLs).fireAndForget() + + case .syncThumbnailURLs(let thumbnailURLs): + return databaseClient + .updateThumbnailURLs(gid: state.gallery.id, thumbnailURLs: thumbnailURLs).fireAndForget() + + case .syncImageURLs(let imageURLs, let originalImageURLs): + return databaseClient + .updateImageURLs(gid: state.gallery.id, imageURLs: imageURLs, originalImageURLs: originalImageURLs) + .fireAndForget() + + case .teardown: + var effects: [EffectTask] = [ + .cancel(id: CancelID()) + ] + if !deviceClient.isPad() { + effects.append(.init(value: .setOrientationPortrait(true))) + } + return .merge(effects) + + case .fetchDatabaseInfos(let gid): + guard let gallery = databaseClient.fetchGallery(gid: gid) else { return .none } + state.gallery = gallery + state.galleryDetail = databaseClient.fetchGalleryDetail(gid: state.gallery.id) + return databaseClient.fetchGalleryState(gid: state.gallery.id) + .map(Action.fetchDatabaseInfosDone).cancellable(id: CancelID()) + + case .fetchDatabaseInfosDone(let galleryState): + if let previewConfig = galleryState.previewConfig { + state.previewConfig = previewConfig + } + state.previewURLs = galleryState.previewURLs + state.imageURLs = galleryState.imageURLs + state.thumbnailURLs = galleryState.thumbnailURLs + state.originalImageURLs = galleryState.originalImageURLs + state.readingProgress = galleryState.readingProgress + state.databaseLoadingState = .idle + return .none + + case .fetchPreviewURLs(let index): + guard state.previewLoadingStates[index] != .loading, + let galleryURL = state.gallery.galleryURL + else { return .none } + state.previewLoadingStates[index] = .loading + let pageNum = state.previewConfig.pageNumber(index: index) + return GalleryPreviewURLsRequest(galleryURL: galleryURL, pageNum: pageNum) + .effect.map({ Action.fetchPreviewURLsDone(index, $0) }).cancellable(id: CancelID()) + + case .fetchPreviewURLsDone(let index, let result): + switch result { + case .success(let previewURLs): + guard !previewURLs.isEmpty else { + state.previewLoadingStates[index] = .failed(.notFound) + return .none + } + state.previewLoadingStates[index] = .idle + state.updatePreviewURLs(previewURLs) + return .init(value: .syncPreviewURLs(previewURLs)) + case .failure(let error): + state.previewLoadingStates[index] = .failed(error) + } + return .none + + case .fetchImageURLs(let index): + if state.mpvKey != nil { + return .init(value: .fetchMPVImageURL(index, false)) + } else { + return .init(value: .fetchThumbnailURLs(index)) + } + + case .refetchImageURLs(let index): + if state.mpvKey != nil { + return .init(value: .fetchMPVImageURL(index, true)) + } else { + return .init(value: .refetchNormalImageURLs(index)) + } + + case .prefetchImages(let index, let prefetchLimit): + func getPrefetchImageURLs(range: ClosedRange) -> [URL] { + (range.lowerBound...range.upperBound).compactMap { index in + if let url = state.imageURLs[index] { + return url + } + return nil + } + } + func getFetchImageURLIndices(range: ClosedRange) -> [Int] { + (range.lowerBound...range.upperBound).compactMap { index in + if state.imageURLs[index] == nil, state.imageURLLoadingStates[index] != .loading { + return index + } + return nil + } + } + var prefetchImageURLs = [URL]() + var fetchImageURLIndices = [Int]() + var effects = [EffectTask]() + let previousUpperBound = max(index - 2, 1) + let previousLowerBound = max(previousUpperBound - prefetchLimit / 2, 1) + if previousUpperBound - previousLowerBound > 0 { + prefetchImageURLs += getPrefetchImageURLs(range: previousLowerBound...previousUpperBound) + fetchImageURLIndices += getFetchImageURLIndices(range: previousLowerBound...previousUpperBound) + } + let nextLowerBound = min(index + 2, state.gallery.pageCount) + let nextUpperBound = min(nextLowerBound + prefetchLimit / 2, state.gallery.pageCount) + if nextUpperBound - nextLowerBound > 0 { + prefetchImageURLs += getPrefetchImageURLs(range: nextLowerBound...nextUpperBound) + fetchImageURLIndices += getFetchImageURLIndices(range: nextLowerBound...nextUpperBound) + } + fetchImageURLIndices.forEach { + effects.append(.init(value: .fetchImageURLs($0))) + } + effects.append(imageClient.prefetchImages(prefetchImageURLs).fireAndForget()) + return .merge(effects) + + case .fetchThumbnailURLs(let index): + guard state.imageURLLoadingStates[index] != .loading, + let galleryURL = state.gallery.galleryURL + else { return .none } + state.previewConfig.batchRange(index: index).forEach { + state.imageURLLoadingStates[$0] = .loading + } + let pageNum = state.previewConfig.pageNumber(index: index) + return ThumbnailURLsRequest(galleryURL: galleryURL, pageNum: pageNum) + .effect.map({ Action.fetchThumbnailURLsDone(index, $0) }).cancellable(id: CancelID()) + + case .fetchThumbnailURLsDone(let index, let result): + let batchRange = state.previewConfig.batchRange(index: index) + switch result { + case .success(let thumbnailURLs): + guard !thumbnailURLs.isEmpty else { + batchRange.forEach { + state.imageURLLoadingStates[$0] = .failed(.notFound) + } + return .none + } + if let url = thumbnailURLs[index], urlClient.checkIfMPVURL(url) { + return .init(value: .fetchMPVKeys(index, url)) + } else { + state.updateThumbnailURLs(thumbnailURLs) + return .merge( + .init(value: .syncThumbnailURLs(thumbnailURLs)), + .init(value: .fetchNormalImageURLs(index, thumbnailURLs)) + ) + } + case .failure(let error): + batchRange.forEach { + state.imageURLLoadingStates[$0] = .failed(error) + } + } + return .none + + case .fetchNormalImageURLs(let index, let thumbnailURLs): + return GalleryNormalImageURLsRequest(thumbnailURLs: thumbnailURLs) + .effect.map({ Action.fetchNormalImageURLsDone(index, $0) }).cancellable(id: CancelID()) + + case .fetchNormalImageURLsDone(let index, let result): + let batchRange = state.previewConfig.batchRange(index: index) + switch result { + case .success(let (imageURLs, originalImageURLs)): + guard !imageURLs.isEmpty else { + batchRange.forEach { + state.imageURLLoadingStates[$0] = .failed(.notFound) + } + return .none + } + batchRange.forEach { + state.imageURLLoadingStates[$0] = .idle + } + state.updateImageURLs(imageURLs, originalImageURLs) + return .init(value: .syncImageURLs(imageURLs, originalImageURLs)) + case .failure(let error): + batchRange.forEach { + state.imageURLLoadingStates[$0] = .failed(error) + } + } + return .none + + case .refetchNormalImageURLs(let index): + guard state.imageURLLoadingStates[index] != .loading, + let galleryURL = state.gallery.galleryURL, + let imageURL = state.imageURLs[index] + else { return .none } + state.imageURLLoadingStates[index] = .loading + let pageNum = state.previewConfig.pageNumber(index: index) + return GalleryNormalImageURLRefetchRequest( + index: index, pageNum: pageNum, + galleryURL: galleryURL, + thumbnailURL: state.thumbnailURLs[index], + storedImageURL: imageURL + ) + .effect.map({ Action.refetchNormalImageURLsDone(index, $0) }).cancellable(id: CancelID()) + + case .refetchNormalImageURLsDone(let index, let result): + switch result { + case .success(let (imageURLs, response)): + var effects = [EffectTask]() + if let response = response { + effects.append(cookieClient.setSkipServer(response: response).fireAndForget()) + } + guard !imageURLs.isEmpty else { + state.imageURLLoadingStates[index] = .failed(.notFound) + return effects.isEmpty ? .none : .merge(effects) + } + state.imageURLLoadingStates[index] = .idle + state.updateImageURLs(imageURLs, [:]) + effects.append(.init(value: .syncImageURLs(imageURLs, [:]))) + return .merge(effects) + case .failure(let error): + state.imageURLLoadingStates[index] = .failed(error) + } + return .none + + case .fetchMPVKeys(let index, let mpvURL): + return MPVKeysRequest(mpvURL: mpvURL) + .effect.map({ Action.fetchMPVKeysDone(index, $0) }).cancellable(id: CancelID()) + + case .fetchMPVKeysDone(let index, let result): + let batchRange = state.previewConfig.batchRange(index: index) + switch result { + case .success(let (mpvKey, mpvImageKeys)): + let pageCount = state.gallery.pageCount + guard mpvImageKeys.count == pageCount else { + batchRange.forEach { + state.imageURLLoadingStates[$0] = .failed(.notFound) + } + return .none + } + batchRange.forEach { + state.imageURLLoadingStates[$0] = .idle + } + state.mpvKey = mpvKey + state.mpvImageKeys = mpvImageKeys + return .merge( + Array(1...min(3, max(1, pageCount))).map { + .init(value: .fetchMPVImageURL($0, false)) + } + ) + case .failure(let error): + batchRange.forEach { + state.imageURLLoadingStates[$0] = .failed(error) + } + } + return .none + + case .fetchMPVImageURL(let index, let isRefresh): + guard let gidInteger = Int(state.gallery.id), let mpvKey = state.mpvKey, + let mpvImageKey = state.mpvImageKeys[index], + state.imageURLLoadingStates[index] != .loading + else { return .none } + state.imageURLLoadingStates[index] = .loading + let skipServerIdentifier = isRefresh ? state.mpvSkipServerIdentifiers[index] : nil + return GalleryMPVImageURLRequest( + gid: gidInteger, index: index, mpvKey: mpvKey, + mpvImageKey: mpvImageKey, skipServerIdentifier: skipServerIdentifier + ) + .effect.map({ Action.fetchMPVImageURLDone(index, $0) }).cancellable(id: CancelID()) + + case .fetchMPVImageURLDone(let index, let result): + switch result { + case .success(let (imageURL, originalImageURL, skipServerIdentifier)): + let imageURLs: [Int: URL] = [index: imageURL] + var originalImageURLs = [Int: URL]() + if let originalImageURL = originalImageURL { + originalImageURLs[index] = originalImageURL + } + state.imageURLLoadingStates[index] = .idle + state.mpvSkipServerIdentifiers[index] = skipServerIdentifier + state.updateImageURLs(imageURLs, originalImageURLs) + return .init(value: .syncImageURLs(imageURLs, originalImageURLs)) + case .failure(let error): + state.imageURLLoadingStates[index] = .failed(error) + } + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.readingSetting, + hapticsClient: hapticsClient + ) + .haptics( + unwrapping: \.route, + case: /Route.share, + hapticsClient: hapticsClient + ) + + BindingReducer() + } +} diff --git a/EhPanda/View/Reading/ReadingStore.swift b/EhPanda/View/Reading/ReadingStore.swift deleted file mode 100644 index 8c4f4351..00000000 --- a/EhPanda/View/Reading/ReadingStore.swift +++ /dev/null @@ -1,587 +0,0 @@ -// -// ReadingStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/22. -// - -import SwiftUI -import TTProgressHUD -import ComposableArchitecture - -struct ReadingState: Equatable { - enum Route: Equatable { - case hud - case share(ShareItem) - case readingSetting - } - enum ShareItem: Equatable { - var associatedValue: Any { - switch self { - case .data(let data): - return data - case .image(let image): - return image - } - } - case data(Data) - case image(UIImage) - } - enum ImageAction { - case copy(Bool) - case save(Bool) - case share(Bool) - } - struct CancelID: Hashable { - let id = String(describing: ReadingState.CancelID.self) - } - - @BindingState var route: Route? - var gallery: Gallery = .empty - var galleryDetail: GalleryDetail? - - var readingProgress: Int = .zero - var forceRefreshID: UUID = .init() - var hudConfig: TTProgressHUDConfig = .loading - - var webImageLoadSuccessIndices = Set() - var imageURLLoadingStates = [Int: LoadingState]() - var previewLoadingStates = [Int: LoadingState]() - var databaseLoadingState: LoadingState = .loading - var previewConfig: PreviewConfig = .normal(rows: 4) - - var previewURLs = [Int: URL]() - - var thumbnailURLs = [Int: URL]() - var imageURLs = [Int: URL]() - var originalImageURLs = [Int: URL]() - - var mpvKey: String? - var mpvImageKeys = [Int: String]() - var mpvSkipServerIdentifiers = [Int: String]() - - @BindingState var showsPanel = false - @BindingState var showsSliderPreview = false - - // Update - func update(stored: inout [Int: T], new: [Int: T], replaceExisting: Bool = true) { - guard !new.isEmpty else { return } - stored = stored.merging(new, uniquingKeysWith: { stored, new in replaceExisting ? new : stored }) - } - mutating func updatePreviewURLs(_ previewURLs: [Int: URL]) { - update(stored: &self.previewURLs, new: previewURLs) - } - mutating func updateThumbnailURLs(_ thumbnailURLs: [Int: URL]) { - update(stored: &self.thumbnailURLs, new: thumbnailURLs) - } - mutating func updateImageURLs(_ imageURLs: [Int: URL], _ originalImageURLs: [Int: URL]) { - update(stored: &self.imageURLs, new: imageURLs) - update(stored: &self.originalImageURLs, new: originalImageURLs) - } - - // Image - func containerDataSource(setting: Setting, isLandscape: Bool = DeviceUtil.isLandscape) -> [Int] { - let defaultData = Array(1...gallery.pageCount) - guard isLandscape && setting.enablesDualPageMode - && setting.readingDirection != .vertical - else { return defaultData } - - let data = setting.exceptCover - ? [1] + Array(stride(from: 2, through: gallery.pageCount, by: 2)) - : Array(stride(from: 1, through: gallery.pageCount, by: 2)) - - return data - } - func imageContainerConfigs( - index: Int, setting: Setting, isLandscape: Bool = DeviceUtil.isLandscape - ) -> ImageStackConfig { - let direction = setting.readingDirection - let isReversed = direction == .rightToLeft - let isFirstSingle = setting.exceptCover - let isFirstPageAndSingle = index == 1 && isFirstSingle - let isDualPage = isLandscape && setting.enablesDualPageMode && direction != .vertical - let firstIndex = isDualPage && isReversed && !isFirstPageAndSingle ? index + 1 : index - let secondIndex = firstIndex + (isReversed ? -1 : 1) - let isValidFirstRange = firstIndex >= 1 && firstIndex <= gallery.pageCount - let isValidSecondRange = isFirstSingle - ? secondIndex >= 2 && secondIndex <= gallery.pageCount - : secondIndex >= 1 && secondIndex <= gallery.pageCount - return .init( - firstIndex: firstIndex, secondIndex: secondIndex, isFirstAvailable: isValidFirstRange, - isSecondAvailable: !isFirstPageAndSingle && isValidSecondRange && isDualPage - ) - } -} - -enum ReadingAction: BindableAction { - case binding(BindingAction) - case setNavigation(ReadingState.Route?) - - case toggleShowsPanel - case setOrientationPortrait(Bool) - case onPerformDismiss - case onAppear(String, Bool) - - case onWebImageRetry(Int) - case onWebImageSucceeded(Int) - case onWebImageFailed(Int) - case reloadAllWebImages - case retryAllFailedWebImages - - case copyImage(URL) - case saveImage(URL) - case saveImageDone(Bool) - case shareImage(URL) - case fetchImage(ReadingState.ImageAction, URL) - case fetchImageDone(ReadingState.ImageAction, Result) - - case syncReadingProgress(Int) - case syncPreviewURLs([Int: URL]) - case syncThumbnailURLs([Int: URL]) - case syncImageURLs([Int: URL], [Int: URL]) - - case teardown - case fetchDatabaseInfos(String) - case fetchDatabaseInfosDone(GalleryState) - - case fetchPreviewURLs(Int) - case fetchPreviewURLsDone(Int, Result<[Int: URL], AppError>) - - case fetchImageURLs(Int) - case refetchImageURLs(Int) - case prefetchImages(Int, Int) - - case fetchThumbnailURLs(Int) - case fetchThumbnailURLsDone(Int, Result<[Int: URL], AppError>) - case fetchNormalImageURLs(Int, [Int: URL]) - case fetchNormalImageURLsDone(Int, Result<([Int: URL], [Int: URL]), AppError>) - case refetchNormalImageURLs(Int) - case refetchNormalImageURLsDone(Int, Result<([Int: URL], HTTPURLResponse?), AppError>) - - case fetchMPVKeys(Int, URL) - case fetchMPVKeysDone(Int, Result<(String, [Int: String]), AppError>) - case fetchMPVImageURL(Int, Bool) - case fetchMPVImageURLDone(Int, Result<(URL, URL?, String), AppError>) -} - -struct ReadingEnvironment { - let urlClient: URLClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient -} - -let readingReducer = Reducer { state, action, environment in - switch action { - case .binding(\.$showsSliderPreview): - return environment.hapticsClient.generateFeedback(.soft).fireAndForget() - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return .none - - case .toggleShowsPanel: - state.showsPanel.toggle() - return .none - - case .setOrientationPortrait(let isPortrait): - var effects = [EffectTask]() - if isPortrait { - effects.append(environment.appDelegateClient.setPortraitOrientationMask().fireAndForget()) - effects.append(environment.appDelegateClient.setPortraitOrientation().fireAndForget()) - } else { - effects.append(environment.appDelegateClient.setAllOrientationMask().fireAndForget()) - } - return .merge(effects) - - case .onPerformDismiss: - return environment.hapticsClient.generateFeedback(.light).fireAndForget() - - case .onAppear(let gid, let enablesLandscape): - var effects: [EffectTask] = [ - .init(value: .fetchDatabaseInfos(gid)) - ] - if enablesLandscape { - effects.append(.init(value: .setOrientationPortrait(false))) - } - return .merge(effects) - - case .onWebImageRetry(let index): - state.imageURLLoadingStates[index] = .idle - return .none - - case .onWebImageSucceeded(let index): - state.imageURLLoadingStates[index] = .idle - state.webImageLoadSuccessIndices.insert(index) - return .none - - case .onWebImageFailed(let index): - state.imageURLLoadingStates[index] = .failed(.webImageFailed) - return .none - - case .reloadAllWebImages: - state.previewURLs = .init() - state.thumbnailURLs = .init() - state.imageURLs = .init() - state.originalImageURLs = .init() - state.mpvKey = nil - state.mpvImageKeys = .init() - state.mpvSkipServerIdentifiers = .init() - state.forceRefreshID = .init() - return environment.databaseClient.removeImageURLs(gid: state.gallery.id).fireAndForget() - - case .retryAllFailedWebImages: - state.imageURLLoadingStates.forEach { (index, loadingState) in - if case .failed = loadingState { - state.imageURLLoadingStates[index] = .idle - } - } - state.previewLoadingStates.forEach { (index, loadingState) in - if case .failed = loadingState { - state.previewLoadingStates[index] = .idle - } - } - return .none - - case .copyImage(let imageURL): - return .init(value: .fetchImage(.copy(imageURL.isGIF), imageURL)) - - case .saveImage(let imageURL): - return .init(value: .fetchImage(.save(imageURL.isGIF), imageURL)) - - case .saveImageDone(let isSucceeded): - state.hudConfig = isSucceeded ? .savedToPhotoLibrary : .error - return .init(value: .setNavigation(.hud)) - - case .shareImage(let imageURL): - return .init(value: .fetchImage(.share(imageURL.isGIF), imageURL)) - - case .fetchImage(let action, let imageURL): - return environment.imageClient.fetchImage(url: imageURL) - .map({ ReadingAction.fetchImageDone(action, $0) }) - .cancellable(id: ReadingState.CancelID()) - - case .fetchImageDone(let action, let result): - if case .success(let image) = result { - switch action { - case .copy(let isAnimated): - state.hudConfig = .copiedToClipboardSucceeded - return .merge( - .init(value: .setNavigation(.hud)), - environment.clipboardClient.saveImage(image, isAnimated).fireAndForget() - ) - case .save(let isAnimated): - return environment.imageClient - .saveImageToPhotoLibrary(image, isAnimated).map(ReadingAction.saveImageDone) - case .share(let isAnimated): - if isAnimated, let data = image.kf.data(format: .GIF) { - return .init(value: .setNavigation(.share(.data(data)))) - } else { - return .init(value: .setNavigation(.share(.image(image)))) - } - } - } else { - state.hudConfig = .error - return .init(value: .setNavigation(.hud)) - } - - case .syncReadingProgress(let progress): - return environment.databaseClient - .updateReadingProgress(gid: state.gallery.id, progress: progress).fireAndForget() - - case .syncPreviewURLs(let previewURLs): - return environment.databaseClient - .updatePreviewURLs(gid: state.gallery.id, previewURLs: previewURLs).fireAndForget() - - case .syncThumbnailURLs(let thumbnailURLs): - return environment.databaseClient - .updateThumbnailURLs(gid: state.gallery.id, thumbnailURLs: thumbnailURLs).fireAndForget() - - case .syncImageURLs(let imageURLs, let originalImageURLs): - return environment.databaseClient - .updateImageURLs(gid: state.gallery.id, imageURLs: imageURLs, originalImageURLs: originalImageURLs) - .fireAndForget() - - case .teardown: - var effects: [EffectTask] = [ - .cancel(id: ReadingState.CancelID()) - ] - if !environment.deviceClient.isPad() { - effects.append(.init(value: .setOrientationPortrait(true))) - } - return .merge(effects) - - case .fetchDatabaseInfos(let gid): - guard let gallery = environment.databaseClient.fetchGallery(gid: gid) else { return .none } - state.gallery = gallery - state.galleryDetail = environment.databaseClient.fetchGalleryDetail(gid: state.gallery.id) - return environment.databaseClient.fetchGalleryState(gid: state.gallery.id) - .map(ReadingAction.fetchDatabaseInfosDone).cancellable(id: ReadingState.CancelID()) - - case .fetchDatabaseInfosDone(let galleryState): - if let previewConfig = galleryState.previewConfig { - state.previewConfig = previewConfig - } - state.previewURLs = galleryState.previewURLs - state.imageURLs = galleryState.imageURLs - state.thumbnailURLs = galleryState.thumbnailURLs - state.originalImageURLs = galleryState.originalImageURLs - state.readingProgress = galleryState.readingProgress - state.databaseLoadingState = .idle - return .none - - case .fetchPreviewURLs(let index): - guard state.previewLoadingStates[index] != .loading, - let galleryURL = state.gallery.galleryURL - else { return .none } - state.previewLoadingStates[index] = .loading - let pageNum = state.previewConfig.pageNumber(index: index) - return GalleryPreviewURLsRequest(galleryURL: galleryURL, pageNum: pageNum) - .effect.map({ ReadingAction.fetchPreviewURLsDone(index, $0) }).cancellable(id: ReadingState.CancelID()) - - case .fetchPreviewURLsDone(let index, let result): - switch result { - case .success(let previewURLs): - guard !previewURLs.isEmpty else { - state.previewLoadingStates[index] = .failed(.notFound) - return .none - } - state.previewLoadingStates[index] = .idle - state.updatePreviewURLs(previewURLs) - return .init(value: .syncPreviewURLs(previewURLs)) - case .failure(let error): - state.previewLoadingStates[index] = .failed(error) - } - return .none - - case .fetchImageURLs(let index): - if state.mpvKey != nil { - return .init(value: .fetchMPVImageURL(index, false)) - } else { - return .init(value: .fetchThumbnailURLs(index)) - } - - case .refetchImageURLs(let index): - if state.mpvKey != nil { - return .init(value: .fetchMPVImageURL(index, true)) - } else { - return .init(value: .refetchNormalImageURLs(index)) - } - - case .prefetchImages(let index, let prefetchLimit): - func getPrefetchImageURLs(range: ClosedRange) -> [URL] { - (range.lowerBound...range.upperBound).compactMap { index in - if let url = state.imageURLs[index] { - return url - } - return nil - } - } - func getFetchImageURLIndices(range: ClosedRange) -> [Int] { - (range.lowerBound...range.upperBound).compactMap { index in - if state.imageURLs[index] == nil, state.imageURLLoadingStates[index] != .loading { - return index - } - return nil - } - } - var prefetchImageURLs = [URL]() - var fetchImageURLIndices = [Int]() - var effects = [EffectTask]() - let previousUpperBound = max(index - 2, 1) - let previousLowerBound = max(previousUpperBound - prefetchLimit / 2, 1) - if previousUpperBound - previousLowerBound > 0 { - prefetchImageURLs += getPrefetchImageURLs(range: previousLowerBound...previousUpperBound) - fetchImageURLIndices += getFetchImageURLIndices(range: previousLowerBound...previousUpperBound) - } - let nextLowerBound = min(index + 2, state.gallery.pageCount) - let nextUpperBound = min(nextLowerBound + prefetchLimit / 2, state.gallery.pageCount) - if nextUpperBound - nextLowerBound > 0 { - prefetchImageURLs += getPrefetchImageURLs(range: nextLowerBound...nextUpperBound) - fetchImageURLIndices += getFetchImageURLIndices(range: nextLowerBound...nextUpperBound) - } - fetchImageURLIndices.forEach { - effects.append(.init(value: .fetchImageURLs($0))) - } - effects.append(environment.imageClient.prefetchImages(prefetchImageURLs).fireAndForget()) - return .merge(effects) - - case .fetchThumbnailURLs(let index): - guard state.imageURLLoadingStates[index] != .loading, - let galleryURL = state.gallery.galleryURL - else { return .none } - state.previewConfig.batchRange(index: index).forEach { - state.imageURLLoadingStates[$0] = .loading - } - let pageNum = state.previewConfig.pageNumber(index: index) - return ThumbnailURLsRequest(galleryURL: galleryURL, pageNum: pageNum) - .effect.map({ ReadingAction.fetchThumbnailURLsDone(index, $0) }).cancellable(id: ReadingState.CancelID()) - - case .fetchThumbnailURLsDone(let index, let result): - let batchRange = state.previewConfig.batchRange(index: index) - switch result { - case .success(let thumbnailURLs): - guard !thumbnailURLs.isEmpty else { - batchRange.forEach { - state.imageURLLoadingStates[$0] = .failed(.notFound) - } - return .none - } - if let url = thumbnailURLs[index], environment.urlClient.checkIfMPVURL(url) { - return .init(value: .fetchMPVKeys(index, url)) - } else { - state.updateThumbnailURLs(thumbnailURLs) - return .merge( - .init(value: .syncThumbnailURLs(thumbnailURLs)), - .init(value: .fetchNormalImageURLs(index, thumbnailURLs)) - ) - } - case .failure(let error): - batchRange.forEach { - state.imageURLLoadingStates[$0] = .failed(error) - } - } - return .none - - case .fetchNormalImageURLs(let index, let thumbnailURLs): - return GalleryNormalImageURLsRequest(thumbnailURLs: thumbnailURLs) - .effect.map({ ReadingAction.fetchNormalImageURLsDone(index, $0) }).cancellable(id: ReadingState.CancelID()) - - case .fetchNormalImageURLsDone(let index, let result): - let batchRange = state.previewConfig.batchRange(index: index) - switch result { - case .success(let (imageURLs, originalImageURLs)): - guard !imageURLs.isEmpty else { - batchRange.forEach { - state.imageURLLoadingStates[$0] = .failed(.notFound) - } - return .none - } - batchRange.forEach { - state.imageURLLoadingStates[$0] = .idle - } - state.updateImageURLs(imageURLs, originalImageURLs) - return .init(value: .syncImageURLs(imageURLs, originalImageURLs)) - case .failure(let error): - batchRange.forEach { - state.imageURLLoadingStates[$0] = .failed(error) - } - } - return .none - - case .refetchNormalImageURLs(let index): - guard state.imageURLLoadingStates[index] != .loading, - let galleryURL = state.gallery.galleryURL, - let imageURL = state.imageURLs[index] - else { return .none } - state.imageURLLoadingStates[index] = .loading - let pageNum = state.previewConfig.pageNumber(index: index) - return GalleryNormalImageURLRefetchRequest( - index: index, pageNum: pageNum, - galleryURL: galleryURL, - thumbnailURL: state.thumbnailURLs[index], - storedImageURL: imageURL - ) - .effect.map({ ReadingAction.refetchNormalImageURLsDone(index, $0) }).cancellable(id: ReadingState.CancelID()) - - case .refetchNormalImageURLsDone(let index, let result): - switch result { - case .success(let (imageURLs, response)): - var effects = [EffectTask]() - if let response = response { - effects.append(environment.cookieClient.setSkipServer(response: response).fireAndForget()) - } - guard !imageURLs.isEmpty else { - state.imageURLLoadingStates[index] = .failed(.notFound) - return effects.isEmpty ? .none : .merge(effects) - } - state.imageURLLoadingStates[index] = .idle - state.updateImageURLs(imageURLs, [:]) - effects.append(.init(value: .syncImageURLs(imageURLs, [:]))) - return .merge(effects) - case .failure(let error): - state.imageURLLoadingStates[index] = .failed(error) - } - return .none - - case .fetchMPVKeys(let index, let mpvURL): - return MPVKeysRequest(mpvURL: mpvURL) - .effect.map({ ReadingAction.fetchMPVKeysDone(index, $0) }).cancellable(id: ReadingState.CancelID()) - - case .fetchMPVKeysDone(let index, let result): - let batchRange = state.previewConfig.batchRange(index: index) - switch result { - case .success(let (mpvKey, mpvImageKeys)): - let pageCount = state.gallery.pageCount - guard mpvImageKeys.count == pageCount else { - batchRange.forEach { - state.imageURLLoadingStates[$0] = .failed(.notFound) - } - return .none - } - batchRange.forEach { - state.imageURLLoadingStates[$0] = .idle - } - state.mpvKey = mpvKey - state.mpvImageKeys = mpvImageKeys - return .merge( - Array(1...min(3, max(1, pageCount))).map { - .init(value: .fetchMPVImageURL($0, false)) - } - ) - case .failure(let error): - batchRange.forEach { - state.imageURLLoadingStates[$0] = .failed(error) - } - } - return .none - - case .fetchMPVImageURL(let index, let isRefresh): - guard let gidInteger = Int(state.gallery.id), let mpvKey = state.mpvKey, - let mpvImageKey = state.mpvImageKeys[index], - state.imageURLLoadingStates[index] != .loading - else { return .none } - state.imageURLLoadingStates[index] = .loading - let skipServerIdentifier = isRefresh ? state.mpvSkipServerIdentifiers[index] : nil - return GalleryMPVImageURLRequest( - gid: gidInteger, index: index, mpvKey: mpvKey, - mpvImageKey: mpvImageKey, skipServerIdentifier: skipServerIdentifier - ) - .effect.map({ ReadingAction.fetchMPVImageURLDone(index, $0) }).cancellable(id: ReadingState.CancelID()) - - case .fetchMPVImageURLDone(let index, let result): - switch result { - case .success(let (imageURL, originalImageURL, skipServerIdentifier)): - let imageURLs: [Int: URL] = [index: imageURL] - var originalImageURLs = [Int: URL]() - if let originalImageURL = originalImageURL { - originalImageURLs[index] = originalImageURL - } - state.imageURLLoadingStates[index] = .idle - state.mpvSkipServerIdentifiers[index] = skipServerIdentifier - state.updateImageURLs(imageURLs, originalImageURLs) - return .init(value: .syncImageURLs(imageURLs, originalImageURLs)) - case .failure(let error): - state.imageURLLoadingStates[index] = .failed(error) - } - return .none - } -} -.haptics( - unwrapping: \.route, - case: /ReadingState.Route.readingSetting, - hapticsClient: \.hapticsClient -) -.haptics( - unwrapping: \.route, - case: /ReadingState.Route.share, - hapticsClient: \.hapticsClient -) -.binding() diff --git a/EhPanda/View/Reading/ReadingView.swift b/EhPanda/View/Reading/ReadingView.swift index 7e3a4095..cb361eca 100644 --- a/EhPanda/View/Reading/ReadingView.swift +++ b/EhPanda/View/Reading/ReadingView.swift @@ -13,8 +13,8 @@ import ComposableArchitecture struct ReadingView: View { @Environment(\.colorScheme) private var colorScheme - let store: Store - @ObservedObject private var viewStore: ViewStore + let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let gid: String @Binding private var setting: Setting private let blurRadius: Double @@ -26,7 +26,7 @@ struct ReadingView: View { @StateObject private var page: Page = .first() init( - store: Store, + store: StoreOf, gid: String, setting: Binding, blurRadius: Double ) { self.store = store @@ -81,7 +81,7 @@ struct ReadingView: View { fetchPreviewURLsAction: { viewStore.send(.fetchPreviewURLs($0)) } ) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /ReadingState.Route.readingSetting) { _ in + .sheet(unwrapping: viewStore.binding(\.$route), case: /ReadingReducer.Route.readingSetting) { _ in NavigationView { ReadingSettingView( readingDirection: $setting.readingDirection, @@ -106,14 +106,14 @@ struct ReadingView: View { .accentColor(setting.accentColor).tint(setting.accentColor) .autoBlur(radius: blurRadius).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /ReadingState.Route.share) { route in + .sheet(unwrapping: viewStore.binding(\.$route), case: /ReadingReducer.Route.share) { route in ActivityView(activityItems: [route.wrappedValue.associatedValue]) .accentColor(setting.accentColor).autoBlur(radius: blurRadius) } .progressHUD( config: viewStore.hudConfig, unwrapping: viewStore.binding(\.$route), - case: /ReadingState.Route.hud + case: /ReadingReducer.Route.hud ) // Page @@ -600,17 +600,7 @@ struct ReadingView_Previews: PreviewProvider { ReadingView( store: .init( initialState: .init(gallery: .empty), - reducer: readingReducer, - environment: ReadingEnvironment( - urlClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live - ) + reducer: ReadingReducer() ), gid: .init(), setting: .constant(.init()), diff --git a/EhPanda/View/Search/SearchReducer.swift b/EhPanda/View/Search/SearchReducer.swift index b4f0da65..b8e2a1ab 100644 --- a/EhPanda/View/Search/SearchReducer.swift +++ b/EhPanda/View/Search/SearchReducer.swift @@ -29,7 +29,7 @@ struct SearchReducer: ReducerProtocol { var footerLoadingState: LoadingState = .idle var filtersState = FiltersReducer.State() - @Heap var detailState: DetailState! + @Heap var detailState: DetailReducer.State! var quickSearchState = QuickSearchReducer.State() init() { @@ -56,7 +56,7 @@ struct SearchReducer: ReducerProtocol { case fetchMoreGalleries case fetchMoreGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) - case detail(DetailAction) + case detail(DetailReducer.Action) case filters(FiltersReducer.Action) case quickSearch(QuickSearchReducer.Action) } @@ -182,25 +182,7 @@ struct SearchReducer: ReducerProtocol { Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) - -// detailReducer.pullback( -// state: \.detailState, -// action: /SearchAction.detail, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ) + Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) BindingReducer() } diff --git a/EhPanda/View/Search/SearchRootReducer.swift b/EhPanda/View/Search/SearchRootReducer.swift index 406d080b..da3f97bd 100644 --- a/EhPanda/View/Search/SearchRootReducer.swift +++ b/EhPanda/View/Search/SearchRootReducer.swift @@ -26,7 +26,7 @@ struct SearchRootReducer: ReducerProtocol { var searchState = SearchReducer.State() var filtersState = FiltersReducer.State() var quickSearchState = QuickSearchReducer.State() - @Heap var detailState: DetailState! + @Heap var detailState: DetailReducer.State! init() { _detailState = .init(.init()) @@ -80,7 +80,7 @@ struct SearchRootReducer: ReducerProtocol { case search(SearchReducer.Action) case filters(FiltersReducer.Action) case quickSearch(QuickSearchReducer.Action) - case detail(DetailAction) + case detail(DetailReducer.Action) } @Dependency(\.databaseClient) private var databaseClient @@ -185,25 +185,7 @@ struct SearchRootReducer: ReducerProtocol { 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) - -// detailReducer.pullback( -// state: \.detailState, -// action: /SearchRootAction.detail, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ) + Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) BindingReducer() } From 48a7db16e1928603a4ee2fb94af4066cebdc8f08 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Fri, 26 May 2023 23:00:45 +0800 Subject: [PATCH 14/29] Refactor FavoritesReducer --- EhPanda.xcodeproj/project.pbxproj | 8 +- EhPanda/DataFlow/AppStore.swift | 42 ++-- EhPanda/View/Favorites/FavoritesReducer.swift | 194 +++++++++++++++ EhPanda/View/Favorites/FavoritesStore.swift | 232 ------------------ EhPanda/View/Favorites/FavoritesView.swift | 33 +-- 5 files changed, 229 insertions(+), 280 deletions(-) create mode 100644 EhPanda/View/Favorites/FavoritesReducer.swift delete mode 100644 EhPanda/View/Favorites/FavoritesStore.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 42e5ef6d..50e982fc 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -232,7 +232,7 @@ ABCD2F0E25976B95008E5A20 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD2F0D25976B95008E5A20 /* Parser.swift */; }; ABD4032626B78E5A00001B8C /* GalleryThumbnailCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD4032526B78E5A00001B8C /* GalleryThumbnailCell.swift */; }; ABD4032826B7967F00001B8C /* CategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD4032726B7967F00001B8C /* CategoryView.swift */; }; - ABD49D5A277C5356003D1A07 /* FavoritesStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D59277C5356003D1A07 /* FavoritesStore.swift */; }; + ABD49D5A277C5356003D1A07 /* FavoritesReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D59277C5356003D1A07 /* FavoritesReducer.swift */; }; ABD49D5D277C6C9D003D1A07 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = ABD49D5C277C6C9D003D1A07 /* SFSafeSymbols */; }; ABD49D60277C7722003D1A07 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D5F277C7722003D1A07 /* TabBarView.swift */; }; ABD49D64277C7AD5003D1A07 /* TabBarStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D63277C7AD5003D1A07 /* TabBarStore.swift */; }; @@ -537,7 +537,7 @@ ABCD2F0D25976B95008E5A20 /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; ABD4032526B78E5A00001B8C /* GalleryThumbnailCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryThumbnailCell.swift; sourceTree = ""; }; ABD4032726B7967F00001B8C /* CategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryView.swift; sourceTree = ""; }; - ABD49D59277C5356003D1A07 /* FavoritesStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesStore.swift; sourceTree = ""; }; + ABD49D59277C5356003D1A07 /* FavoritesReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesReducer.swift; sourceTree = ""; }; ABD49D5F277C7722003D1A07 /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; ABD49D63277C7AD5003D1A07 /* TabBarStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarStore.swift; sourceTree = ""; }; ABD49D66277EAC90003D1A07 /* URLUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLUtil.swift; sourceTree = ""; }; @@ -681,7 +681,7 @@ isa = PBXGroup; children = ( AB24C55927674EDF0085C33A /* FavoritesView.swift */, - ABD49D59277C5356003D1A07 /* FavoritesStore.swift */, + ABD49D59277C5356003D1A07 /* FavoritesReducer.swift */, ); path = Favorites; sourceTree = ""; @@ -1791,7 +1791,7 @@ AB7BF30A27ABDFF1001865A3 /* CoreDataMigrationStep.swift in Sources */, AB69CB8226B3DAF400699359 /* ControlPanel.swift in Sources */, AB7BF2D227AA3EDC001865A3 /* HapticsUtil.swift in Sources */, - ABD49D5A277C5356003D1A07 /* FavoritesStore.swift in Sources */, + ABD49D5A277C5356003D1A07 /* FavoritesReducer.swift in Sources */, AB1EF25427AFA19200F507D6 /* Heap.swift in Sources */, AB7BF2C227A96760001865A3 /* GalleryDetail.swift in Sources */, ABE1867826A1733000689FDC /* LaboratorySettingView.swift in Sources */, diff --git a/EhPanda/DataFlow/AppStore.swift b/EhPanda/DataFlow/AppStore.swift index cd88f567..84851807 100644 --- a/EhPanda/DataFlow/AppStore.swift +++ b/EhPanda/DataFlow/AppStore.swift @@ -14,7 +14,7 @@ struct AppState: Equatable { var appLockState = AppLockState() var tabBarState = TabBarState() var homeState = HomeState() - var favoritesState = FavoritesState() + var favoritesState = FavoritesReducer.State() var searchRootState = SearchRootReducer.State() var settingState = SettingReducer.State() } @@ -30,7 +30,7 @@ enum AppAction: BindableAction { case tabBar(TabBarAction) case home(HomeAction) - case favorites(FavoritesAction) + case favorites(FavoritesReducer.Action) case searchRoot(SearchRootReducer.Action) case setting(SettingReducer.Action) } @@ -277,27 +277,27 @@ let appReducer = Reducer.combine( uiApplicationClient: $0.uiApplicationClient ) } - ), - favoritesReducer.pullback( - state: \.favoritesState, - action: /AppAction.favorites, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - userDefaultsClient: $0.userDefaultsClient, - uiApplicationClient: $0.uiApplicationClient - ) - } ) // , +// favoritesReducer.pullback( +// state: \.favoritesState, +// action: /AppAction.favorites, +// environment: { +// .init( +// urlClient: $0.urlClient, +// fileClient: $0.fileClient, +// imageClient: $0.imageClient, +// deviceClient: $0.deviceClient, +// hapticsClient: $0.hapticsClient, +// cookieClient: $0.cookieClient, +// databaseClient: $0.databaseClient, +// clipboardClient: $0.clipboardClient, +// appDelegateClient: $0.appDelegateClient, +// userDefaultsClient: $0.userDefaultsClient, +// uiApplicationClient: $0.uiApplicationClient +// ) +// } +// ), // searchRootReducer.pullback( // state: \.searchRootState, // action: /AppAction.searchRoot, diff --git a/EhPanda/View/Favorites/FavoritesReducer.swift b/EhPanda/View/Favorites/FavoritesReducer.swift new file mode 100644 index 00000000..c592d4d2 --- /dev/null +++ b/EhPanda/View/Favorites/FavoritesReducer.swift @@ -0,0 +1,194 @@ +// +// FavoritesReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/12/29. +// + +import SwiftUI +import IdentifiedCollections +import ComposableArchitecture + +struct FavoritesReducer: ReducerProtocol { + enum Route: Equatable { + case quickSearch + case detail(String) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var keyword = "" + + var index = -1 + var sortOrder: FavoritesSortOrder? + + var rawGalleries = [Int: [Gallery]]() + var rawPageNumber = [Int: PageNumber]() + var rawLoadingState = [Int: LoadingState]() + var rawFooterLoadingState = [Int: LoadingState]() + + var galleries: [Gallery]? { + rawGalleries[index] + } + var pageNumber: PageNumber? { + rawPageNumber[index] + } + var loadingState: LoadingState? { + rawLoadingState[index] + } + var footerLoadingState: LoadingState? { + rawFooterLoadingState[index] + } + + @Heap var detailState: DetailReducer.State! + var quickSearchState = QuickSearchReducer.State() + + init() { + _detailState = .init(.init()) + } + + mutating func insertGalleries(index: Int, galleries: [Gallery]) { + galleries.forEach { gallery in + if rawGalleries[index]?.contains(gallery) == false { + rawGalleries[index]?.append(gallery) + } + } + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case setFavoritesIndex(Int) + case clearSubStates + case onNotLoginViewButtonTapped + + case fetchGalleries(String? = nil, FavoritesSortOrder? = nil) + case fetchGalleriesDone(Int, Result<(PageNumber, FavoritesSortOrder?, [Gallery]), AppError>) + case fetchMoreGalleries + case fetchMoreGalleriesDone(Int, Result<(PageNumber, FavoritesSortOrder?, [Gallery]), AppError>) + + case detail(DetailReducer.Action) + case quickSearch(QuickSearchReducer.Action) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .setFavoritesIndex(let index): + state.index = index + guard state.galleries?.isEmpty != false else { return .none } + return .init(value: Action.fetchGalleries()) + + case .clearSubStates: + state.detailState = .init() + return .init(value: .detail(.teardown)) + + case .onNotLoginViewButtonTapped: + return .none + + case .fetchGalleries(let keyword, let sortOrder): + guard state.loadingState != .loading else { return .none } + state.rawLoadingState[state.index] = .loading + if let keyword = keyword { + state.keyword = keyword + } + if state.pageNumber == nil { + state.rawPageNumber[state.index] = PageNumber() + } else { + state.rawPageNumber[state.index]?.resetPages() + } + return FavoritesGalleriesRequest( + favIndex: state.index, keyword: state.keyword, sortOrder: sortOrder + ) + .effect.map { [index = state.index] result in Action.fetchGalleriesDone(index, result) } + + case .fetchGalleriesDone(let targetFavIndex, let result): + state.rawLoadingState[targetFavIndex] = .idle + switch result { + case .success(let (pageNumber, sortOrder, galleries)): + guard !galleries.isEmpty else { + state.rawLoadingState[targetFavIndex] = .failed(.notFound) + guard pageNumber.hasNextPage() else { return .none } + return .init(value: .fetchMoreGalleries) + } + state.rawPageNumber[targetFavIndex] = pageNumber + state.rawGalleries[targetFavIndex] = galleries + state.sortOrder = sortOrder + return databaseClient.cacheGalleries(galleries).fireAndForget() + case .failure(let error): + state.rawLoadingState[targetFavIndex] = .failed(error) + } + return .none + + case .fetchMoreGalleries: + let pageNumber = state.pageNumber ?? .init() + guard pageNumber.hasNextPage(), + state.footerLoadingState != .loading, + let lastID = state.galleries?.last?.id, + let lastItemTimestamp = pageNumber.lastItemTimestamp + else { return .none } + state.rawFooterLoadingState[state.index] = .loading + return MoreFavoritesGalleriesRequest( + favIndex: state.index, + lastID: lastID, + lastTimestamp: lastItemTimestamp, + keyword: state.keyword + ) + .effect.map { [index = state.index] result in Action.fetchMoreGalleriesDone(index, result) } + + case .fetchMoreGalleriesDone(let targetFavIndex, let result): + state.rawFooterLoadingState[targetFavIndex] = .idle + switch result { + case .success(let (pageNumber, sortOrder, galleries)): + state.rawPageNumber[targetFavIndex] = pageNumber + state.insertGalleries(index: targetFavIndex, galleries: galleries) + state.sortOrder = sortOrder + + var effects: [EffectTask] = [ + databaseClient.cacheGalleries(galleries).fireAndForget() + ] + if galleries.isEmpty, pageNumber.hasNextPage() { + effects.append(.init(value: .fetchMoreGalleries)) + } else if !galleries.isEmpty { + state.rawLoadingState[targetFavIndex] = .idle + } + return .merge(effects) + + case .failure(let error): + state.rawFooterLoadingState[targetFavIndex] = .failed(error) + } + return .none + + case .detail: + return .none + + case .quickSearch: + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.quickSearch, + hapticsClient: hapticsClient + ) + + Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) + + BindingReducer() + } +} diff --git a/EhPanda/View/Favorites/FavoritesStore.swift b/EhPanda/View/Favorites/FavoritesStore.swift deleted file mode 100644 index 1c12f256..00000000 --- a/EhPanda/View/Favorites/FavoritesStore.swift +++ /dev/null @@ -1,232 +0,0 @@ -// -// FavoritesStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 3/12/29. -// - -import SwiftUI -import IdentifiedCollections -import ComposableArchitecture - -// MARK: State -struct FavoritesState: Equatable { - enum Route: Equatable { - case quickSearch - case detail(String) - } - - init() { - _detailState = .init(.init()) - } - - @BindingState var route: Route? - @BindingState var keyword = "" - - var index = -1 - var sortOrder: FavoritesSortOrder? - - var rawGalleries = [Int: [Gallery]]() - var rawPageNumber = [Int: PageNumber]() - var rawLoadingState = [Int: LoadingState]() - var rawFooterLoadingState = [Int: LoadingState]() - - var galleries: [Gallery]? { - rawGalleries[index] - } - var pageNumber: PageNumber? { - rawPageNumber[index] - } - var loadingState: LoadingState? { - rawLoadingState[index] - } - var footerLoadingState: LoadingState? { - rawFooterLoadingState[index] - } - - @Heap var detailState: DetailReducer.State! - var quickSearchState = QuickSearchReducer.State() - - mutating func insertGalleries(index: Int, galleries: [Gallery]) { - galleries.forEach { gallery in - if rawGalleries[index]?.contains(gallery) == false { - rawGalleries[index]?.append(gallery) - } - } - } -} - -// MARK: Action -enum FavoritesAction: BindableAction { - case binding(BindingAction) - case setNavigation(FavoritesState.Route?) - case setFavoritesIndex(Int) - case clearSubStates - case onNotLoginViewButtonTapped - - case fetchGalleries(String? = nil, FavoritesSortOrder? = nil) - case fetchGalleriesDone(Int, Result<(PageNumber, FavoritesSortOrder?, [Gallery]), AppError>) - case fetchMoreGalleries - case fetchMoreGalleriesDone(Int, Result<(PageNumber, FavoritesSortOrder?, [Gallery]), AppError>) - - case detail(DetailReducer.Action) - case quickSearch(QuickSearchReducer.Action) -} - -// MARK: Environment -struct FavoritesEnvironment { - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let userDefaultsClient: UserDefaultsClient - let uiApplicationClient: UIApplicationClient -} - -// MARK: Reducer -let favoritesReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .setFavoritesIndex(let index): - state.index = index - guard state.galleries?.isEmpty != false else { return .none } - return .init(value: FavoritesAction.fetchGalleries()) - - case .clearSubStates: - state.detailState = .init() - return .init(value: .detail(.teardown)) - - case .onNotLoginViewButtonTapped: - return .none - - case .fetchGalleries(let keyword, let sortOrder): - guard state.loadingState != .loading else { return .none } - state.rawLoadingState[state.index] = .loading - if let keyword = keyword { - state.keyword = keyword - } - if state.pageNumber == nil { - state.rawPageNumber[state.index] = PageNumber() - } else { - state.rawPageNumber[state.index]?.resetPages() - } - return FavoritesGalleriesRequest( - favIndex: state.index, keyword: state.keyword, sortOrder: sortOrder - ) - .effect.map { [index = state.index] result in FavoritesAction.fetchGalleriesDone(index, result) } - - case .fetchGalleriesDone(let targetFavIndex, let result): - state.rawLoadingState[targetFavIndex] = .idle - switch result { - case .success(let (pageNumber, sortOrder, galleries)): - guard !galleries.isEmpty else { - state.rawLoadingState[targetFavIndex] = .failed(.notFound) - guard pageNumber.hasNextPage() else { return .none } - return .init(value: .fetchMoreGalleries) - } - state.rawPageNumber[targetFavIndex] = pageNumber - state.rawGalleries[targetFavIndex] = galleries - state.sortOrder = sortOrder - return environment.databaseClient.cacheGalleries(galleries).fireAndForget() - case .failure(let error): - state.rawLoadingState[targetFavIndex] = .failed(error) - } - return .none - - case .fetchMoreGalleries: - let pageNumber = state.pageNumber ?? .init() - guard pageNumber.hasNextPage(), - state.footerLoadingState != .loading, - let lastID = state.galleries?.last?.id, - let lastItemTimestamp = pageNumber.lastItemTimestamp - else { return .none } - state.rawFooterLoadingState[state.index] = .loading - return MoreFavoritesGalleriesRequest( - favIndex: state.index, - lastID: lastID, - lastTimestamp: lastItemTimestamp, - keyword: state.keyword - ) - .effect.map { [index = state.index] result in FavoritesAction.fetchMoreGalleriesDone(index, result) } - - case .fetchMoreGalleriesDone(let targetFavIndex, let result): - state.rawFooterLoadingState[targetFavIndex] = .idle - switch result { - case .success(let (pageNumber, sortOrder, galleries)): - state.rawPageNumber[targetFavIndex] = pageNumber - state.insertGalleries(index: targetFavIndex, galleries: galleries) - state.sortOrder = sortOrder - - var effects: [EffectTask] = [ - environment.databaseClient.cacheGalleries(galleries).fireAndForget() - ] - if galleries.isEmpty, pageNumber.hasNextPage() { - effects.append(.init(value: .fetchMoreGalleries)) - } else if !galleries.isEmpty { - state.rawLoadingState[targetFavIndex] = .idle - } - return .merge(effects) - - case .failure(let error): - state.rawFooterLoadingState[targetFavIndex] = .failed(error) - } - return .none - - case .detail: - return .none - - case .quickSearch: - return .none - } - } - .haptics( - unwrapping: \.route, - case: /FavoritesState.Route.quickSearch, - hapticsClient: \.hapticsClient - ) - .binding() -// , -// detailReducer.pullback( -// state: \FavoritesState.detailState, -// action: /FavoritesAction.detail, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ) -// , -// quickSearchReducer.pullback( -// state: \.quickSearchState, -// action: /FavoritesAction.quickSearch, -// environment: { -// .init( -// databaseClient: $0.databaseClient -// ) -// } -// ) -) diff --git a/EhPanda/View/Favorites/FavoritesView.swift b/EhPanda/View/Favorites/FavoritesView.swift index 7943c0a5..0887c65a 100644 --- a/EhPanda/View/Favorites/FavoritesView.swift +++ b/EhPanda/View/Favorites/FavoritesView.swift @@ -10,15 +10,15 @@ import AlertKit import ComposableArchitecture struct FavoritesView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double private let tagTranslator: TagTranslator init( - store: Store, + store: StoreOf, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store @@ -57,21 +57,21 @@ struct FavoritesView: View { } .sheet( unwrapping: viewStore.binding(\.$route), - case: /FavoritesState.Route.detail, + case: /FavoritesReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: FavoritesAction.detail), + store: store.scope(state: \.detailState, action: FavoritesReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /FavoritesState.Route.quickSearch) { _ in + .sheet(unwrapping: viewStore.binding(\.$route), case: /FavoritesReducer.Route.quickSearch) { _ in QuickSearchView( - store: store.scope(state: \.quickSearchState, action: FavoritesAction.quickSearch) + store: store.scope(state: \.quickSearchState, action: FavoritesReducer.Action.quickSearch) ) { keyword in viewStore.send(.setNavigation(nil)) viewStore.send(.fetchGalleries(keyword)) @@ -103,9 +103,9 @@ struct FavoritesView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /FavoritesState.Route.detail) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /FavoritesReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: FavoritesAction.detail), + store: store.scope(state: \.detailState, action: FavoritesReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -136,20 +136,7 @@ struct FavoritesView_Previews: PreviewProvider { FavoritesView( store: .init( initialState: .init(), - reducer: favoritesReducer, - environment: FavoritesEnvironment( - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - userDefaultsClient: .live, - uiApplicationClient: .live - ) + reducer: FavoritesReducer() ), user: .init(), setting: .constant(.init()), From b4ced8504f210ae778448137d74cdb29738a03c6 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Fri, 26 May 2023 23:07:28 +0800 Subject: [PATCH 15/29] Refactor HomeReducer Refactor ToplistsReducer Refactor PopularReducer Refactor WatchedReducer Refactor HistoryReducer Refactor FrontpageReducer --- EhPanda.xcodeproj/project.pbxproj | 106 +++-- EhPanda/DataFlow/AppStore.swift | 42 +- .../View/Home/DataFlow/FrontpageStore.swift | 197 ---------- EhPanda/View/Home/DataFlow/HistoryStore.swift | 122 ------ EhPanda/View/Home/DataFlow/HomeStore.swift | 368 ------------------ EhPanda/View/Home/DataFlow/PopularStore.swift | 147 ------- .../View/Home/DataFlow/ToplistsStore.swift | 230 ----------- EhPanda/View/Home/DataFlow/WatchedStore.swift | 223 ----------- .../Home/Frontpage/FrontpageReducer.swift | 166 ++++++++ .../Home/{ => Frontpage}/FrontpageView.swift | 32 +- .../View/Home/History/HistoryReducer.swift | 98 +++++ .../View/Home/{ => History}/HistoryView.swift | 30 +- EhPanda/View/Home/HomeReducer.swift | 259 ++++++++++++ EhPanda/View/Home/HomeView.swift | 43 +- .../View/Home/Popular/PopularReducer.swift | 116 ++++++ .../View/Home/{ => Popular}/PopularView.swift | 32 +- .../View/Home/Toplists/ToplistsReducer.swift | 206 ++++++++++ .../Home/{ => Toplists}/ToplistsView.swift | 28 +- .../View/Home/Watched/WatchedReducer.swift | 184 +++++++++ .../View/Home/{ => Watched}/WatchedView.swift | 36 +- 20 files changed, 1183 insertions(+), 1482 deletions(-) delete mode 100644 EhPanda/View/Home/DataFlow/FrontpageStore.swift delete mode 100644 EhPanda/View/Home/DataFlow/HistoryStore.swift delete mode 100644 EhPanda/View/Home/DataFlow/HomeStore.swift delete mode 100644 EhPanda/View/Home/DataFlow/PopularStore.swift delete mode 100644 EhPanda/View/Home/DataFlow/ToplistsStore.swift delete mode 100644 EhPanda/View/Home/DataFlow/WatchedStore.swift create mode 100644 EhPanda/View/Home/Frontpage/FrontpageReducer.swift rename EhPanda/View/Home/{ => Frontpage}/FrontpageView.swift (77%) create mode 100644 EhPanda/View/Home/History/HistoryReducer.swift rename EhPanda/View/Home/{ => History}/HistoryView.swift (79%) create mode 100644 EhPanda/View/Home/HomeReducer.swift create mode 100644 EhPanda/View/Home/Popular/PopularReducer.swift rename EhPanda/View/Home/{ => Popular}/PopularView.swift (77%) create mode 100644 EhPanda/View/Home/Toplists/ToplistsReducer.swift rename EhPanda/View/Home/{ => Toplists}/ToplistsView.swift (85%) create mode 100644 EhPanda/View/Home/Watched/WatchedReducer.swift rename EhPanda/View/Home/{ => Watched}/WatchedView.swift (81%) diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 50e982fc..03913060 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -116,19 +116,19 @@ AB6D106A27EBF890003A2245 /* GeneralSettingStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6D106927EBF890003A2245 /* GeneralSettingStoreTests.swift */; }; AB6DE897268822390087C579 /* LogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6DE896268822390087C579 /* LogsView.swift */; }; AB706F7927890A6C0025A48A /* AppRouteStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F7827890A6C0025A48A /* AppRouteStore.swift */; }; - AB706F7B278937500025A48A /* FrontpageStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F7A278937500025A48A /* FrontpageStore.swift */; }; + AB706F7B278937500025A48A /* FrontpageReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F7A278937500025A48A /* FrontpageReducer.swift */; }; AB706F80278981370025A48A /* AlertKit_Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F7F278981370025A48A /* AlertKit_Extension.swift */; }; AB706F82278986120025A48A /* ToolbarItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F81278986120025A48A /* ToolbarItems.swift */; }; AB706F842789AD2D0025A48A /* ToplistsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F832789AD2D0025A48A /* ToplistsView.swift */; }; - AB706F862789AD490025A48A /* ToplistsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F852789AD490025A48A /* ToplistsStore.swift */; }; + AB706F862789AD490025A48A /* ToplistsReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F852789AD490025A48A /* ToplistsReducer.swift */; }; AB706F88278A4C8A0025A48A /* PopularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F87278A4C8A0025A48A /* PopularView.swift */; }; - AB706F8A278A4CC50025A48A /* PopularStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F89278A4CC50025A48A /* PopularStore.swift */; }; + AB706F8A278A4CC50025A48A /* PopularReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F89278A4CC50025A48A /* PopularReducer.swift */; }; AB706F8C278A4F6C0025A48A /* WatchedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F8B278A4F6C0025A48A /* WatchedView.swift */; }; AB706F8E278A5DCF0025A48A /* DeviceClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F8D278A5DCF0025A48A /* DeviceClient.swift */; }; AB706F90278A5F680025A48A /* AppDelegateClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F8F278A5F680025A48A /* AppDelegateClient.swift */; }; - AB706F92278A6E8C0025A48A /* WatchedStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F91278A6E8C0025A48A /* WatchedStore.swift */; }; + AB706F92278A6E8C0025A48A /* WatchedReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F91278A6E8C0025A48A /* WatchedReducer.swift */; }; AB706F95278A75D30025A48A /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F94278A75D30025A48A /* HistoryView.swift */; }; - AB706F97278A77E20025A48A /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F96278A77E20025A48A /* HistoryStore.swift */; }; + AB706F97278A77E20025A48A /* HistoryReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F96278A77E20025A48A /* HistoryReducer.swift */; }; AB706F99278A820C0025A48A /* FiltersReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F98278A820C0025A48A /* FiltersReducer.swift */; }; AB706F9B278AC5A30025A48A /* SearchRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F9A278AC5A30025A48A /* SearchRootView.swift */; }; AB706F9D278ACCA20025A48A /* SearchRootReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F9C278ACCA20025A48A /* SearchRootReducer.swift */; }; @@ -173,7 +173,7 @@ AB86AC0A2782FAFA00E61E6A /* AppearanceSettingReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC092782FAFA00E61E6A /* AppearanceSettingReducer.swift */; }; AB86AC1027831AD100E61E6A /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = AB86AC0F27831AD100E61E6A /* ComposableArchitecture */; }; AB86AC1327856F2700E61E6A /* AppLockStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC1227856F2700E61E6A /* AppLockStore.swift */; }; - AB86AC1A2785C2B300E61E6A /* HomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC192785C2B300E61E6A /* HomeStore.swift */; }; + AB86AC1A2785C2B300E61E6A /* HomeReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC192785C2B300E61E6A /* HomeReducer.swift */; }; AB8C821926BF801700E8C5E6 /* EhSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8C821826BF801700E8C5E6 /* EhSetting.swift */; }; AB90276B291F548700697256 /* AppIcon_NotMyPresident@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB902766291F548600697256 /* AppIcon_NotMyPresident@3x.png */; }; AB90276C291F548700697256 /* AppIcon_NotMyPresident_iPad@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB902767291F548600697256 /* AppIcon_NotMyPresident_iPad@2x.png */; }; @@ -415,20 +415,20 @@ AB6D106927EBF890003A2245 /* GeneralSettingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingStoreTests.swift; sourceTree = ""; }; AB6DE896268822390087C579 /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = ""; }; AB706F7827890A6C0025A48A /* AppRouteStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteStore.swift; sourceTree = ""; }; - AB706F7A278937500025A48A /* FrontpageStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrontpageStore.swift; sourceTree = ""; }; + AB706F7A278937500025A48A /* FrontpageReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrontpageReducer.swift; sourceTree = ""; }; AB706F7F278981370025A48A /* AlertKit_Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertKit_Extension.swift; sourceTree = ""; }; AB706F81278986120025A48A /* ToolbarItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarItems.swift; sourceTree = ""; }; AB706F832789AD2D0025A48A /* ToplistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToplistsView.swift; sourceTree = ""; }; - AB706F852789AD490025A48A /* ToplistsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToplistsStore.swift; sourceTree = ""; }; + AB706F852789AD490025A48A /* ToplistsReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToplistsReducer.swift; sourceTree = ""; }; AB706F87278A4C8A0025A48A /* PopularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularView.swift; sourceTree = ""; }; - AB706F89278A4CC50025A48A /* PopularStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularStore.swift; sourceTree = ""; }; + AB706F89278A4CC50025A48A /* PopularReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularReducer.swift; sourceTree = ""; }; AB706F8B278A4F6C0025A48A /* WatchedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchedView.swift; sourceTree = ""; }; AB706F8D278A5DCF0025A48A /* DeviceClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceClient.swift; sourceTree = ""; }; AB706F8F278A5F680025A48A /* AppDelegateClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateClient.swift; sourceTree = ""; }; - AB706F91278A6E8C0025A48A /* WatchedStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchedStore.swift; sourceTree = ""; }; + AB706F91278A6E8C0025A48A /* WatchedReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchedReducer.swift; sourceTree = ""; }; AB706F93278A6F2B0025A48A /* Model 6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 6.xcdatamodel"; sourceTree = ""; }; AB706F94278A75D30025A48A /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; - AB706F96278A77E20025A48A /* HistoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryStore.swift; sourceTree = ""; }; + AB706F96278A77E20025A48A /* HistoryReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryReducer.swift; sourceTree = ""; }; AB706F98278A820C0025A48A /* FiltersReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersReducer.swift; sourceTree = ""; }; AB706F9A278AC5A30025A48A /* SearchRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRootView.swift; sourceTree = ""; }; AB706F9C278ACCA20025A48A /* SearchRootReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRootReducer.swift; sourceTree = ""; }; @@ -474,7 +474,7 @@ AB86ABF82782EC0D00E61E6A /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; AB86AC092782FAFA00E61E6A /* AppearanceSettingReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingReducer.swift; sourceTree = ""; }; AB86AC1227856F2700E61E6A /* AppLockStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockStore.swift; sourceTree = ""; }; - AB86AC192785C2B300E61E6A /* HomeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeStore.swift; sourceTree = ""; }; + AB86AC192785C2B300E61E6A /* HomeReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeReducer.swift; sourceTree = ""; }; AB8C821826BF801700E8C5E6 /* EhSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EhSetting.swift; sourceTree = ""; }; AB902766291F548600697256 /* AppIcon_NotMyPresident@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon_NotMyPresident@3x.png"; sourceTree = ""; }; AB902767291F548600697256 /* AppIcon_NotMyPresident_iPad@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon_NotMyPresident_iPad@2x.png"; sourceTree = ""; }; @@ -901,19 +901,6 @@ path = ShareExtension; sourceTree = ""; }; - AB706F7D278937A70025A48A /* DataFlow */ = { - isa = PBXGroup; - children = ( - AB86AC192785C2B300E61E6A /* HomeStore.swift */, - AB706F7A278937500025A48A /* FrontpageStore.swift */, - AB706F852789AD490025A48A /* ToplistsStore.swift */, - AB706F89278A4CC50025A48A /* PopularStore.swift */, - AB706F91278A6E8C0025A48A /* WatchedStore.swift */, - AB706F96278A77E20025A48A /* HistoryStore.swift */, - ); - path = DataFlow; - sourceTree = ""; - }; AB706F7E278981210025A48A /* Extensions */ = { isa = PBXGroup; children = ( @@ -1271,13 +1258,13 @@ ABF45AC025F3313D00ECB568 /* Home */ = { isa = PBXGroup; children = ( + EA7E47EC2A210C4300971697 /* History */, + EA7E47EA2A2103FE00971697 /* Popular */, + EA7E47E92A2102BA00971697 /* Toplists */, + EA7E47EB2A2107CF00971697 /* Watched */, + EA7E47E82A21015400971697 /* Frontpage */, AB24C55B2767565A0085C33A /* HomeView.swift */, - AB3072D3276E19AA00EFF242 /* FrontpageView.swift */, - AB706F832789AD2D0025A48A /* ToplistsView.swift */, - AB706F87278A4C8A0025A48A /* PopularView.swift */, - AB706F8B278A4F6C0025A48A /* WatchedView.swift */, - AB706F94278A75D30025A48A /* HistoryView.swift */, - AB706F7D278937A70025A48A /* DataFlow */, + AB86AC192785C2B300E61E6A /* HomeReducer.swift */, ); path = Home; sourceTree = ""; @@ -1453,6 +1440,51 @@ path = DetailSearch; sourceTree = ""; }; + EA7E47E82A21015400971697 /* Frontpage */ = { + isa = PBXGroup; + children = ( + AB3072D3276E19AA00EFF242 /* FrontpageView.swift */, + AB706F7A278937500025A48A /* FrontpageReducer.swift */, + ); + path = Frontpage; + sourceTree = ""; + }; + EA7E47E92A2102BA00971697 /* Toplists */ = { + isa = PBXGroup; + children = ( + AB706F832789AD2D0025A48A /* ToplistsView.swift */, + AB706F852789AD490025A48A /* ToplistsReducer.swift */, + ); + path = Toplists; + sourceTree = ""; + }; + EA7E47EA2A2103FE00971697 /* Popular */ = { + isa = PBXGroup; + children = ( + AB706F87278A4C8A0025A48A /* PopularView.swift */, + AB706F89278A4CC50025A48A /* PopularReducer.swift */, + ); + path = Popular; + sourceTree = ""; + }; + EA7E47EB2A2107CF00971697 /* Watched */ = { + isa = PBXGroup; + children = ( + AB706F8B278A4F6C0025A48A /* WatchedView.swift */, + AB706F91278A6E8C0025A48A /* WatchedReducer.swift */, + ); + path = Watched; + sourceTree = ""; + }; + EA7E47EC2A210C4300971697 /* History */ = { + isa = PBXGroup; + children = ( + AB706F94278A75D30025A48A /* HistoryView.swift */, + AB706F96278A77E20025A48A /* HistoryReducer.swift */, + ); + path = History; + sourceTree = ""; + }; EAEC870B2A1F74D500E1A97A /* EhSetting */ = { isa = PBXGroup; children = ( @@ -1761,7 +1793,7 @@ AB7BF31B27ABE028001865A3 /* NSManagedObjectModel+Resource.swift in Sources */, ABBC332826BE31AE0084A331 /* EhSettingView.swift in Sources */, AB7BF2C827A968F7001865A3 /* GalleryComment.swift in Sources */, - AB706F97278A77E20025A48A /* HistoryStore.swift in Sources */, + AB706F97278A77E20025A48A /* HistoryReducer.swift in Sources */, AB0929CC2781A0B000F107CA /* HapticsClient.swift in Sources */, ABD4032626B78E5A00001B8C /* GalleryThumbnailCell.swift in Sources */, AB0CFBCD27C1CC67004BD372 /* EhTagTranslationDatabaseModel.swift in Sources */, @@ -1786,7 +1818,7 @@ AB0CFBD527C24B3B004BD372 /* MarkdownUtil.swift in Sources */, ABF45AF625F3313D00ECB568 /* AppearanceSettingView.swift in Sources */, AB7BF2CE27AA3E58001865A3 /* AppUtil.swift in Sources */, - AB86AC1A2785C2B300E61E6A /* HomeStore.swift in Sources */, + AB86AC1A2785C2B300E61E6A /* HomeReducer.swift in Sources */, AB7BF2D427AA3F12001865A3 /* CookieUtil.swift in Sources */, AB7BF30A27ABDFF1001865A3 /* CoreDataMigrationStep.swift in Sources */, AB69CB8226B3DAF400699359 /* ControlPanel.swift in Sources */, @@ -1823,7 +1855,7 @@ ABCD2F0E25976B95008E5A20 /* Parser.swift in Sources */, ABF45AF725F3313D00ECB568 /* SettingView.swift in Sources */, AB1FA8FC27C5E0E50063EF55 /* TagDetail.swift in Sources */, - AB706F862789AD490025A48A /* ToplistsStore.swift in Sources */, + AB706F862789AD490025A48A /* ToplistsReducer.swift in Sources */, AB7BF2CC27A96A3C001865A3 /* GalleryTorrent.swift in Sources */, ABF45AEA25F3313D00ECB568 /* Placeholder.swift in Sources */, ABD4032826B7967F00001B8C /* CategoryView.swift in Sources */, @@ -1832,7 +1864,7 @@ ABBB2640279417EC007B6149 /* CommentsReducer.swift in Sources */, AB0929C027805A8200F107CA /* LoginReducer.swift in Sources */, ABBB2631278E6EF3007B6149 /* SearchView.swift in Sources */, - AB706F92278A6E8C0025A48A /* WatchedStore.swift in Sources */, + AB706F92278A6E8C0025A48A /* WatchedReducer.swift in Sources */, AB706F80278981370025A48A /* AlertKit_Extension.swift in Sources */, ABA9A6C228EC7BD000EE28DE /* Strings.swift in Sources */, AB58A5B22776B99000C0D285 /* AppStore.swift in Sources */, @@ -1845,7 +1877,7 @@ AB0929CE2781AADA00F107CA /* DatabaseClient.swift in Sources */, AB6DE897268822390087C579 /* LogsView.swift in Sources */, AB7BF31D27ABE028001865A3 /* FileManager+ApplicationSupport.swift in Sources */, - AB706F7B278937500025A48A /* FrontpageStore.swift in Sources */, + AB706F7B278937500025A48A /* FrontpageReducer.swift in Sources */, AB86ABF72782DDE600E61E6A /* FileClient.swift in Sources */, AB7B29F226AC471E00EE1F14 /* Model5toModel6MigrationPolicy.swift in Sources */, AB706F7927890A6C0025A48A /* AppRouteStore.swift in Sources */, @@ -1853,7 +1885,7 @@ AB706FA1278BCEC60025A48A /* DetailView.swift in Sources */, ABE9401526FF158D0085E158 /* QuickSearchView.swift in Sources */, AB706F9B278AC5A30025A48A /* SearchRootView.swift in Sources */, - AB706F8A278A4CC50025A48A /* PopularStore.swift in Sources */, + AB706F8A278A4CC50025A48A /* PopularReducer.swift in Sources */, ABD49D64277C7AD5003D1A07 /* TabBarStore.swift in Sources */, ABF45AF025F3313D00ECB568 /* CommentsView.swift in Sources */, ABC8356527B36E550091DCDB /* AutoPlayHandler.swift in Sources */, diff --git a/EhPanda/DataFlow/AppStore.swift b/EhPanda/DataFlow/AppStore.swift index 84851807..be345f44 100644 --- a/EhPanda/DataFlow/AppStore.swift +++ b/EhPanda/DataFlow/AppStore.swift @@ -13,7 +13,7 @@ struct AppState: Equatable { var appRouteState = AppRouteState() var appLockState = AppLockState() var tabBarState = TabBarState() - var homeState = HomeState() + var homeState = HomeReducer.State() var favoritesState = FavoritesReducer.State() var searchRootState = SearchRootReducer.State() var settingState = SettingReducer.State() @@ -29,7 +29,7 @@ enum AppAction: BindableAction { case tabBar(TabBarAction) - case home(HomeAction) + case home(HomeReducer.Action) case favorites(FavoritesReducer.Action) case searchRoot(SearchRootReducer.Action) case setting(SettingReducer.Action) @@ -258,27 +258,27 @@ let appReducer = Reducer.combine( deviceClient: $0.deviceClient ) } - ), - homeReducer.pullback( - state: \.homeState, - action: /AppAction.home, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - libraryClient: $0.libraryClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } ) // , +// homeReducer.pullback( +// state: \.homeState, +// action: /AppAction.home, +// environment: { +// .init( +// urlClient: $0.urlClient, +// fileClient: $0.fileClient, +// imageClient: $0.imageClient, +// deviceClient: $0.deviceClient, +// hapticsClient: $0.hapticsClient, +// libraryClient: $0.libraryClient, +// cookieClient: $0.cookieClient, +// databaseClient: $0.databaseClient, +// clipboardClient: $0.clipboardClient, +// appDelegateClient: $0.appDelegateClient, +// uiApplicationClient: $0.uiApplicationClient +// ) +// } +// ), // favoritesReducer.pullback( // state: \.favoritesState, // action: /AppAction.favorites, diff --git a/EhPanda/View/Home/DataFlow/FrontpageStore.swift b/EhPanda/View/Home/DataFlow/FrontpageStore.swift deleted file mode 100644 index dd034f23..00000000 --- a/EhPanda/View/Home/DataFlow/FrontpageStore.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// FrontpageStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/08. -// - -import ComposableArchitecture - -struct FrontpageState: Equatable { - enum Route: Equatable { - case filters - case detail(String) - } - struct CancelID: Hashable { - let id = String(describing: FrontpageState.self) - } - - init() { - _detailState = .init(.init()) - } - - @BindingState var route: Route? - @BindingState var keyword = "" - - var filteredGalleries: [Gallery] { - guard !keyword.isEmpty else { return galleries } - return galleries.filter({ $0.title.caseInsensitiveContains(keyword) }) - } - var galleries = [Gallery]() - var pageNumber = PageNumber() - var loadingState: LoadingState = .idle - var footerLoadingState: LoadingState = .idle - - var filtersState = FiltersReducer.State() - @Heap var detailState: DetailReducer.State! - - mutating func insertGalleries(_ galleries: [Gallery]) { - galleries.forEach { gallery in - if !self.galleries.contains(gallery) { - self.galleries.append(gallery) - } - } - } -} - -enum FrontpageAction: BindableAction { - case binding(BindingAction) - case setNavigation(FrontpageState.Route?) - case clearSubStates - - case teardown - case fetchGalleries - case fetchGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) - case fetchMoreGalleries - case fetchMoreGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) - - case filters(FiltersReducer.Action) - case detail(DetailReducer.Action) -} - -struct FrontpageEnvironment { - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let uiApplicationClient: UIApplicationClient -} - -let frontpageReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .clearSubStates: - state.detailState = .init() - state.filtersState = .init() - return .init(value: .detail(.teardown)) - - case .teardown: - return .cancel(id: FrontpageState.CancelID()) - - case .fetchGalleries: - guard state.loadingState != .loading else { return .none } - state.loadingState = .loading - state.pageNumber.resetPages() - let filter = environment.databaseClient.fetchFilterSynchronously(range: .global) - return FrontpageGalleriesRequest(filter: filter).effect - .map(FrontpageAction.fetchGalleriesDone) - .cancellable(id: FrontpageState.CancelID()) - - case .fetchGalleriesDone(let result): - state.loadingState = .idle - switch result { - case .success(let (pageNumber, galleries)): - guard !galleries.isEmpty else { - state.loadingState = .failed(.notFound) - guard pageNumber.hasNextPage() else { return .none } - return .init(value: .fetchMoreGalleries) - } - state.pageNumber = pageNumber - state.galleries = galleries - return environment.databaseClient.cacheGalleries(galleries).fireAndForget() - case .failure(let error): - state.loadingState = .failed(error) - } - return .none - - case .fetchMoreGalleries: - let pageNumber = state.pageNumber - guard pageNumber.hasNextPage(), - state.footerLoadingState != .loading, - let lastID = state.galleries.last?.id - else { return .none } - state.footerLoadingState = .loading - let filter = environment.databaseClient.fetchFilterSynchronously(range: .global) - return MoreFrontpageGalleriesRequest(filter: filter, lastID: lastID).effect - .map(FrontpageAction.fetchMoreGalleriesDone) - .cancellable(id: FrontpageState.CancelID()) - - case .fetchMoreGalleriesDone(let result): - state.footerLoadingState = .idle - switch result { - case .success(let (pageNumber, galleries)): - state.pageNumber = pageNumber - state.insertGalleries(galleries) - - var effects: [EffectTask] = [ - environment.databaseClient.cacheGalleries(galleries).fireAndForget() - ] - if galleries.isEmpty, pageNumber.hasNextPage() { - effects.append(.init(value: .fetchMoreGalleries)) - } else if !galleries.isEmpty { - state.loadingState = .idle - } - return .merge(effects) - - case .failure(let error): - state.footerLoadingState = .failed(error) - } - return .none - - case .filters: - return .none - - case .detail: - return .none - } - } - .haptics( - unwrapping: \.route, - case: /FrontpageState.Route.filters, - hapticsClient: \.hapticsClient - ) - .binding() -// , -// filtersReducer.pullback( -// state: \.filtersState, -// action: /FrontpageAction.filters, -// environment: { -// .init( -// databaseClient: $0.databaseClient -// ) -// } -// ), -// detailReducer.pullback( -// state: \.detailState, -// action: /FrontpageAction.detail, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ) -) diff --git a/EhPanda/View/Home/DataFlow/HistoryStore.swift b/EhPanda/View/Home/DataFlow/HistoryStore.swift deleted file mode 100644 index 874a330a..00000000 --- a/EhPanda/View/Home/DataFlow/HistoryStore.swift +++ /dev/null @@ -1,122 +0,0 @@ -// -// HistoryStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/09. -// - -import Foundation -import ComposableArchitecture - -struct HistoryState: Equatable { - enum Route: Equatable { - case detail(String) - case clearHistory - } - - init() { - _detailState = .init(.init()) - } - - @BindingState var route: Route? - @BindingState var keyword = "" - @BindingState var clearDialogPresented = false - - var filteredGalleries: [Gallery] { - guard !keyword.isEmpty else { return galleries } - return galleries.filter({ $0.title.caseInsensitiveContains(keyword) }) - } - var galleries = [Gallery]() - var loadingState: LoadingState = .idle - - @Heap var detailState: DetailReducer.State! -} - -enum HistoryAction: BindableAction { - case binding(BindingAction) - case setNavigation(HistoryState.Route?) - case clearSubStates - case clearHistoryGalleries - - case fetchGalleries - case fetchGalleriesDone([Gallery]) - - case detail(DetailReducer.Action) -} - -struct HistoryEnvironment { - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let uiApplicationClient: UIApplicationClient -} - -let historyReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .clearSubStates: - state.detailState = .init() - return .init(value: .detail(.teardown)) - - case .clearHistoryGalleries: - return .merge( - environment.databaseClient.clearHistoryGalleries().fireAndForget(), - .init(value: .fetchGalleries) - .delay(for: .milliseconds(200), scheduler: DispatchQueue.main).eraseToEffect() - ) - - case .fetchGalleries: - guard state.loadingState != .loading else { return .none } - state.loadingState = .loading - return environment.databaseClient.fetchHistoryGalleries().map(HistoryAction.fetchGalleriesDone) - - case .fetchGalleriesDone(let galleries): - state.loadingState = .idle - if galleries.isEmpty { - state.loadingState = .failed(.notFound) - } else { - state.galleries = galleries - } - return .none - - case .detail: - return .none - } - } - .binding() -// , -// detailReducer.pullback( -// state: \.detailState, -// action: /HistoryAction.detail, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ) -) diff --git a/EhPanda/View/Home/DataFlow/HomeStore.swift b/EhPanda/View/Home/DataFlow/HomeStore.swift deleted file mode 100644 index bdd0631f..00000000 --- a/EhPanda/View/Home/DataFlow/HomeStore.swift +++ /dev/null @@ -1,368 +0,0 @@ -// -// HomeStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/05. -// - -import SwiftUI -import Kingfisher -import UIImageColors -import ComposableArchitecture - -struct HomeState: Equatable { - enum Route: Equatable, Hashable { - case detail(String) - case misc(HomeMiscGridType) - case section(HomeSectionType) - } - - init() { - _detailState = .init(.init()) - } - - @BindingState var route: Route? - @BindingState var cardPageIndex = 1 - @BindingState var currentCardID = "" - var allowsCardHitTesting = true - var rawCardColors = [String: [Color]]() - var cardColors: [Color] { - rawCardColors[currentCardID] ?? [.clear] - } - - var popularGalleries = [Gallery]() - var popularLoadingState: LoadingState = .idle - var frontpageGalleries = [Gallery]() - var frontpageLoadingState: LoadingState = .idle - var toplistsGalleries = [Int: [Gallery]]() - var toplistsLoadingState = [Int: LoadingState]() - - var frontpageState = FrontpageState() - var toplistsState = ToplistsState() - var popularState = PopularState() - var watchedState = WatchedState() - var historyState = HistoryState() - @Heap var detailState: DetailReducer.State! - - mutating func setPopularGalleries(_ galleries: [Gallery]) { - let sortedGalleries = galleries.sorted { lhs, rhs in - lhs.title.count > rhs.title.count - } - var trimmedGalleries = Array(sortedGalleries.prefix(min(sortedGalleries.count, 10))) - .removeDuplicates(by: \.trimmedTitle) - if trimmedGalleries.count >= 6 { - trimmedGalleries = Array(trimmedGalleries.prefix(6)) - } - trimmedGalleries.shuffle() - popularGalleries = trimmedGalleries - currentCardID = trimmedGalleries[cardPageIndex].gid - } - mutating func setFrontpageGalleries(_ galleries: [Gallery]) { - frontpageGalleries = Array(galleries.prefix(min(galleries.count, 25))) - .removeDuplicates(by: \.trimmedTitle) - } -} - -enum HomeAction: BindableAction { - case binding(BindingAction) - case setNavigation(HomeState.Route?) - case clearSubStates - case setAllowsCardHitTesting(Bool) - case analyzeImageColors(String, RetrieveImageResult) - case analyzeImageColorsDone(String, UIImageColors?) - - case fetchAllGalleries - case fetchAllToplistsGalleries - case fetchPopularGalleries - case fetchPopularGalleriesDone(Result<[Gallery], AppError>) - case fetchFrontpageGalleries - case fetchFrontpageGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) - case fetchToplistsGalleries(Int, Int? = nil) - case fetchToplistsGalleriesDone(Int, Result<(PageNumber, [Gallery]), AppError>) - - case frontpage(FrontpageAction) - case toplists(ToplistsAction) - case popular(PopularAction) - case watched(WatchedAction) - case history(HistoryAction) - case detail(DetailReducer.Action) -} - -struct HomeEnvironment { - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let libraryClient: LibraryClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let uiApplicationClient: UIApplicationClient -} - -let homeReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding(\.$cardPageIndex): - guard state.cardPageIndex < state.popularGalleries.count else { return .none } - state.currentCardID = state.popularGalleries[state.cardPageIndex].gid - state.allowsCardHitTesting = false - return .init(value: .setAllowsCardHitTesting(true)) - .delay(for: .milliseconds(300), scheduler: DispatchQueue.main) - .eraseToEffect() - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .clearSubStates: - state.frontpageState = .init() - state.toplistsState = .init() - state.popularState = .init() - state.watchedState = .init() - state.historyState = .init() - state.detailState = .init() - return .merge( - .init(value: .frontpage(.teardown)), - .init(value: .toplists(.teardown)), - .init(value: .popular(.teardown)), - .init(value: .watched(.teardown)), - .init(value: .detail(.teardown)) - ) - - case .setAllowsCardHitTesting(let isAllowed): - state.allowsCardHitTesting = isAllowed - return .none - - case .fetchAllGalleries: - return .merge( - .init(value: .fetchPopularGalleries), - .init(value: .fetchFrontpageGalleries), - .init(value: .fetchAllToplistsGalleries) - ) - - case .fetchAllToplistsGalleries: - return .merge( - ToplistsType.allCases.map({ HomeAction.fetchToplistsGalleries($0.categoryIndex) }) - .map(EffectTask.init) - ) - - case .fetchPopularGalleries: - guard state.popularLoadingState != .loading else { return .none } - state.popularLoadingState = .loading - state.rawCardColors = [String: [Color]]() - let filter = environment.databaseClient.fetchFilterSynchronously(range: .global) - return PopularGalleriesRequest(filter: filter) - .effect.map(HomeAction.fetchPopularGalleriesDone) - - case .fetchPopularGalleriesDone(let result): - state.popularLoadingState = .idle - switch result { - case .success(let galleries): - guard !galleries.isEmpty else { - state.popularLoadingState = .failed(.notFound) - return .none - } - state.setPopularGalleries(galleries) - return environment.databaseClient.cacheGalleries(galleries).fireAndForget() - case .failure(let error): - state.popularLoadingState = .failed(error) - } - return .none - - case .fetchFrontpageGalleries: - guard state.frontpageLoadingState != .loading else { return .none } - state.frontpageLoadingState = .loading - let filter = environment.databaseClient.fetchFilterSynchronously(range: .global) - return FrontpageGalleriesRequest(filter: filter) - .effect.map(HomeAction.fetchFrontpageGalleriesDone) - - case .fetchFrontpageGalleriesDone(let result): - state.frontpageLoadingState = .idle - switch result { - case .success(let (_, galleries)): - guard !galleries.isEmpty else { - state.frontpageLoadingState = .failed(.notFound) - return .none - } - state.setFrontpageGalleries(galleries) - return environment.databaseClient.cacheGalleries(galleries).fireAndForget() - case .failure(let error): - state.frontpageLoadingState = .failed(error) - } - return .none - - case .fetchToplistsGalleries(let index, let pageNum): - guard state.toplistsLoadingState[index] != .loading else { return .none } - state.toplistsLoadingState[index] = .loading - return ToplistsGalleriesRequest(catIndex: index, pageNum: pageNum) - .effect.map({ HomeAction.fetchToplistsGalleriesDone(index, $0) }) - - case .fetchToplistsGalleriesDone(let index, let result): - state.toplistsLoadingState[index] = .idle - switch result { - case .success(let (_, galleries)): - guard !galleries.isEmpty else { - state.toplistsLoadingState[index] = .failed(.notFound) - return .none - } - state.toplistsGalleries[index] = galleries - return environment.databaseClient.cacheGalleries(galleries).fireAndForget() - case .failure(let error): - state.toplistsLoadingState[index] = .failed(error) - } - return .none - - case .analyzeImageColors(let gid, let result): - guard !state.rawCardColors.keys.contains(gid) else { return .none } - return environment.libraryClient.analyzeImageColors(result.image) - .map({ HomeAction.analyzeImageColorsDone(gid, $0) }) - - case .analyzeImageColorsDone(let gid, let colors): - if let colors = colors { - state.rawCardColors[gid] = [ - colors.primary, colors.secondary, - colors.detail, colors.background - ] - .map(Color.init) - } - return .none - - case .frontpage: - return .none - - case .toplists: - return .none - - case .popular: - return .none - - case .watched: - return .none - - case .history: - return .none - - case .detail: - return .none - } - } - .binding(), - frontpageReducer.pullback( - state: \.frontpageState, - action: /HomeAction.frontpage, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ), - toplistsReducer.pullback( - state: \.toplistsState, - action: /HomeAction.toplists, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ), - popularReducer.pullback( - state: \.popularState, - action: /HomeAction.popular, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ), - watchedReducer.pullback( - state: \.watchedState, - action: /HomeAction.watched, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ), - historyReducer.pullback( - state: \.historyState, - action: /HomeAction.history, - environment: { - .init( - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - hapticsClient: $0.hapticsClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - uiApplicationClient: $0.uiApplicationClient - ) - } - ) -// , -// detailReducer.pullback( -// state: \.detailState, -// action: /HomeAction.detail, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ) -) diff --git a/EhPanda/View/Home/DataFlow/PopularStore.swift b/EhPanda/View/Home/DataFlow/PopularStore.swift deleted file mode 100644 index f54be131..00000000 --- a/EhPanda/View/Home/DataFlow/PopularStore.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// PopularStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/09. -// - -import ComposableArchitecture - -struct PopularState: Equatable { - enum Route: Equatable { - case filters - case detail(String) - } - struct CancelID: Hashable { - let id = String(describing: PopularState.self) - } - - init() { - _detailState = .init(.init()) - } - - @BindingState var route: Route? - @BindingState var keyword = "" - - var filteredGalleries: [Gallery] { - guard !keyword.isEmpty else { return galleries } - return galleries.filter({ $0.title.caseInsensitiveContains(keyword) }) - } - var galleries = [Gallery]() - var loadingState: LoadingState = .idle - - var filtersState = FiltersReducer.State() - @Heap var detailState: DetailReducer.State! -} - -enum PopularAction: BindableAction { - case binding(BindingAction) - case setNavigation(PopularState.Route?) - case clearSubStates - - case teardown - case fetchGalleries - case fetchGalleriesDone(Result<[Gallery], AppError>) - - case filters(FiltersReducer.Action) - case detail(DetailReducer.Action) -} - -struct PopularEnvironment { - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let uiApplicationClient: UIApplicationClient -} - -let popularReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .clearSubStates: - state.detailState = .init() - state.filtersState = .init() - return .init(value: .detail(.teardown)) - - case .teardown: - return .cancel(id: PopularState.CancelID()) - - case .fetchGalleries: - guard state.loadingState != .loading else { return .none } - state.loadingState = .loading - let filter = environment.databaseClient.fetchFilterSynchronously(range: .global) - return PopularGalleriesRequest(filter: filter) - .effect.map(PopularAction.fetchGalleriesDone).cancellable(id: PopularState.CancelID()) - - case .fetchGalleriesDone(let result): - state.loadingState = .idle - switch result { - case .success(let galleries): - guard !galleries.isEmpty else { - state.loadingState = .failed(.notFound) - return .none - } - state.galleries = galleries - return environment.databaseClient.cacheGalleries(galleries).fireAndForget() - case .failure(let error): - state.loadingState = .failed(error) - } - return .none - - case .filters: - return .none - - case .detail: - return .none - } - } - .haptics( - unwrapping: \.route, - case: /PopularState.Route.filters, - hapticsClient: \.hapticsClient - ) - .binding() -// , -// filtersReducer.pullback( -// state: \.filtersState, -// action: /PopularAction.filters, -// environment: { -// .init( -// databaseClient: $0.databaseClient -// ) -// } -// ), -// detailReducer.pullback( -// state: \.detailState, -// action: /PopularAction.detail, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ) -) diff --git a/EhPanda/View/Home/DataFlow/ToplistsStore.swift b/EhPanda/View/Home/DataFlow/ToplistsStore.swift deleted file mode 100644 index 4e516539..00000000 --- a/EhPanda/View/Home/DataFlow/ToplistsStore.swift +++ /dev/null @@ -1,230 +0,0 @@ -// -// ToplistsStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/08. -// - -import ComposableArchitecture - -struct ToplistsState: Equatable { - enum Route: Equatable { - case detail(String) - } - struct CancelID: Hashable { - let id = String(describing: ToplistsState.self) - } - - init() { - _detailState = .init(.init()) - } - - @BindingState var route: Route? - @BindingState var keyword = "" - @BindingState var jumpPageIndex = "" - @BindingState var jumpPageAlertFocused = false - @BindingState var jumpPageAlertPresented = false - - var type: ToplistsType = .yesterday - - var filteredGalleries: [Gallery]? { - guard !keyword.isEmpty else { return galleries } - return galleries?.filter({ $0.title.caseInsensitiveContains(keyword) }) - } - - var rawGalleries = [ToplistsType: [Gallery]]() - var rawPageNumber = [ToplistsType: PageNumber]() - var rawLoadingState = [ToplistsType: LoadingState]() - var rawFooterLoadingState = [ToplistsType: LoadingState]() - - var galleries: [Gallery]? { - rawGalleries[type] - } - var pageNumber: PageNumber? { - rawPageNumber[type] - } - var loadingState: LoadingState? { - rawLoadingState[type] - } - var footerLoadingState: LoadingState? { - rawFooterLoadingState[type] - } - - @Heap var detailState: DetailReducer.State! - - mutating func insertGalleries(type: ToplistsType, galleries: [Gallery]) { - galleries.forEach { gallery in - if rawGalleries[type]?.contains(gallery) == false { - rawGalleries[type]?.append(gallery) - } - } - } -} - -enum ToplistsAction: BindableAction { - case binding(BindingAction) - case setNavigation(ToplistsState.Route?) - case setToplistsType(ToplistsType) - case clearSubStates - - case performJumpPage - case presentJumpPageAlert - case setJumpPageAlertFocused(Bool) - - case teardown - case fetchGalleries(Int? = nil) - case fetchGalleriesDone(ToplistsType, Result<(PageNumber, [Gallery]), AppError>) - case fetchMoreGalleries - case fetchMoreGalleriesDone(ToplistsType, Result<(PageNumber, [Gallery]), AppError>) - - case detail(DetailReducer.Action) -} - -struct ToplistsEnvironment { - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let uiApplicationClient: UIApplicationClient -} - -let toplistsReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding(\.$jumpPageAlertPresented): - if !state.jumpPageAlertPresented { - state.jumpPageAlertFocused = false - } - return .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .setToplistsType(let type): - state.type = type - guard state.galleries?.isEmpty != false else { return .none } - return .init(value: ToplistsAction.fetchGalleries()) - - case .clearSubStates: - state.detailState = .init() - return .init(value: .detail(.teardown)) - - case .performJumpPage: - guard let index = Int(state.jumpPageIndex), - let pageNumber = state.pageNumber, - index > 0, index <= pageNumber.maximum + 1 else { - return environment.hapticsClient.generateNotificationFeedback(.error).fireAndForget() - } - return .init(value: .fetchGalleries(index - 1)) - - case .presentJumpPageAlert: - state.jumpPageAlertPresented = true - return environment.hapticsClient.generateFeedback(.light).fireAndForget() - - case .setJumpPageAlertFocused(let isFocused): - state.jumpPageAlertFocused = isFocused - return .none - - case .teardown: - return .cancel(id: ToplistsState.CancelID()) - - case .fetchGalleries(let pageNum): - guard state.loadingState != .loading else { return .none } - state.rawLoadingState[state.type] = .loading - if state.pageNumber == nil { - state.rawPageNumber[state.type] = PageNumber() - } else { - state.rawPageNumber[state.type]?.resetPages() - } - return ToplistsGalleriesRequest(catIndex: state.type.categoryIndex, pageNum: pageNum) - .effect.map({ [type = state.type] in ToplistsAction.fetchGalleriesDone(type, $0) }) - .cancellable(id: ToplistsState.CancelID()) - - case .fetchGalleriesDone(let type, let result): - state.rawLoadingState[type] = .idle - switch result { - case .success(let (pageNumber, galleries)): - guard !galleries.isEmpty else { - state.rawLoadingState[type] = .failed(.notFound) - guard pageNumber.hasNextPage() else { return .none } - return .init(value: .fetchMoreGalleries) - } - state.rawPageNumber[type] = pageNumber - state.rawGalleries[type] = galleries - return environment.databaseClient.cacheGalleries(galleries).fireAndForget() - case .failure(let error): - state.rawLoadingState[type] = .failed(error) - } - return .none - - case .fetchMoreGalleries: - let pageNumber = state.pageNumber ?? .init() - guard pageNumber.hasNextPage(), - state.footerLoadingState != .loading, - let lastID = state.rawGalleries[state.type]?.last?.id - else { return .none } - state.rawFooterLoadingState[state.type] = .loading - let pageNum = pageNumber.current + 1 - return MoreToplistsGalleriesRequest(catIndex: state.type.categoryIndex, pageNum: pageNum) - .effect.map({ [type = state.type] in ToplistsAction.fetchMoreGalleriesDone(type, $0) }) - .cancellable(id: ToplistsState.CancelID()) - - case .fetchMoreGalleriesDone(let type, let result): - state.rawFooterLoadingState[type] = .idle - switch result { - case .success(let (pageNumber, galleries)): - state.rawPageNumber[type] = pageNumber - state.insertGalleries(type: type, galleries: galleries) - - var effects: [EffectTask] = [ - environment.databaseClient.cacheGalleries(galleries).fireAndForget() - ] - if galleries.isEmpty, pageNumber.hasNextPage() { - effects.append(.init(value: .fetchMoreGalleries)) - } else if !galleries.isEmpty { - state.rawLoadingState[type] = .idle - } - return .merge(effects) - - case .failure(let error): - state.rawFooterLoadingState[type] = .failed(error) - } - return .none - - case .detail: - return .none - } - } - .binding() -// , -// detailReducer.pullback( -// state: \.detailState, -// action: /ToplistsAction.detail, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ) -) diff --git a/EhPanda/View/Home/DataFlow/WatchedStore.swift b/EhPanda/View/Home/DataFlow/WatchedStore.swift deleted file mode 100644 index 64586728..00000000 --- a/EhPanda/View/Home/DataFlow/WatchedStore.swift +++ /dev/null @@ -1,223 +0,0 @@ -// -// WatchedStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/09. -// - -import ComposableArchitecture - -struct WatchedState: Equatable { - enum Route: Equatable { - case filters - case quickSearch - case detail(String) - } - struct CancelID: Hashable { - let id = String(describing: WatchedState.self) - } - - init() { - _detailState = .init(.init()) - } - - @BindingState var route: Route? - @BindingState var keyword = "" - - var galleries = [Gallery]() - var pageNumber = PageNumber() - var loadingState: LoadingState = .idle - var footerLoadingState: LoadingState = .idle - - var filtersState = FiltersReducer.State() - var quickSearchState = QuickSearchReducer.State() - @Heap var detailState: DetailReducer.State! - - mutating func insertGalleries(_ galleries: [Gallery]) { - galleries.forEach { gallery in - if !self.galleries.contains(gallery) { - self.galleries.append(gallery) - } - } - } -} - -enum WatchedAction: BindableAction { - case binding(BindingAction) - case setNavigation(WatchedState.Route?) - case clearSubStates - case onNotLoginViewButtonTapped - - case teardown - case fetchGalleries(String? = nil) - case fetchGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) - case fetchMoreGalleries - case fetchMoreGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) - - case filters(FiltersReducer.Action) - case detail(DetailReducer.Action) - case quickSearch(QuickSearchReducer.Action) -} - -struct WatchedEnvironment { - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let hapticsClient: HapticsClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let uiApplicationClient: UIApplicationClient -} - -let watchedReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .clearSubStates: - state.detailState = .init() - state.filtersState = .init() - state.quickSearchState = .init() - return .merge( - .init(value: .detail(.teardown)), - .init(value: .quickSearch(.teardown)) - ) - - case .onNotLoginViewButtonTapped: - return .none - - case .teardown: - return .cancel(id: WatchedState.CancelID()) - - case .fetchGalleries(let keyword): - guard state.loadingState != .loading else { return .none } - if let keyword = keyword { - state.keyword = keyword - } - state.loadingState = .loading - state.pageNumber.resetPages() - let filter = environment.databaseClient.fetchFilterSynchronously(range: .watched) - return WatchedGalleriesRequest(filter: filter, keyword: state.keyword) - .effect.map(WatchedAction.fetchGalleriesDone).cancellable(id: WatchedState.CancelID()) - - case .fetchGalleriesDone(let result): - state.loadingState = .idle - switch result { - case .success(let (pageNumber, galleries)): - guard !galleries.isEmpty else { - state.loadingState = .failed(.notFound) - guard pageNumber.hasNextPage() else { return .none } - return .init(value: .fetchMoreGalleries) - } - state.pageNumber = pageNumber - state.galleries = galleries - return environment.databaseClient.cacheGalleries(galleries).fireAndForget() - case .failure(let error): - state.loadingState = .failed(error) - } - return .none - - case .fetchMoreGalleries: - let pageNumber = state.pageNumber - guard pageNumber.hasNextPage(), - state.footerLoadingState != .loading, - let lastID = state.galleries.last?.id - else { return .none } - state.footerLoadingState = .loading - let filter = environment.databaseClient.fetchFilterSynchronously(range: .watched) - return MoreWatchedGalleriesRequest(filter: filter, lastID: lastID, keyword: state.keyword).effect - .map(WatchedAction.fetchMoreGalleriesDone) - .cancellable(id: WatchedState.CancelID()) - - case .fetchMoreGalleriesDone(let result): - state.footerLoadingState = .idle - switch result { - case .success(let (pageNumber, galleries)): - state.pageNumber = pageNumber - state.insertGalleries(galleries) - - var effects: [EffectTask] = [ - environment.databaseClient.cacheGalleries(galleries).fireAndForget() - ] - if galleries.isEmpty, pageNumber.hasNextPage() { - effects.append(.init(value: .fetchMoreGalleries)) - } else if !galleries.isEmpty { - state.loadingState = .idle - } - return .merge(effects) - - case .failure(let error): - state.footerLoadingState = .failed(error) - } - return .none - - case .quickSearch: - return .none - - case .filters: - return .none - - case .detail: - return .none - } - } - .haptics( - unwrapping: \.route, - case: /WatchedState.Route.quickSearch, - hapticsClient: \.hapticsClient - ) - .haptics( - unwrapping: \.route, - case: /WatchedState.Route.filters, - hapticsClient: \.hapticsClient - ) - .binding() -// , -// filtersReducer.pullback( -// state: \.filtersState, -// action: /WatchedAction.filters, -// environment: { -// .init( -// databaseClient: $0.databaseClient -// ) -// } -// ), -// quickSearchReducer.pullback( -// state: \.quickSearchState, -// action: /WatchedAction.quickSearch, -// environment: { -// .init( -// databaseClient: $0.databaseClient -// ) -// } -// ), -// detailReducer.pullback( -// state: \.detailState, -// action: /WatchedAction.detail, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ) -) diff --git a/EhPanda/View/Home/Frontpage/FrontpageReducer.swift b/EhPanda/View/Home/Frontpage/FrontpageReducer.swift new file mode 100644 index 00000000..99bd66ca --- /dev/null +++ b/EhPanda/View/Home/Frontpage/FrontpageReducer.swift @@ -0,0 +1,166 @@ +// +// FrontpageReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/08. +// + +import ComposableArchitecture + +struct FrontpageReducer: ReducerProtocol { + enum Route: Equatable { + case filters + case detail(String) + } + + struct CancelID: Hashable { + let id = String(describing: FrontpageReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var keyword = "" + + var filteredGalleries: [Gallery] { + guard !keyword.isEmpty else { return galleries } + return galleries.filter({ $0.title.caseInsensitiveContains(keyword) }) + } + var galleries = [Gallery]() + var pageNumber = PageNumber() + var loadingState: LoadingState = .idle + var footerLoadingState: LoadingState = .idle + + var filtersState = FiltersReducer.State() + @Heap var detailState: DetailReducer.State! + + init() { + _detailState = .init(.init()) + } + + mutating func insertGalleries(_ galleries: [Gallery]) { + galleries.forEach { gallery in + if !self.galleries.contains(gallery) { + self.galleries.append(gallery) + } + } + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + + case teardown + case fetchGalleries + case fetchGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) + case fetchMoreGalleries + case fetchMoreGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) + + case filters(FiltersReducer.Action) + case detail(DetailReducer.Action) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .clearSubStates: + state.detailState = .init() + state.filtersState = .init() + return .init(value: .detail(.teardown)) + + case .teardown: + return .cancel(id: CancelID()) + + case .fetchGalleries: + guard state.loadingState != .loading else { return .none } + state.loadingState = .loading + state.pageNumber.resetPages() + let filter = databaseClient.fetchFilterSynchronously(range: .global) + return FrontpageGalleriesRequest(filter: filter).effect + .map(Action.fetchGalleriesDone) + .cancellable(id: CancelID()) + + case .fetchGalleriesDone(let result): + state.loadingState = .idle + switch result { + case .success(let (pageNumber, galleries)): + guard !galleries.isEmpty else { + state.loadingState = .failed(.notFound) + guard pageNumber.hasNextPage() else { return .none } + return .init(value: .fetchMoreGalleries) + } + state.pageNumber = pageNumber + state.galleries = galleries + return databaseClient.cacheGalleries(galleries).fireAndForget() + case .failure(let error): + state.loadingState = .failed(error) + } + return .none + + case .fetchMoreGalleries: + let pageNumber = state.pageNumber + guard pageNumber.hasNextPage(), + state.footerLoadingState != .loading, + let lastID = state.galleries.last?.id + else { return .none } + state.footerLoadingState = .loading + let filter = databaseClient.fetchFilterSynchronously(range: .global) + return MoreFrontpageGalleriesRequest(filter: filter, lastID: lastID).effect + .map(Action.fetchMoreGalleriesDone) + .cancellable(id: CancelID()) + + case .fetchMoreGalleriesDone(let result): + state.footerLoadingState = .idle + switch result { + case .success(let (pageNumber, galleries)): + state.pageNumber = pageNumber + state.insertGalleries(galleries) + + var effects: [EffectTask] = [ + databaseClient.cacheGalleries(galleries).fireAndForget() + ] + if galleries.isEmpty, pageNumber.hasNextPage() { + effects.append(.init(value: .fetchMoreGalleries)) + } else if !galleries.isEmpty { + state.loadingState = .idle + } + return .merge(effects) + + case .failure(let error): + state.footerLoadingState = .failed(error) + } + return .none + + case .filters: + return .none + + case .detail: + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.filters, + hapticsClient: hapticsClient + ) + + Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) + Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + + BindingReducer() + } +} diff --git a/EhPanda/View/Home/FrontpageView.swift b/EhPanda/View/Home/Frontpage/FrontpageView.swift similarity index 77% rename from EhPanda/View/Home/FrontpageView.swift rename to EhPanda/View/Home/Frontpage/FrontpageView.swift index 92e8b085..b82e137b 100644 --- a/EhPanda/View/Home/FrontpageView.swift +++ b/EhPanda/View/Home/Frontpage/FrontpageView.swift @@ -10,15 +10,15 @@ import AlertKit import ComposableArchitecture struct FrontpageView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double private let tagTranslator: TagTranslator init( - store: Store, + store: StoreOf, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store @@ -45,20 +45,20 @@ struct FrontpageView: View { ) .sheet( unwrapping: viewStore.binding(\.$route), - case: /FrontpageState.Route.detail, + case: /FrontpageReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: FrontpageAction.detail), + store: store.scope(state: \.detailState, action: FrontpageReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /FrontpageState.Route.filters) { _ in - FiltersView(store: store.scope(state: \.filtersState, action: FrontpageAction.filters)) + .sheet(unwrapping: viewStore.binding(\.$route), case: /FrontpageReducer.Route.filters) { _ in + FiltersView(store: store.scope(state: \.filtersState, action: FrontpageReducer.Action.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } .searchable(text: viewStore.binding(\.$keyword), prompt: L10n.Localizable.Searchable.Prompt.filter) @@ -76,9 +76,9 @@ struct FrontpageView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /FrontpageState.Route.detail) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /FrontpageReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: FrontpageAction.detail), + store: store.scope(state: \.detailState, action: FrontpageReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -100,19 +100,7 @@ struct FrontpageView_Previews: PreviewProvider { FrontpageView( store: .init( initialState: .init(), - reducer: frontpageReducer, - environment: FrontpageEnvironment( - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - uiApplicationClient: .live - ) + reducer: FrontpageReducer() ), user: .init(), setting: .constant(.init()), diff --git a/EhPanda/View/Home/History/HistoryReducer.swift b/EhPanda/View/Home/History/HistoryReducer.swift new file mode 100644 index 00000000..55bc8f02 --- /dev/null +++ b/EhPanda/View/Home/History/HistoryReducer.swift @@ -0,0 +1,98 @@ +// +// HistoryReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/09. +// + +import Foundation +import ComposableArchitecture + +struct HistoryReducer: ReducerProtocol { + enum Route: Equatable { + case detail(String) + case clearHistory + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var keyword = "" + @BindingState var clearDialogPresented = false + + var filteredGalleries: [Gallery] { + guard !keyword.isEmpty else { return galleries } + return galleries.filter({ $0.title.caseInsensitiveContains(keyword) }) + } + var galleries = [Gallery]() + var loadingState: LoadingState = .idle + + @Heap var detailState: DetailReducer.State! + + init() { + _detailState = .init(.init()) + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + case clearHistoryGalleries + + case fetchGalleries + case fetchGalleriesDone([Gallery]) + + case detail(DetailReducer.Action) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .clearSubStates: + state.detailState = .init() + return .init(value: .detail(.teardown)) + + case .clearHistoryGalleries: + return .merge( + databaseClient.clearHistoryGalleries().fireAndForget(), + .init(value: .fetchGalleries) + .delay(for: .milliseconds(200), scheduler: DispatchQueue.main).eraseToEffect() + ) + + case .fetchGalleries: + guard state.loadingState != .loading else { return .none } + state.loadingState = .loading + return databaseClient.fetchHistoryGalleries().map(Action.fetchGalleriesDone) + + case .fetchGalleriesDone(let galleries): + state.loadingState = .idle + if galleries.isEmpty { + state.loadingState = .failed(.notFound) + } else { + state.galleries = galleries + } + return .none + + case .detail: + return .none + } + } + + Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + + BindingReducer() + } +} diff --git a/EhPanda/View/Home/HistoryView.swift b/EhPanda/View/Home/History/HistoryView.swift similarity index 79% rename from EhPanda/View/Home/HistoryView.swift rename to EhPanda/View/Home/History/HistoryView.swift index 574f55e1..59e409bc 100644 --- a/EhPanda/View/Home/HistoryView.swift +++ b/EhPanda/View/Home/History/HistoryView.swift @@ -9,15 +9,15 @@ import SwiftUI import ComposableArchitecture struct HistoryView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double private let tagTranslator: TagTranslator init( - store: Store, + store: StoreOf, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store @@ -43,12 +43,12 @@ struct HistoryView: View { ) .sheet( unwrapping: viewStore.binding(\.$route), - case: /HistoryState.Route.detail, + case: /HistoryReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: HistoryAction.detail), + store: store.scope(state: \.detailState, action: HistoryReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -70,9 +70,9 @@ struct HistoryView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /HistoryState.Route.detail) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /HistoryReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: HistoryAction.detail), + store: store.scope(state: \.detailState, action: HistoryReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -90,7 +90,7 @@ struct HistoryView: View { .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.clear, unwrapping: viewStore.binding(\.$route), - case: /HistoryState.Route.clearHistory + case: /HistoryReducer.Route.clearHistory ) { Button(L10n.Localizable.ConfirmationDialog.Button.clear, role: .destructive) { viewStore.send(.clearHistoryGalleries) @@ -106,19 +106,7 @@ struct HistoryView_Previews: PreviewProvider { HistoryView( store: .init( initialState: .init(), - reducer: historyReducer, - environment: HistoryEnvironment( - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - uiApplicationClient: .live - ) + reducer: HistoryReducer() ), user: .init(), setting: .constant(.init()), diff --git a/EhPanda/View/Home/HomeReducer.swift b/EhPanda/View/Home/HomeReducer.swift new file mode 100644 index 00000000..ce8c559b --- /dev/null +++ b/EhPanda/View/Home/HomeReducer.swift @@ -0,0 +1,259 @@ +// +// HomeReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/05. +// + +import SwiftUI +import Kingfisher +import UIImageColors +import ComposableArchitecture + +struct HomeReducer: ReducerProtocol { + enum Route: Equatable, Hashable { + case detail(String) + case misc(HomeMiscGridType) + case section(HomeSectionType) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var cardPageIndex = 1 + @BindingState var currentCardID = "" + var allowsCardHitTesting = true + var rawCardColors = [String: [Color]]() + var cardColors: [Color] { + rawCardColors[currentCardID] ?? [.clear] + } + + var popularGalleries = [Gallery]() + var popularLoadingState: LoadingState = .idle + var frontpageGalleries = [Gallery]() + var frontpageLoadingState: LoadingState = .idle + var toplistsGalleries = [Int: [Gallery]]() + var toplistsLoadingState = [Int: LoadingState]() + + var frontpageState = FrontpageReducer.State() + var toplistsState = ToplistsReducer.State() + var popularState = PopularReducer.State() + var watchedState = WatchedReducer.State() + var historyState = HistoryReducer.State() + @Heap var detailState: DetailReducer.State! + + init() { + _detailState = .init(.init()) + } + + mutating func setPopularGalleries(_ galleries: [Gallery]) { + let sortedGalleries = galleries.sorted { lhs, rhs in + lhs.title.count > rhs.title.count + } + var trimmedGalleries = Array(sortedGalleries.prefix(min(sortedGalleries.count, 10))) + .removeDuplicates(by: \.trimmedTitle) + if trimmedGalleries.count >= 6 { + trimmedGalleries = Array(trimmedGalleries.prefix(6)) + } + trimmedGalleries.shuffle() + popularGalleries = trimmedGalleries + currentCardID = trimmedGalleries[cardPageIndex].gid + } + + mutating func setFrontpageGalleries(_ galleries: [Gallery]) { + frontpageGalleries = Array(galleries.prefix(min(galleries.count, 25))) + .removeDuplicates(by: \.trimmedTitle) + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + case setAllowsCardHitTesting(Bool) + case analyzeImageColors(String, RetrieveImageResult) + case analyzeImageColorsDone(String, UIImageColors?) + + case fetchAllGalleries + case fetchAllToplistsGalleries + case fetchPopularGalleries + case fetchPopularGalleriesDone(Result<[Gallery], AppError>) + case fetchFrontpageGalleries + case fetchFrontpageGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) + case fetchToplistsGalleries(Int, Int? = nil) + case fetchToplistsGalleriesDone(Int, Result<(PageNumber, [Gallery]), AppError>) + + case frontpage(FrontpageReducer.Action) + case toplists(ToplistsReducer.Action) + case popular(PopularReducer.Action) + case watched(WatchedReducer.Action) + case history(HistoryReducer.Action) + case detail(DetailReducer.Action) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.libraryClient) private var libraryClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding(\.$cardPageIndex): + guard state.cardPageIndex < state.popularGalleries.count else { return .none } + state.currentCardID = state.popularGalleries[state.cardPageIndex].gid + state.allowsCardHitTesting = false + return .init(value: .setAllowsCardHitTesting(true)) + .delay(for: .milliseconds(300), scheduler: DispatchQueue.main) + .eraseToEffect() + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .clearSubStates: + state.frontpageState = .init() + state.toplistsState = .init() + state.popularState = .init() + state.watchedState = .init() + state.historyState = .init() + state.detailState = .init() + return .merge( + .init(value: .frontpage(.teardown)), + .init(value: .toplists(.teardown)), + .init(value: .popular(.teardown)), + .init(value: .watched(.teardown)), + .init(value: .detail(.teardown)) + ) + + case .setAllowsCardHitTesting(let isAllowed): + state.allowsCardHitTesting = isAllowed + return .none + + case .fetchAllGalleries: + return .merge( + .init(value: .fetchPopularGalleries), + .init(value: .fetchFrontpageGalleries), + .init(value: .fetchAllToplistsGalleries) + ) + + case .fetchAllToplistsGalleries: + return .merge( + ToplistsType.allCases.map({ Action.fetchToplistsGalleries($0.categoryIndex) }) + .map(EffectTask.init) + ) + + case .fetchPopularGalleries: + guard state.popularLoadingState != .loading else { return .none } + state.popularLoadingState = .loading + state.rawCardColors = [String: [Color]]() + let filter = databaseClient.fetchFilterSynchronously(range: .global) + return PopularGalleriesRequest(filter: filter) + .effect.map(Action.fetchPopularGalleriesDone) + + case .fetchPopularGalleriesDone(let result): + state.popularLoadingState = .idle + switch result { + case .success(let galleries): + guard !galleries.isEmpty else { + state.popularLoadingState = .failed(.notFound) + return .none + } + state.setPopularGalleries(galleries) + return databaseClient.cacheGalleries(galleries).fireAndForget() + case .failure(let error): + state.popularLoadingState = .failed(error) + } + return .none + + case .fetchFrontpageGalleries: + guard state.frontpageLoadingState != .loading else { return .none } + state.frontpageLoadingState = .loading + let filter = databaseClient.fetchFilterSynchronously(range: .global) + return FrontpageGalleriesRequest(filter: filter) + .effect.map(Action.fetchFrontpageGalleriesDone) + + case .fetchFrontpageGalleriesDone(let result): + state.frontpageLoadingState = .idle + switch result { + case .success(let (_, galleries)): + guard !galleries.isEmpty else { + state.frontpageLoadingState = .failed(.notFound) + return .none + } + state.setFrontpageGalleries(galleries) + return databaseClient.cacheGalleries(galleries).fireAndForget() + case .failure(let error): + state.frontpageLoadingState = .failed(error) + } + return .none + + case .fetchToplistsGalleries(let index, let pageNum): + guard state.toplistsLoadingState[index] != .loading else { return .none } + state.toplistsLoadingState[index] = .loading + return ToplistsGalleriesRequest(catIndex: index, pageNum: pageNum) + .effect.map({ Action.fetchToplistsGalleriesDone(index, $0) }) + + case .fetchToplistsGalleriesDone(let index, let result): + state.toplistsLoadingState[index] = .idle + switch result { + case .success(let (_, galleries)): + guard !galleries.isEmpty else { + state.toplistsLoadingState[index] = .failed(.notFound) + return .none + } + state.toplistsGalleries[index] = galleries + return databaseClient.cacheGalleries(galleries).fireAndForget() + case .failure(let error): + state.toplistsLoadingState[index] = .failed(error) + } + return .none + + case .analyzeImageColors(let gid, let result): + guard !state.rawCardColors.keys.contains(gid) else { return .none } + return libraryClient.analyzeImageColors(result.image) + .map({ Action.analyzeImageColorsDone(gid, $0) }) + + case .analyzeImageColorsDone(let gid, let colors): + if let colors = colors { + state.rawCardColors[gid] = [ + colors.primary, colors.secondary, + colors.detail, colors.background + ] + .map(Color.init) + } + return .none + + case .frontpage: + return .none + + case .toplists: + return .none + + case .popular: + return .none + + case .watched: + return .none + + case .history: + return .none + + case .detail: + return .none + } + } + + 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, action: /Action.detail, child: DetailReducer.init) + + BindingReducer() + } +} diff --git a/EhPanda/View/Home/HomeView.swift b/EhPanda/View/Home/HomeView.swift index 41533d2e..922ee525 100644 --- a/EhPanda/View/Home/HomeView.swift +++ b/EhPanda/View/Home/HomeView.swift @@ -12,15 +12,15 @@ import SFSafeSymbols import ComposableArchitecture struct HomeView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double private let tagTranslator: TagTranslator init( - store: Store, + store: StoreOf, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store @@ -89,12 +89,12 @@ struct HomeView: View { } .sheet( unwrapping: viewStore.binding(\.$route), - case: /HomeState.Route.detail, + case: /HomeReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: HomeAction.detail), + store: store.scope(state: \.detailState, action: HomeReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -136,46 +136,46 @@ private extension HomeView { sectionLink } var detailViewLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /HomeState.Route.detail) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /HomeReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: HomeAction.detail), + store: store.scope(state: \.detailState, action: HomeReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } } var miscGridLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /HomeState.Route.misc) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /HomeReducer.Route.misc) { route in switch route.wrappedValue { case .popular: PopularView( - store: store.scope(state: \.popularState, action: HomeAction.popular), + store: store.scope(state: \.popularState, action: HomeReducer.Action.popular), user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) case .watched: WatchedView( - store: store.scope(state: \.watchedState, action: HomeAction.watched), + store: store.scope(state: \.watchedState, action: HomeReducer.Action.watched), user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) case .history: HistoryView( - store: store.scope(state: \.historyState, action: HomeAction.history), + store: store.scope(state: \.historyState, action: HomeReducer.Action.history), user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } } } var sectionLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /HomeState.Route.section) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /HomeReducer.Route.section) { route in switch route.wrappedValue { case .frontpage: FrontpageView( - store: store.scope(state: \.frontpageState, action: HomeAction.frontpage), + store: store.scope(state: \.frontpageState, action: HomeReducer.Action.frontpage), user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) case .toplists: ToplistsView( - store: store.scope(state: \.toplistsState, action: HomeAction.toplists), + store: store.scope(state: \.toplistsState, action: HomeReducer.Action.toplists), user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } @@ -521,20 +521,7 @@ struct HomeView_Previews: PreviewProvider { HomeView( store: .init( initialState: .init(), - reducer: homeReducer, - environment: HomeEnvironment( - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - libraryClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - uiApplicationClient: .live - ) + reducer: HomeReducer() ), user: .init(), setting: .constant(.init()), diff --git a/EhPanda/View/Home/Popular/PopularReducer.swift b/EhPanda/View/Home/Popular/PopularReducer.swift new file mode 100644 index 00000000..6996b01c --- /dev/null +++ b/EhPanda/View/Home/Popular/PopularReducer.swift @@ -0,0 +1,116 @@ +// +// PopularReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/09. +// + +import ComposableArchitecture + +struct PopularReducer: ReducerProtocol { + enum Route: Equatable { + case filters + case detail(String) + } + + struct CancelID: Hashable { + let id = String(describing: PopularReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var keyword = "" + + var filteredGalleries: [Gallery] { + guard !keyword.isEmpty else { return galleries } + return galleries.filter({ $0.title.caseInsensitiveContains(keyword) }) + } + var galleries = [Gallery]() + var loadingState: LoadingState = .idle + + var filtersState = FiltersReducer.State() + @Heap var detailState: DetailReducer.State! + + init() { + _detailState = .init(.init()) + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + + case teardown + case fetchGalleries + case fetchGalleriesDone(Result<[Gallery], AppError>) + + case filters(FiltersReducer.Action) + case detail(DetailReducer.Action) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .clearSubStates: + state.detailState = .init() + state.filtersState = .init() + return .init(value: .detail(.teardown)) + + case .teardown: + return .cancel(id: CancelID()) + + case .fetchGalleries: + guard state.loadingState != .loading else { return .none } + state.loadingState = .loading + let filter = databaseClient.fetchFilterSynchronously(range: .global) + return PopularGalleriesRequest(filter: filter) + .effect.map(Action.fetchGalleriesDone).cancellable(id: CancelID()) + + case .fetchGalleriesDone(let result): + state.loadingState = .idle + switch result { + case .success(let galleries): + guard !galleries.isEmpty else { + state.loadingState = .failed(.notFound) + return .none + } + state.galleries = galleries + return databaseClient.cacheGalleries(galleries).fireAndForget() + case .failure(let error): + state.loadingState = .failed(error) + } + return .none + + case .filters: + return .none + + case .detail: + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.filters, + hapticsClient: hapticsClient + ) + + Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) + Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + + BindingReducer() + } +} diff --git a/EhPanda/View/Home/PopularView.swift b/EhPanda/View/Home/Popular/PopularView.swift similarity index 77% rename from EhPanda/View/Home/PopularView.swift rename to EhPanda/View/Home/Popular/PopularView.swift index 530af91e..77a85236 100644 --- a/EhPanda/View/Home/PopularView.swift +++ b/EhPanda/View/Home/Popular/PopularView.swift @@ -9,15 +9,15 @@ import SwiftUI import ComposableArchitecture struct PopularView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double private let tagTranslator: TagTranslator init( - store: Store, + store: StoreOf, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store @@ -42,20 +42,20 @@ struct PopularView: View { ) .sheet( unwrapping: viewStore.binding(\.$route), - case: /PopularState.Route.detail, + case: /PopularReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: PopularAction.detail), + store: store.scope(state: \.detailState, action: PopularReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /PopularState.Route.filters) { _ in - FiltersView(store: store.scope(state: \.filtersState, action: PopularAction.filters)) + .sheet(unwrapping: viewStore.binding(\.$route), case: /PopularReducer.Route.filters) { _ in + FiltersView(store: store.scope(state: \.filtersState, action: PopularReducer.Action.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } .searchable(text: viewStore.binding(\.$keyword), prompt: L10n.Localizable.Searchable.Prompt.filter) @@ -73,9 +73,9 @@ struct PopularView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /PopularState.Route.detail) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /PopularReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: PopularAction.detail), + store: store.scope(state: \.detailState, action: PopularReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -97,19 +97,7 @@ struct PopularView_Previews: PreviewProvider { PopularView( store: .init( initialState: .init(), - reducer: popularReducer, - environment: PopularEnvironment( - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - uiApplicationClient: .live - ) + reducer: PopularReducer() ), user: .init(), setting: .constant(.init()), diff --git a/EhPanda/View/Home/Toplists/ToplistsReducer.swift b/EhPanda/View/Home/Toplists/ToplistsReducer.swift new file mode 100644 index 00000000..a5fb30e2 --- /dev/null +++ b/EhPanda/View/Home/Toplists/ToplistsReducer.swift @@ -0,0 +1,206 @@ +// +// ToplistsReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/08. +// + +import ComposableArchitecture + +struct ToplistsReducer: ReducerProtocol { + enum Route: Equatable { + case detail(String) + } + + struct CancelID: Hashable { + let id = String(describing: ToplistsReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var keyword = "" + @BindingState var jumpPageIndex = "" + @BindingState var jumpPageAlertFocused = false + @BindingState var jumpPageAlertPresented = false + + var type: ToplistsType = .yesterday + + var filteredGalleries: [Gallery]? { + guard !keyword.isEmpty else { return galleries } + return galleries?.filter({ $0.title.caseInsensitiveContains(keyword) }) + } + + var rawGalleries = [ToplistsType: [Gallery]]() + var rawPageNumber = [ToplistsType: PageNumber]() + var rawLoadingState = [ToplistsType: LoadingState]() + var rawFooterLoadingState = [ToplistsType: LoadingState]() + + var galleries: [Gallery]? { + rawGalleries[type] + } + var pageNumber: PageNumber? { + rawPageNumber[type] + } + var loadingState: LoadingState? { + rawLoadingState[type] + } + var footerLoadingState: LoadingState? { + rawFooterLoadingState[type] + } + + @Heap var detailState: DetailReducer.State! + + init() { + _detailState = .init(.init()) + } + + mutating func insertGalleries(type: ToplistsType, galleries: [Gallery]) { + galleries.forEach { gallery in + if rawGalleries[type]?.contains(gallery) == false { + rawGalleries[type]?.append(gallery) + } + } + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case setToplistsType(ToplistsType) + case clearSubStates + + case performJumpPage + case presentJumpPageAlert + case setJumpPageAlertFocused(Bool) + + case teardown + case fetchGalleries(Int? = nil) + case fetchGalleriesDone(ToplistsType, Result<(PageNumber, [Gallery]), AppError>) + case fetchMoreGalleries + case fetchMoreGalleriesDone(ToplistsType, Result<(PageNumber, [Gallery]), AppError>) + + case detail(DetailReducer.Action) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding(\.$jumpPageAlertPresented): + if !state.jumpPageAlertPresented { + state.jumpPageAlertFocused = false + } + return .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .setToplistsType(let type): + state.type = type + guard state.galleries?.isEmpty != false else { return .none } + return .init(value: Action.fetchGalleries()) + + case .clearSubStates: + state.detailState = .init() + return .init(value: .detail(.teardown)) + + case .performJumpPage: + guard let index = Int(state.jumpPageIndex), + let pageNumber = state.pageNumber, + index > 0, index <= pageNumber.maximum + 1 else { + return hapticsClient.generateNotificationFeedback(.error).fireAndForget() + } + return .init(value: .fetchGalleries(index - 1)) + + case .presentJumpPageAlert: + state.jumpPageAlertPresented = true + return hapticsClient.generateFeedback(.light).fireAndForget() + + case .setJumpPageAlertFocused(let isFocused): + state.jumpPageAlertFocused = isFocused + return .none + + case .teardown: + return .cancel(id: CancelID()) + + case .fetchGalleries(let pageNum): + guard state.loadingState != .loading else { return .none } + state.rawLoadingState[state.type] = .loading + if state.pageNumber == nil { + state.rawPageNumber[state.type] = PageNumber() + } else { + state.rawPageNumber[state.type]?.resetPages() + } + return ToplistsGalleriesRequest(catIndex: state.type.categoryIndex, pageNum: pageNum) + .effect.map({ [type = state.type] in Action.fetchGalleriesDone(type, $0) }) + .cancellable(id: CancelID()) + + case .fetchGalleriesDone(let type, let result): + state.rawLoadingState[type] = .idle + switch result { + case .success(let (pageNumber, galleries)): + guard !galleries.isEmpty else { + state.rawLoadingState[type] = .failed(.notFound) + guard pageNumber.hasNextPage() else { return .none } + return .init(value: .fetchMoreGalleries) + } + state.rawPageNumber[type] = pageNumber + state.rawGalleries[type] = galleries + return databaseClient.cacheGalleries(galleries).fireAndForget() + case .failure(let error): + state.rawLoadingState[type] = .failed(error) + } + return .none + + case .fetchMoreGalleries: + let pageNumber = state.pageNumber ?? .init() + guard pageNumber.hasNextPage(), + state.footerLoadingState != .loading + else { return .none } + state.rawFooterLoadingState[state.type] = .loading + let pageNum = pageNumber.current + 1 + return MoreToplistsGalleriesRequest(catIndex: state.type.categoryIndex, pageNum: pageNum) + .effect.map({ [type = state.type] in Action.fetchMoreGalleriesDone(type, $0) }) + .cancellable(id: CancelID()) + + case .fetchMoreGalleriesDone(let type, let result): + state.rawFooterLoadingState[type] = .idle + switch result { + case .success(let (pageNumber, galleries)): + state.rawPageNumber[type] = pageNumber + state.insertGalleries(type: type, galleries: galleries) + + var effects: [EffectTask] = [ + databaseClient.cacheGalleries(galleries).fireAndForget() + ] + if galleries.isEmpty, pageNumber.hasNextPage() { + effects.append(.init(value: .fetchMoreGalleries)) + } else if !galleries.isEmpty { + state.rawLoadingState[type] = .idle + } + return .merge(effects) + + case .failure(let error): + state.rawFooterLoadingState[type] = .failed(error) + } + return .none + + case .detail: + return .none + } + } + + Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + + BindingReducer() + } +} diff --git a/EhPanda/View/Home/ToplistsView.swift b/EhPanda/View/Home/Toplists/ToplistsView.swift similarity index 85% rename from EhPanda/View/Home/ToplistsView.swift rename to EhPanda/View/Home/Toplists/ToplistsView.swift index e857f3cf..e68d917a 100644 --- a/EhPanda/View/Home/ToplistsView.swift +++ b/EhPanda/View/Home/Toplists/ToplistsView.swift @@ -9,15 +9,15 @@ import SwiftUI import ComposableArchitecture struct ToplistsView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double private let tagTranslator: TagTranslator init( - store: Store, + store: StoreOf, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store @@ -48,12 +48,12 @@ struct ToplistsView: View { ) .sheet( unwrapping: viewStore.binding(\.$route), - case: /ToplistsState.Route.detail, + case: /ToplistsReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: ToplistsAction.detail), + store: store.scope(state: \.detailState, action: ToplistsReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -84,9 +84,9 @@ struct ToplistsView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /ToplistsState.Route.detail) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /ToplistsReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: ToplistsAction.detail), + store: store.scope(state: \.detailState, action: ToplistsReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -155,19 +155,7 @@ struct ToplistsView_Previews: PreviewProvider { ToplistsView( store: .init( initialState: .init(), - reducer: toplistsReducer, - environment: ToplistsEnvironment( - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - uiApplicationClient: .live - ) + reducer: ToplistsReducer() ), user: .init(), setting: .constant(.init()), diff --git a/EhPanda/View/Home/Watched/WatchedReducer.swift b/EhPanda/View/Home/Watched/WatchedReducer.swift new file mode 100644 index 00000000..478fbfa9 --- /dev/null +++ b/EhPanda/View/Home/Watched/WatchedReducer.swift @@ -0,0 +1,184 @@ +// +// WatchedReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/09. +// + +import ComposableArchitecture + +struct WatchedReducer: ReducerProtocol { + enum Route: Equatable { + case filters + case quickSearch + case detail(String) + } + + struct CancelID: Hashable { + let id = String(describing: WatchedReducer.self) + } + + struct State: Equatable { + @BindingState var route: Route? + @BindingState var keyword = "" + + var galleries = [Gallery]() + var pageNumber = PageNumber() + var loadingState: LoadingState = .idle + var footerLoadingState: LoadingState = .idle + + var filtersState = FiltersReducer.State() + var quickSearchState = QuickSearchReducer.State() + @Heap var detailState: DetailReducer.State! + + init() { + _detailState = .init(.init()) + } + + mutating func insertGalleries(_ galleries: [Gallery]) { + galleries.forEach { gallery in + if !self.galleries.contains(gallery) { + self.galleries.append(gallery) + } + } + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case clearSubStates + case onNotLoginViewButtonTapped + + case teardown + case fetchGalleries(String? = nil) + case fetchGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) + case fetchMoreGalleries + case fetchMoreGalleriesDone(Result<(PageNumber, [Gallery]), AppError>) + + case filters(FiltersReducer.Action) + case detail(DetailReducer.Action) + case quickSearch(QuickSearchReducer.Action) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .clearSubStates: + state.detailState = .init() + state.filtersState = .init() + state.quickSearchState = .init() + return .merge( + .init(value: .detail(.teardown)), + .init(value: .quickSearch(.teardown)) + ) + + case .onNotLoginViewButtonTapped: + return .none + + case .teardown: + return .cancel(id: CancelID()) + + case .fetchGalleries(let keyword): + guard state.loadingState != .loading else { return .none } + if let keyword = keyword { + state.keyword = keyword + } + state.loadingState = .loading + state.pageNumber.resetPages() + let filter = databaseClient.fetchFilterSynchronously(range: .watched) + return WatchedGalleriesRequest(filter: filter, keyword: state.keyword) + .effect.map(Action.fetchGalleriesDone).cancellable(id: CancelID()) + + case .fetchGalleriesDone(let result): + state.loadingState = .idle + switch result { + case .success(let (pageNumber, galleries)): + guard !galleries.isEmpty else { + state.loadingState = .failed(.notFound) + guard pageNumber.hasNextPage() else { return .none } + return .init(value: .fetchMoreGalleries) + } + state.pageNumber = pageNumber + state.galleries = galleries + return databaseClient.cacheGalleries(galleries).fireAndForget() + case .failure(let error): + state.loadingState = .failed(error) + } + return .none + + case .fetchMoreGalleries: + let pageNumber = state.pageNumber + guard pageNumber.hasNextPage(), + state.footerLoadingState != .loading, + let lastID = state.galleries.last?.id + else { return .none } + state.footerLoadingState = .loading + let filter = databaseClient.fetchFilterSynchronously(range: .watched) + return MoreWatchedGalleriesRequest(filter: filter, lastID: lastID, keyword: state.keyword).effect + .map(Action.fetchMoreGalleriesDone) + .cancellable(id: CancelID()) + + case .fetchMoreGalleriesDone(let result): + state.footerLoadingState = .idle + switch result { + case .success(let (pageNumber, galleries)): + state.pageNumber = pageNumber + state.insertGalleries(galleries) + + var effects: [EffectTask] = [ + databaseClient.cacheGalleries(galleries).fireAndForget() + ] + if galleries.isEmpty, pageNumber.hasNextPage() { + effects.append(.init(value: .fetchMoreGalleries)) + } else if !galleries.isEmpty { + state.loadingState = .idle + } + return .merge(effects) + + case .failure(let error): + state.footerLoadingState = .failed(error) + } + return .none + + case .quickSearch: + return .none + + case .filters: + return .none + + case .detail: + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.quickSearch, + hapticsClient: hapticsClient + ) + .haptics( + unwrapping: \.route, + case: /Route.filters, + hapticsClient: hapticsClient + ) + + Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) + Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) + Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + + BindingReducer() + } +} diff --git a/EhPanda/View/Home/WatchedView.swift b/EhPanda/View/Home/Watched/WatchedView.swift similarity index 81% rename from EhPanda/View/Home/WatchedView.swift rename to EhPanda/View/Home/Watched/WatchedView.swift index f2f94e54..416f95d2 100644 --- a/EhPanda/View/Home/WatchedView.swift +++ b/EhPanda/View/Home/Watched/WatchedView.swift @@ -9,15 +9,15 @@ import SwiftUI import ComposableArchitecture struct WatchedView: View { - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private let user: User @Binding private var setting: Setting private let blurRadius: Double private let tagTranslator: TagTranslator init( - store: Store, + store: StoreOf, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store @@ -50,21 +50,21 @@ struct WatchedView: View { } .sheet( unwrapping: viewStore.binding(\.$route), - case: /WatchedState.Route.detail, + case: /WatchedReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in NavigationView { DetailView( - store: store.scope(state: \.detailState, action: WatchedAction.detail), + store: store.scope(state: \.detailState, action: WatchedReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /WatchedState.Route.quickSearch) { _ in + .sheet(unwrapping: viewStore.binding(\.$route), case: /WatchedReducer.Route.quickSearch) { _ in QuickSearchView( - store: store.scope(state: \.quickSearchState, action: WatchedAction.quickSearch) + store: store.scope(state: \.quickSearchState, action: WatchedReducer.Action.quickSearch) ) { keyword in viewStore.send(.setNavigation(nil)) viewStore.send(.fetchGalleries(keyword)) @@ -72,8 +72,8 @@ struct WatchedView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /WatchedState.Route.filters) { _ in - FiltersView(store: store.scope(state: \.filtersState, action: WatchedAction.filters)) + .sheet(unwrapping: viewStore.binding(\.$route), case: /WatchedReducer.Route.filters) { _ in + FiltersView(store: store.scope(state: \.filtersState, action: WatchedReducer.Action.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } .searchable(text: viewStore.binding(\.$keyword)) { @@ -99,9 +99,9 @@ struct WatchedView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /WatchedState.Route.detail) { route in + NavigationLink(unwrapping: viewStore.binding(\.$route), case: /WatchedReducer.Route.detail) { route in DetailView( - store: store.scope(state: \.detailState, action: WatchedAction.detail), + store: store.scope(state: \.detailState, action: WatchedReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, blurRadius: blurRadius, tagTranslator: tagTranslator ) @@ -128,19 +128,7 @@ struct WatchedView_Previews: PreviewProvider { WatchedView( store: .init( initialState: .init(), - reducer: watchedReducer, - environment: WatchedEnvironment( - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - hapticsClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - uiApplicationClient: .live - ) + reducer: WatchedReducer() ), user: .init(), setting: .constant(.init()), From 71c12a9073c8165ec81d0035ddb4ad75f537b621 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 00:09:22 +0800 Subject: [PATCH 16/29] Refactor TabBarReducer --- EhPanda.xcodeproj/project.pbxproj | 8 +++---- EhPanda/DataFlow/AppStore.swift | 22 ++++++++--------- EhPanda/View/TabBar/TabBarReducer.swift | 32 +++++++++++++++++++++++++ EhPanda/View/TabBar/TabBarStore.swift | 30 ----------------------- 4 files changed, 47 insertions(+), 45 deletions(-) create mode 100644 EhPanda/View/TabBar/TabBarReducer.swift delete mode 100644 EhPanda/View/TabBar/TabBarStore.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 03913060..8f11155e 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -235,7 +235,7 @@ ABD49D5A277C5356003D1A07 /* FavoritesReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D59277C5356003D1A07 /* FavoritesReducer.swift */; }; ABD49D5D277C6C9D003D1A07 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = ABD49D5C277C6C9D003D1A07 /* SFSafeSymbols */; }; ABD49D60277C7722003D1A07 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D5F277C7722003D1A07 /* TabBarView.swift */; }; - ABD49D64277C7AD5003D1A07 /* TabBarStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D63277C7AD5003D1A07 /* TabBarStore.swift */; }; + ABD49D64277C7AD5003D1A07 /* TabBarReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D63277C7AD5003D1A07 /* TabBarReducer.swift */; }; ABD49D67277EAC90003D1A07 /* URLUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD49D66277EAC90003D1A07 /* URLUtil.swift */; }; ABD5FDD4263D05110021A4C6 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = ABD5FDD3263D05110021A4C6 /* .swiftlint.yml */; }; ABD7005926B1C31500DC59C9 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = ABD7005826B1C31500DC59C9 /* Kanna */; }; @@ -539,7 +539,7 @@ ABD4032726B7967F00001B8C /* CategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryView.swift; sourceTree = ""; }; ABD49D59277C5356003D1A07 /* FavoritesReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesReducer.swift; sourceTree = ""; }; ABD49D5F277C7722003D1A07 /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; - ABD49D63277C7AD5003D1A07 /* TabBarStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarStore.swift; sourceTree = ""; }; + ABD49D63277C7AD5003D1A07 /* TabBarReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarReducer.swift; sourceTree = ""; }; ABD49D66277EAC90003D1A07 /* URLUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLUtil.swift; sourceTree = ""; }; ABD5FDD3263D05110021A4C6 /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = SOURCE_ROOT; }; ABD9770D27B65A7300983DE7 /* ListParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListParserTests.swift; sourceTree = ""; }; @@ -1187,7 +1187,7 @@ isa = PBXGroup; children = ( ABD49D5F277C7722003D1A07 /* TabBarView.swift */, - ABD49D63277C7AD5003D1A07 /* TabBarStore.swift */, + ABD49D63277C7AD5003D1A07 /* TabBarReducer.swift */, ); path = TabBar; sourceTree = ""; @@ -1886,7 +1886,7 @@ ABE9401526FF158D0085E158 /* QuickSearchView.swift in Sources */, AB706F9B278AC5A30025A48A /* SearchRootView.swift in Sources */, AB706F8A278A4CC50025A48A /* PopularReducer.swift in Sources */, - ABD49D64277C7AD5003D1A07 /* TabBarStore.swift in Sources */, + ABD49D64277C7AD5003D1A07 /* TabBarReducer.swift in Sources */, ABF45AF025F3313D00ECB568 /* CommentsView.swift in Sources */, ABC8356527B36E550091DCDB /* AutoPlayHandler.swift in Sources */, ABBB2671279AFA61007B6149 /* EnvironmentKeys.swift in Sources */, diff --git a/EhPanda/DataFlow/AppStore.swift b/EhPanda/DataFlow/AppStore.swift index be345f44..96a96506 100644 --- a/EhPanda/DataFlow/AppStore.swift +++ b/EhPanda/DataFlow/AppStore.swift @@ -12,7 +12,7 @@ struct AppState: Equatable { var appDelegateState = AppDelegateState() var appRouteState = AppRouteState() var appLockState = AppLockState() - var tabBarState = TabBarState() + var tabBarState = TabBarReducer.State() var homeState = HomeReducer.State() var favoritesState = FavoritesReducer.State() var searchRootState = SearchRootReducer.State() @@ -27,7 +27,7 @@ enum AppAction: BindableAction { case appRoute(AppRouteAction) case appLock(AppLockAction) - case tabBar(TabBarAction) + case tabBar(TabBarReducer.Action) case home(HomeReducer.Action) case favorites(FavoritesReducer.Action) @@ -249,17 +249,17 @@ let appReducer = Reducer.combine( databaseClient: $0.databaseClient ) } - ), - tabBarReducer.pullback( - state: \.tabBarState, - action: /AppAction.tabBar, - environment: { - .init( - deviceClient: $0.deviceClient - ) - } ) // , +// tabBarReducer.pullback( +// state: \.tabBarState, +// action: /AppAction.tabBar, +// environment: { +// .init( +// deviceClient: $0.deviceClient +// ) +// } +// ), // homeReducer.pullback( // state: \.homeState, // action: /AppAction.home, diff --git a/EhPanda/View/TabBar/TabBarReducer.swift b/EhPanda/View/TabBar/TabBarReducer.swift new file mode 100644 index 00000000..a60089ee --- /dev/null +++ b/EhPanda/View/TabBar/TabBarReducer.swift @@ -0,0 +1,32 @@ +// +// TabBarReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/12/29. +// + +import ComposableArchitecture + +struct TabBarReducer: ReducerProtocol { + struct State: Equatable { + var tabBarItemType: TabBarItemType = .home + } + + enum Action: Equatable { + case setTabBarItemType(TabBarItemType) + } + + @Dependency(\.deviceClient) private var deviceClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .setTabBarItemType(let type): + if !deviceClient.isPad() || type != .setting { + state.tabBarItemType = type + } + return .none + } + } + } +} diff --git a/EhPanda/View/TabBar/TabBarStore.swift b/EhPanda/View/TabBar/TabBarStore.swift deleted file mode 100644 index 3da6db7c..00000000 --- a/EhPanda/View/TabBar/TabBarStore.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// TabBarStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 3/12/29. -// - -import ComposableArchitecture - -struct TabBarState: Equatable { - var tabBarItemType: TabBarItemType = .home -} - -enum TabBarAction { - case setTabBarItemType(TabBarItemType) -} - -struct TabBarEnvironment { - let deviceClient: DeviceClient -} - -let tabBarReducer = Reducer { state, action, environment in - switch action { - case .setTabBarItemType(let type): - if !environment.deviceClient.isPad() || type != .setting { - state.tabBarItemType = type - } - return .none - } -} From e94007d68d53a597a524ce674cde021ec3d5ea00 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 00:25:21 +0800 Subject: [PATCH 17/29] Refactor AppDelegateReducer --- EhPanda.xcodeproj/project.pbxproj | 16 ++-- .../App/Tools/Clients/DatabaseClient.swift | 28 ++++-- ...teStore.swift => AppDelegateReducer.swift} | 92 +++++++++---------- EhPanda/DataFlow/AppStore.swift | 28 +++--- EhPanda/View/Migration/MigrationReducer.swift | 78 ++++++++++++++++ EhPanda/View/Migration/MigrationStore.swift | 77 ---------------- EhPanda/View/Migration/MigrationView.swift | 13 +-- 7 files changed, 169 insertions(+), 163 deletions(-) rename EhPanda/DataFlow/{AppDelegateStore.swift => AppDelegateReducer.swift} (50%) create mode 100644 EhPanda/View/Migration/MigrationReducer.swift delete mode 100644 EhPanda/View/Migration/MigrationStore.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 8f11155e..03de8204 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -103,7 +103,7 @@ AB41DB5027B760D700DD3604 /* PopularCompactList.html in Resources */ = {isa = PBXBuildFile; fileRef = AB41DB3B27B760D700DD3604 /* PopularCompactList.html */; }; AB41DB5127B760D700DD3604 /* PopularMinimalList.html in Resources */ = {isa = PBXBuildFile; fileRef = AB41DB3C27B760D700DD3604 /* PopularMinimalList.html */; }; AB4FD2C1268AB83300A95968 /* GalleryDetailMO+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4FD2C0268AB83300A95968 /* GalleryDetailMO+CoreDataProperties.swift */; }; - AB58A5AC2776B2BC00C0D285 /* AppDelegateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB58A5AB2776B2BC00C0D285 /* AppDelegateStore.swift */; }; + AB58A5AC2776B2BC00C0D285 /* AppDelegateReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB58A5AB2776B2BC00C0D285 /* AppDelegateReducer.swift */; }; AB58A5B22776B99000C0D285 /* AppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB58A5B12776B99000C0D285 /* AppStore.swift */; }; AB5BE67926B95FDD007D4A55 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5BE67826B95FDD007D4A55 /* ShareViewController.swift */; }; AB5BE68026B95FDD007D4A55 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AB5BE67626B95FDD007D4A55 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -158,7 +158,7 @@ AB7BF2D827AA3F61001865A3 /* UserDefaultsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D727AA3F61001865A3 /* UserDefaultsUtil.swift */; }; AB7BF2DA27AA78CF001865A3 /* Reducer_Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2D927AA78CF001865A3 /* Reducer_Extension.swift */; }; AB7BF2FB27ABCA3A001865A3 /* MigrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2FA27ABCA3A001865A3 /* MigrationView.swift */; }; - AB7BF2FD27ABCAD4001865A3 /* MigrationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2FC27ABCAD4001865A3 /* MigrationStore.swift */; }; + AB7BF2FD27ABCAD4001865A3 /* MigrationReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2FC27ABCAD4001865A3 /* MigrationReducer.swift */; }; AB7BF30727ABDFF1001865A3 /* CoreDataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF2FE27ABDFF1001865A3 /* CoreDataMigrator.swift */; }; AB7BF30A27ABDFF1001865A3 /* CoreDataMigrationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF30327ABDFF1001865A3 /* CoreDataMigrationStep.swift */; }; AB7BF30D27ABDFF1001865A3 /* CoreDataMigrationVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7BF30627ABDFF1001865A3 /* CoreDataMigrationVersion.swift */; }; @@ -403,7 +403,7 @@ AB48BCF626D2539B0021A06C /* Model 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 2.xcdatamodel"; sourceTree = ""; }; AB4FD2C0268AB83300A95968 /* GalleryDetailMO+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GalleryDetailMO+CoreDataProperties.swift"; sourceTree = ""; }; AB543FF126DB7FD9009344C0 /* Model 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 3.xcdatamodel"; sourceTree = ""; }; - AB58A5AB2776B2BC00C0D285 /* AppDelegateStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateStore.swift; sourceTree = ""; }; + AB58A5AB2776B2BC00C0D285 /* AppDelegateReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateReducer.swift; sourceTree = ""; }; AB58A5B12776B99000C0D285 /* AppStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStore.swift; sourceTree = ""; }; AB5BE67626B95FDD007D4A55 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; AB5BE67826B95FDD007D4A55 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; @@ -458,7 +458,7 @@ AB7BF2D727AA3F61001865A3 /* UserDefaultsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsUtil.swift; sourceTree = ""; }; AB7BF2D927AA78CF001865A3 /* Reducer_Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reducer_Extension.swift; sourceTree = ""; }; AB7BF2FA27ABCA3A001865A3 /* MigrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationView.swift; sourceTree = ""; }; - AB7BF2FC27ABCAD4001865A3 /* MigrationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationStore.swift; sourceTree = ""; }; + AB7BF2FC27ABCAD4001865A3 /* MigrationReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationReducer.swift; sourceTree = ""; }; AB7BF2FE27ABDFF1001865A3 /* CoreDataMigrator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataMigrator.swift; sourceTree = ""; }; AB7BF30327ABDFF1001865A3 /* CoreDataMigrationStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataMigrationStep.swift; sourceTree = ""; }; AB7BF30627ABDFF1001865A3 /* CoreDataMigrationVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataMigrationVersion.swift; sourceTree = ""; }; @@ -984,7 +984,7 @@ isa = PBXGroup; children = ( AB7BF2FA27ABCA3A001865A3 /* MigrationView.swift */, - AB7BF2FC27ABCAD4001865A3 /* MigrationStore.swift */, + AB7BF2FC27ABCAD4001865A3 /* MigrationReducer.swift */, ); path = Migration; sourceTree = ""; @@ -1234,7 +1234,7 @@ AB58A5B12776B99000C0D285 /* AppStore.swift */, AB86AC1227856F2700E61E6A /* AppLockStore.swift */, AB706F7827890A6C0025A48A /* AppRouteStore.swift */, - AB58A5AB2776B2BC00C0D285 /* AppDelegateStore.swift */, + AB58A5AB2776B2BC00C0D285 /* AppDelegateReducer.swift */, ); path = DataFlow; sourceTree = ""; @@ -1783,7 +1783,7 @@ ABF45ABB25F3312F00ECB568 /* AppError.swift in Sources */, AB10118026986C1100C2C1A9 /* GalleryStateMO+CoreDataClass.swift in Sources */, AB63EADB2699AC8200090535 /* AppEnvMO+CoreDataProperties.swift in Sources */, - AB7BF2FD27ABCAD4001865A3 /* MigrationStore.swift in Sources */, + AB7BF2FD27ABCAD4001865A3 /* MigrationReducer.swift in Sources */, AB7BF2D827AA3F61001865A3 /* UserDefaultsUtil.swift in Sources */, EA2E2E822A1FA1060038A261 /* SearchReducer.swift in Sources */, AB0929CA278196ED00F107CA /* CookieClient.swift in Sources */, @@ -1832,7 +1832,7 @@ ABC3C7892593699B00E0C11B /* Defaults.swift in Sources */, AB8C821926BF801700E8C5E6 /* EhSetting.swift in Sources */, AB86AC1327856F2700E61E6A /* AppLockStore.swift in Sources */, - AB58A5AC2776B2BC00C0D285 /* AppDelegateStore.swift in Sources */, + AB58A5AC2776B2BC00C0D285 /* AppDelegateReducer.swift in Sources */, ABBB263E2793C648007B6149 /* PreviewsReducer.swift in Sources */, ABBC332A26BE7C940084A331 /* SettingTextField.swift in Sources */, AB358317269D826B009466A5 /* DFStreamHandler.swift in Sources */, diff --git a/EhPanda/App/Tools/Clients/DatabaseClient.swift b/EhPanda/App/Tools/Clients/DatabaseClient.swift index 91f6d415..574f0fb0 100644 --- a/EhPanda/App/Tools/Clients/DatabaseClient.swift +++ b/EhPanda/App/Tools/Clients/DatabaseClient.swift @@ -11,8 +11,8 @@ import CoreData import ComposableArchitecture struct DatabaseClient { - let prepareDatabase: () -> EffectTask> - let dropDatabase: () -> EffectTask> + let prepareDatabase: () -> EffectTask + let dropDatabase: () -> EffectTask private let saveContext: () -> Void private let materializedObjects: (NSManagedObjectContext, NSPredicate) -> [NSManagedObject] } @@ -21,19 +21,35 @@ extension DatabaseClient { static let live: Self = .init( prepareDatabase: { Future { promise in - PersistenceController.shared.prepare(completion: promise) + PersistenceController.shared.prepare { + switch $0 { + case .success: + promise(.success(nil)) + + case .failure(let appError): + promise(.success(appError)) + } + } } .eraseToAnyPublisher() .receive(on: DispatchQueue.main) - .catchToEffect() + .eraseToEffect() }, dropDatabase: { Future { promise in - PersistenceController.shared.rebuild(completion: promise) + PersistenceController.shared.rebuild { + switch $0 { + case .success: + promise(.success(nil)) + + case .failure(let appError): + promise(.success(appError)) + } + } } .eraseToAnyPublisher() .receive(on: DispatchQueue.main) - .catchToEffect() + .eraseToEffect() }, saveContext: { let context = PersistenceController.shared.container.viewContext diff --git a/EhPanda/DataFlow/AppDelegateStore.swift b/EhPanda/DataFlow/AppDelegateReducer.swift similarity index 50% rename from EhPanda/DataFlow/AppDelegateStore.swift rename to EhPanda/DataFlow/AppDelegateReducer.swift index d68d9954..594e1f61 100644 --- a/EhPanda/DataFlow/AppDelegateStore.swift +++ b/EhPanda/DataFlow/AppDelegateReducer.swift @@ -1,5 +1,5 @@ // -// AppDelegateStore.swift +// AppDelegateReducer.swift // EhPanda // // Created by 荒木辰造 on R 3/12/25. @@ -9,6 +9,47 @@ import SwiftUI import SwiftyBeaver import ComposableArchitecture +struct AppDelegateReducer: ReducerProtocol { + struct State: Equatable { + var migrationState = MigrationReducer.State() + } + + enum Action: Equatable { + case onLaunchFinish + case removeExpiredImageURLs + + case migration(MigrationReducer.Action) + } + + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.libraryClient) private var libraryClient + @Dependency(\.cookieClient) private var cookieClient + + var body: some ReducerProtocol { + Reduce { _, action in + switch action { + case .onLaunchFinish: + return .merge( + libraryClient.initializeLogger().fireAndForget(), + libraryClient.initializeWebImage().fireAndForget(), + cookieClient.removeYay().fireAndForget(), + cookieClient.ignoreOffensive().fireAndForget(), + cookieClient.fulfillAnotherHostField().fireAndForget(), + .init(value: .migration(.prepareDatabase)) + ) + + case .removeExpiredImageURLs: + return databaseClient.removeExpiredImageURLs().fireAndForget() + + case .migration: + return .none + } + } + + Scope(state: \.migrationState, action: /Action.migration, child: MigrationReducer.init) + } +} + // MARK: AppDelegate class AppDelegate: UIResponder, UIApplicationDelegate { let store = Store( @@ -49,52 +90,3 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } } - -struct AppDelegateState: Equatable { - var migrationState = MigrationState() -} - -enum AppDelegateAction { - case onLaunchFinish - case removeExpiredImageURLs - - case migration(MigrationAction) -} - -struct AppDelegateEnvironment { - let dfClient: DFClient - let libraryClient: LibraryClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient -} - -let appDelegateReducer = Reducer.combine( - .init { _, action, environment in - switch action { - case .onLaunchFinish: - return .merge( - environment.libraryClient.initializeLogger().fireAndForget(), - environment.libraryClient.initializeWebImage().fireAndForget(), - environment.cookieClient.removeYay().fireAndForget(), - environment.cookieClient.ignoreOffensive().fireAndForget(), - environment.cookieClient.fulfillAnotherHostField().fireAndForget(), - .init(value: .migration(.prepareDatabase)) - ) - - case .removeExpiredImageURLs: - return environment.databaseClient.removeExpiredImageURLs().fireAndForget() - - case .migration: - return .none - } - }, - migrationReducer.pullback( - state: \.migrationState, - action: /AppDelegateAction.migration, - environment: { - .init( - databaseClient: $0.databaseClient - ) - } - ) -) diff --git a/EhPanda/DataFlow/AppStore.swift b/EhPanda/DataFlow/AppStore.swift index 96a96506..ad70918f 100644 --- a/EhPanda/DataFlow/AppStore.swift +++ b/EhPanda/DataFlow/AppStore.swift @@ -9,7 +9,7 @@ import SwiftUI import ComposableArchitecture struct AppState: Equatable { - var appDelegateState = AppDelegateState() + var appDelegateState = AppDelegateReducer.State() var appRouteState = AppRouteState() var appLockState = AppLockState() var tabBarState = TabBarReducer.State() @@ -23,7 +23,7 @@ enum AppAction: BindableAction { case binding(BindingAction) case onScenePhaseChange(ScenePhase) - case appDelegate(AppDelegateAction) + case appDelegate(AppDelegateReducer.Action) case appRoute(AppRouteAction) case appLock(AppLockAction) @@ -237,20 +237,20 @@ let appReducer = Reducer.combine( authorizationClient: $0.authorizationClient ) } - ), - appDelegateReducer.pullback( - state: \.appDelegateState, - action: /AppAction.appDelegate, - environment: { - .init( - dfClient: $0.dfClient, - libraryClient: $0.libraryClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient - ) - } ) // , +// appDelegateReducer.pullback( +// state: \.appDelegateState, +// action: /AppAction.appDelegate, +// environment: { +// .init( +// dfClient: $0.dfClient, +// libraryClient: $0.libraryClient, +// cookieClient: $0.cookieClient, +// databaseClient: $0.databaseClient +// ) +// } +// ), // tabBarReducer.pullback( // state: \.tabBarState, // action: /AppAction.tabBar, diff --git a/EhPanda/View/Migration/MigrationReducer.swift b/EhPanda/View/Migration/MigrationReducer.swift new file mode 100644 index 00000000..97889fe5 --- /dev/null +++ b/EhPanda/View/Migration/MigrationReducer.swift @@ -0,0 +1,78 @@ +// +// MigrationReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/02/03. +// + +import Foundation +import ComposableArchitecture + +struct MigrationReducer: ReducerProtocol { + enum Route: Equatable { + case dropDialog + } + + struct State: Equatable { + @BindingState var route: Route? + var databaseState: LoadingState = .loading + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + case setNavigation(Route?) + case onDatabasePreparationSuccess + + case prepareDatabase + case prepareDatabaseDone(AppError?) + case dropDatabase + case dropDatabaseDone(AppError?) + } + + @Dependency(\.databaseClient) private var databaseClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return .none + + case .onDatabasePreparationSuccess: + return .none + + case .prepareDatabase: + return databaseClient.prepareDatabase().map(Action.prepareDatabaseDone) + + case .prepareDatabaseDone(let appError): + if let appError { + state.databaseState = .failed(appError) + return .none + } else { + state.databaseState = .idle + return .init(value: .onDatabasePreparationSuccess) + } + + case .dropDatabase: + state.databaseState = .loading + return databaseClient.dropDatabase() + .delay(for: .milliseconds(500), scheduler: DispatchQueue.main) + .eraseToEffect().map(Action.dropDatabaseDone) + + case .dropDatabaseDone(let appError): + if let appError { + state.databaseState = .failed(appError) + return .none + } else { + state.databaseState = .idle + return .init(value: .onDatabasePreparationSuccess) + } + } + } + + BindingReducer() + } +} diff --git a/EhPanda/View/Migration/MigrationStore.swift b/EhPanda/View/Migration/MigrationStore.swift deleted file mode 100644 index dcbd3966..00000000 --- a/EhPanda/View/Migration/MigrationStore.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// MigrationStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/02/03. -// - -import Foundation -import ComposableArchitecture - -struct MigrationState: Equatable { - enum Route: Equatable { - case dropDialog - } - - @BindingState var route: Route? - var databaseState: LoadingState = .loading -} - -enum MigrationAction: BindableAction { - case binding(BindingAction) - case setNavigation(MigrationState.Route?) - case onDatabasePreparationSuccess - - case prepareDatabase - case prepareDatabaseDone(Result) - case dropDatabase - case dropDatabaseDone(Result) -} - -struct MigrationEnvironment { - let databaseClient: DatabaseClient -} - -let migrationReducer = Reducer { state, action, environment in - switch action { - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return .none - - case .onDatabasePreparationSuccess: - return .none - - case .prepareDatabase: - return environment.databaseClient.prepareDatabase().map(MigrationAction.prepareDatabaseDone) - - case .prepareDatabaseDone(let result): - switch result { - case .success: - state.databaseState = .idle - return .init(value: .onDatabasePreparationSuccess) - case .failure(let error): - state.databaseState = .failed(error) - return .none - } - - case .dropDatabase: - state.databaseState = .loading - return environment.databaseClient.dropDatabase() - .delay(for: .milliseconds(500), scheduler: DispatchQueue.main) - .eraseToEffect().map(MigrationAction.dropDatabaseDone) - - case .dropDatabaseDone(let result): - switch result { - case .success: - state.databaseState = .idle - return .init(value: .onDatabasePreparationSuccess) - case .failure(let error): - state.databaseState = .failed(error) - return .none - } - } -} -.binding() diff --git a/EhPanda/View/Migration/MigrationView.swift b/EhPanda/View/Migration/MigrationView.swift index 5c45b703..5a2984f1 100644 --- a/EhPanda/View/Migration/MigrationView.swift +++ b/EhPanda/View/Migration/MigrationView.swift @@ -10,14 +10,14 @@ import ComposableArchitecture struct MigrationView: View { @Environment(\.colorScheme) private var colorScheme - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf private var reversedPrimary: Color { colorScheme == .light ? .white : .black } - init(store: Store) { + init(store: StoreOf) { self.store = store viewStore = ViewStore(store) } @@ -37,7 +37,7 @@ struct MigrationView: View { .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.dropDatabase, unwrapping: viewStore.binding(\.$route), - case: /MigrationState.Route.dropDialog + case: /MigrationReducer.Route.dropDialog ) { Button(L10n.Localizable.ConfirmationDialog.Button.dropDatabase, role: .destructive) { viewStore.send(.dropDatabase) @@ -56,10 +56,7 @@ struct MigrationView_Previews: PreviewProvider { MigrationView( store: .init( initialState: .init(), - reducer: migrationReducer, - environment: MigrationEnvironment( - databaseClient: .live - ) + reducer: MigrationReducer() ) ) } From 7ba6b27f6201ed7ac0edf4787dbf8e76baefa5f3 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 00:29:50 +0800 Subject: [PATCH 18/29] Refactor AppLockReducer --- EhPanda.xcodeproj/project.pbxproj | 8 +-- EhPanda/DataFlow/AppLockReducer.swift | 74 +++++++++++++++++++++++++++ EhPanda/DataFlow/AppLockStore.swift | 73 -------------------------- EhPanda/DataFlow/AppStore.swift | 22 ++++---- 4 files changed, 89 insertions(+), 88 deletions(-) create mode 100644 EhPanda/DataFlow/AppLockReducer.swift delete mode 100644 EhPanda/DataFlow/AppLockStore.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 03de8204..fe965708 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -172,7 +172,7 @@ AB86ABF92782EC0D00E61E6A /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86ABF82782EC0D00E61E6A /* AboutView.swift */; }; AB86AC0A2782FAFA00E61E6A /* AppearanceSettingReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC092782FAFA00E61E6A /* AppearanceSettingReducer.swift */; }; AB86AC1027831AD100E61E6A /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = AB86AC0F27831AD100E61E6A /* ComposableArchitecture */; }; - AB86AC1327856F2700E61E6A /* AppLockStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC1227856F2700E61E6A /* AppLockStore.swift */; }; + AB86AC1327856F2700E61E6A /* AppLockReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC1227856F2700E61E6A /* AppLockReducer.swift */; }; AB86AC1A2785C2B300E61E6A /* HomeReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB86AC192785C2B300E61E6A /* HomeReducer.swift */; }; AB8C821926BF801700E8C5E6 /* EhSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8C821826BF801700E8C5E6 /* EhSetting.swift */; }; AB90276B291F548700697256 /* AppIcon_NotMyPresident@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB902766291F548600697256 /* AppIcon_NotMyPresident@3x.png */; }; @@ -473,7 +473,7 @@ AB86ABF62782DDE600E61E6A /* FileClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileClient.swift; sourceTree = ""; }; AB86ABF82782EC0D00E61E6A /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; AB86AC092782FAFA00E61E6A /* AppearanceSettingReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingReducer.swift; sourceTree = ""; }; - AB86AC1227856F2700E61E6A /* AppLockStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockStore.swift; sourceTree = ""; }; + AB86AC1227856F2700E61E6A /* AppLockReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockReducer.swift; sourceTree = ""; }; AB86AC192785C2B300E61E6A /* HomeReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeReducer.swift; sourceTree = ""; }; AB8C821826BF801700E8C5E6 /* EhSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EhSetting.swift; sourceTree = ""; }; AB902766291F548600697256 /* AppIcon_NotMyPresident@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon_NotMyPresident@3x.png"; sourceTree = ""; }; @@ -1232,7 +1232,7 @@ children = ( AB1EF25327AFA19200F507D6 /* Heap.swift */, AB58A5B12776B99000C0D285 /* AppStore.swift */, - AB86AC1227856F2700E61E6A /* AppLockStore.swift */, + AB86AC1227856F2700E61E6A /* AppLockReducer.swift */, AB706F7827890A6C0025A48A /* AppRouteStore.swift */, AB58A5AB2776B2BC00C0D285 /* AppDelegateReducer.swift */, ); @@ -1831,7 +1831,7 @@ AB69CB8026B3DABC00699359 /* AdvancedList.swift in Sources */, ABC3C7892593699B00E0C11B /* Defaults.swift in Sources */, AB8C821926BF801700E8C5E6 /* EhSetting.swift in Sources */, - AB86AC1327856F2700E61E6A /* AppLockStore.swift in Sources */, + AB86AC1327856F2700E61E6A /* AppLockReducer.swift in Sources */, AB58A5AC2776B2BC00C0D285 /* AppDelegateReducer.swift in Sources */, ABBB263E2793C648007B6149 /* PreviewsReducer.swift in Sources */, ABBC332A26BE7C940084A331 /* SettingTextField.swift in Sources */, diff --git a/EhPanda/DataFlow/AppLockReducer.swift b/EhPanda/DataFlow/AppLockReducer.swift new file mode 100644 index 00000000..8c2f5d76 --- /dev/null +++ b/EhPanda/DataFlow/AppLockReducer.swift @@ -0,0 +1,74 @@ +// +// AppLockReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/05. +// + +import SwiftUI +import ComposableArchitecture + +struct AppLockReducer: ReducerProtocol { + struct State: Equatable { + @BindingState var blurRadius: Double = 0 + var becameInactiveDate: Date? + var isAppLocked = false + + // Setting `blurRadius` to zero causes the NavigationBar to collapse + mutating func setBlurRadius(_ radius: Double) { + blurRadius = max(0.00001, radius) + } + } + + enum Action: Equatable { + case onBecomeActive(Int, Double) + case onBecomeInactive(Double) + case lockApp(Double) + case unlockApp + case authorize + case authorizeDone(Bool) + } + + @Dependency(\.authorizationClient) private var authorizationClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .onBecomeActive(let threshold, let blurRadius): + if let date = state.becameInactiveDate, threshold >= 0, + Date().timeIntervalSince(date) > Double(threshold) + { + return .merge( + .init(value: .authorize), + .init(value: .lockApp(blurRadius)) + ) + } else { + return .init(value: .unlockApp) + } + + case .onBecomeInactive(let blurRadius): + state.setBlurRadius(blurRadius) + state.becameInactiveDate = .now + return .none + + case .lockApp(let blurRadius): + state.setBlurRadius(blurRadius) + state.isAppLocked = true + return .none + + case .unlockApp: + state.setBlurRadius(0) + state.isAppLocked = false + state.becameInactiveDate = nil + return .none + + case .authorize: + return authorizationClient.localAuthroize(L10n.Localizable.LocalAuthorization.reason) + .map(Action.authorizeDone) + + case .authorizeDone(let isSucceeded): + return isSucceeded ? .init(value: .unlockApp) : .none + } + } + } +} diff --git a/EhPanda/DataFlow/AppLockStore.swift b/EhPanda/DataFlow/AppLockStore.swift deleted file mode 100644 index fedbe50a..00000000 --- a/EhPanda/DataFlow/AppLockStore.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// AppLockStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/05. -// - -import SwiftUI -import ComposableArchitecture - -struct AppLockState: Equatable { - @BindingState var blurRadius: Double = 0 - var becameInactiveDate: Date? - var isAppLocked = false - - // Setting `blurRadius` to zero causes the NavigationBar to collapse - mutating func setBlurRadius(_ radius: Double) { - blurRadius = max(0.00001, radius) - } -} - -enum AppLockAction { - case onBecomeActive(Int, Double) - case onBecomeInactive(Double) - case lockApp(Double) - case unlockApp - case authorize - case authorizeDone(Bool) -} - -struct AppLockEnvironment { - let authorizationClient: AuthorizationClient -} - -let appLockReducer = Reducer { state, action, environment in - switch action { - case .onBecomeActive(let threshold, let blurRadius): - if let date = state.becameInactiveDate, threshold >= 0, - Date().timeIntervalSince(date) > Double(threshold) - { - return .merge( - .init(value: .authorize), - .init(value: .lockApp(blurRadius)) - ) - } else { - return .init(value: .unlockApp) - } - - case .onBecomeInactive(let blurRadius): - state.setBlurRadius(blurRadius) - state.becameInactiveDate = .now - return .none - - case .lockApp(let blurRadius): - state.setBlurRadius(blurRadius) - state.isAppLocked = true - return .none - - case .unlockApp: - state.setBlurRadius(0) - state.isAppLocked = false - state.becameInactiveDate = nil - return .none - - case .authorize: - return environment.authorizationClient - .localAuthroize(L10n.Localizable.LocalAuthorization.reason) - .map(AppLockAction.authorizeDone) - - case .authorizeDone(let isSucceeded): - return isSucceeded ? .init(value: .unlockApp) : .none - } -} diff --git a/EhPanda/DataFlow/AppStore.swift b/EhPanda/DataFlow/AppStore.swift index ad70918f..5ccd95e8 100644 --- a/EhPanda/DataFlow/AppStore.swift +++ b/EhPanda/DataFlow/AppStore.swift @@ -11,7 +11,7 @@ import ComposableArchitecture struct AppState: Equatable { var appDelegateState = AppDelegateReducer.State() var appRouteState = AppRouteState() - var appLockState = AppLockState() + var appLockState = AppLockReducer.State() var tabBarState = TabBarReducer.State() var homeState = HomeReducer.State() var favoritesState = FavoritesReducer.State() @@ -25,7 +25,7 @@ enum AppAction: BindableAction { case appDelegate(AppDelegateReducer.Action) case appRoute(AppRouteAction) - case appLock(AppLockAction) + case appLock(AppLockReducer.Action) case tabBar(TabBarReducer.Action) @@ -228,17 +228,17 @@ let appReducer = Reducer.combine( authorizationClient: $0.authorizationClient ) } - ), - appLockReducer.pullback( - state: \.appLockState, - action: /AppAction.appLock, - environment: { - .init( - authorizationClient: $0.authorizationClient - ) - } ) // , +// appLockReducer.pullback( +// state: \.appLockState, +// action: /AppAction.appLock, +// environment: { +// .init( +// authorizationClient: $0.authorizationClient +// ) +// } +// ), // appDelegateReducer.pullback( // state: \.appDelegateState, // action: /AppAction.appDelegate, From 4d12ec6de6ff9c927f39cf9844d18872cc43b029 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 00:34:58 +0800 Subject: [PATCH 19/29] Refactor AppRouteReducer --- EhPanda.xcodeproj/project.pbxproj | 8 +- EhPanda/DataFlow/AppRouteReducer.swift | 178 +++++++++++++++++++++ EhPanda/DataFlow/AppRouteStore.swift | 204 ------------------------- EhPanda/DataFlow/AppStore.swift | 52 +++---- EhPanda/View/TabBar/TabBarView.swift | 8 +- 5 files changed, 212 insertions(+), 238 deletions(-) create mode 100644 EhPanda/DataFlow/AppRouteReducer.swift delete mode 100644 EhPanda/DataFlow/AppRouteStore.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index fe965708..cc6765cc 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -115,7 +115,7 @@ AB69CB8226B3DAF400699359 /* ControlPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB69CB8126B3DAF400699359 /* ControlPanel.swift */; }; AB6D106A27EBF890003A2245 /* GeneralSettingStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6D106927EBF890003A2245 /* GeneralSettingStoreTests.swift */; }; AB6DE897268822390087C579 /* LogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6DE896268822390087C579 /* LogsView.swift */; }; - AB706F7927890A6C0025A48A /* AppRouteStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F7827890A6C0025A48A /* AppRouteStore.swift */; }; + AB706F7927890A6C0025A48A /* AppRouteReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F7827890A6C0025A48A /* AppRouteReducer.swift */; }; AB706F7B278937500025A48A /* FrontpageReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F7A278937500025A48A /* FrontpageReducer.swift */; }; AB706F80278981370025A48A /* AlertKit_Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F7F278981370025A48A /* AlertKit_Extension.swift */; }; AB706F82278986120025A48A /* ToolbarItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F81278986120025A48A /* ToolbarItems.swift */; }; @@ -414,7 +414,7 @@ AB69CB8126B3DAF400699359 /* ControlPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlPanel.swift; sourceTree = ""; }; AB6D106927EBF890003A2245 /* GeneralSettingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingStoreTests.swift; sourceTree = ""; }; AB6DE896268822390087C579 /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = ""; }; - AB706F7827890A6C0025A48A /* AppRouteStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteStore.swift; sourceTree = ""; }; + AB706F7827890A6C0025A48A /* AppRouteReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteReducer.swift; sourceTree = ""; }; AB706F7A278937500025A48A /* FrontpageReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrontpageReducer.swift; sourceTree = ""; }; AB706F7F278981370025A48A /* AlertKit_Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertKit_Extension.swift; sourceTree = ""; }; AB706F81278986120025A48A /* ToolbarItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarItems.swift; sourceTree = ""; }; @@ -1233,7 +1233,7 @@ AB1EF25327AFA19200F507D6 /* Heap.swift */, AB58A5B12776B99000C0D285 /* AppStore.swift */, AB86AC1227856F2700E61E6A /* AppLockReducer.swift */, - AB706F7827890A6C0025A48A /* AppRouteStore.swift */, + AB706F7827890A6C0025A48A /* AppRouteReducer.swift */, AB58A5AB2776B2BC00C0D285 /* AppDelegateReducer.swift */, ); path = DataFlow; @@ -1880,7 +1880,7 @@ AB706F7B278937500025A48A /* FrontpageReducer.swift in Sources */, AB86ABF72782DDE600E61E6A /* FileClient.swift in Sources */, AB7B29F226AC471E00EE1F14 /* Model5toModel6MigrationPolicy.swift in Sources */, - AB706F7927890A6C0025A48A /* AppRouteStore.swift in Sources */, + AB706F7927890A6C0025A48A /* AppRouteReducer.swift in Sources */, AB706F88278A4C8A0025A48A /* PopularView.swift in Sources */, AB706FA1278BCEC60025A48A /* DetailView.swift in Sources */, ABE9401526FF158D0085E158 /* QuickSearchView.swift in Sources */, diff --git a/EhPanda/DataFlow/AppRouteReducer.swift b/EhPanda/DataFlow/AppRouteReducer.swift new file mode 100644 index 00000000..404d117d --- /dev/null +++ b/EhPanda/DataFlow/AppRouteReducer.swift @@ -0,0 +1,178 @@ +// +// AppRouteReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 4/01/08. +// + +import SwiftUI +import TTProgressHUD +import ComposableArchitecture + +struct AppRouteReducer: ReducerProtocol { + enum Route: Equatable, Hashable { + case hud + case setting + case detail(String) + case newDawn(Greeting) + } + + struct State: Equatable { + @BindingState var route: Route? + var hudConfig: TTProgressHUDConfig = .loading + + @Heap var detailState: DetailReducer.State! + + init() { + _detailState = .init(.init()) + } + } + + enum Action: BindableAction { + case binding(BindingAction) + case setNavigation(Route?) + case setHUDConfig(TTProgressHUDConfig) + case clearSubStates + + case detectClipboardURL + case handleDeepLink(URL) + case handleGalleryLink(URL) + + case updateReadingProgress(String, Int) + + case fetchGallery(URL, Bool) + case fetchGalleryDone(URL, Result) + case fetchGreetingDone(Result) + + case detail(DetailReducer.Action) + } + + @Dependency(\.userDefaultsClient) private var userDefaultsClient + @Dependency(\.clipboardClient) private var clipboardClient + @Dependency(\.databaseClient) private var databaseClient + @Dependency(\.hapticsClient) private var hapticsClient + @Dependency(\.urlClient) private var urlClient + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .binding(\.$route): + return state.route == nil ? .init(value: .clearSubStates) : .none + + case .binding: + return .none + + case .setNavigation(let route): + state.route = route + return route == nil ? .init(value: .clearSubStates) : .none + + case .setHUDConfig(let config): + state.hudConfig = config + return .none + + case .clearSubStates: + state.detailState = .init() + return .init(value: .detail(.teardown)) + + case .detectClipboardURL: + let currentChangeCount = clipboardClient.changeCount() + guard currentChangeCount != userDefaultsClient + .getValue(.clipboardChangeCount) else { return .none } + var effects: [EffectTask] = [ + userDefaultsClient + .setValue(currentChangeCount, .clipboardChangeCount).fireAndForget() + ] + if let url = clipboardClient.url() { + effects.append(.init(value: .handleDeepLink(url))) + } + return .merge(effects) + + case .handleDeepLink(let url): + let url = urlClient.resolveAppSchemeURL(url) ?? url + guard urlClient.checkIfHandleable(url) else { return .none } + var delay = 0 + if case .detail = state.route { + delay = 1000 + state.route = nil + state.detailState = .init() + } + let (isGalleryImageURL, _, _) = urlClient.analyzeURL(url) + let gid = urlClient.parseGalleryID(url) + guard databaseClient.fetchGallery(gid: gid) == nil else { + return .init(value: .handleGalleryLink(url)) + .delay(for: .milliseconds(delay + 250), scheduler: DispatchQueue.main).eraseToEffect() + } + return .init(value: .fetchGallery(url, isGalleryImageURL)) + .delay(for: .milliseconds(delay), scheduler: DispatchQueue.main).eraseToEffect() + + case .handleGalleryLink(let url): + let (_, pageIndex, commentID) = urlClient.analyzeURL(url) + let gid = urlClient.parseGalleryID(url) + var effects = [EffectTask]() + state.detailState = .init() + effects.append(.init(value: .detail(.fetchDatabaseInfos(gid)))) + if let pageIndex = pageIndex { + effects.append(.init(value: .updateReadingProgress(gid, pageIndex))) + effects.append( + .init(value: .detail(.setNavigation(.reading))) + .delay(for: .milliseconds(500), scheduler: DispatchQueue.main).eraseToEffect() + ) + } else if let commentID = commentID { + state.detailState.commentsState?.scrollCommentID = commentID + effects.append( + .init(value: .detail(.setNavigation(.comments(url)))) + .delay(for: .milliseconds(500), scheduler: DispatchQueue.main).eraseToEffect() + ) + } + effects.append(.init(value: .setNavigation(.detail(gid)))) + return .merge(effects) + + case .updateReadingProgress(let gid, let progress): + guard !gid.isEmpty else { return .none } + return databaseClient + .updateReadingProgress(gid: gid, progress: progress).fireAndForget() + + case .fetchGallery(let url, let isGalleryImageURL): + state.route = .hud + return GalleryReverseRequest(url: url, isGalleryImageURL: isGalleryImageURL) + .effect.map({ Action.fetchGalleryDone(url, $0) }) + + case .fetchGalleryDone(let url, let result): + state.route = nil + switch result { + case .success(let gallery): + return .merge( + databaseClient.cacheGalleries([gallery]).fireAndForget(), + .init(value: .handleGalleryLink(url)) + ) + case .failure: + return .init(value: .setHUDConfig(.error)) + .delay(for: .milliseconds(500), scheduler: DispatchQueue.main).eraseToEffect() + } + + case .fetchGreetingDone(let result): + if case .success(let greeting) = result, !greeting.gainedNothing { + return .init(value: .setNavigation(.newDawn(greeting))) + } + return .none + + case .detail: + return .none + } + } + .haptics( + unwrapping: \.route, + case: /Route.newDawn, + hapticsClient: hapticsClient + ) + .haptics( + unwrapping: \.route, + case: /Route.detail, + hapticsClient: hapticsClient + ) + + Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) + + BindingReducer() + } +} diff --git a/EhPanda/DataFlow/AppRouteStore.swift b/EhPanda/DataFlow/AppRouteStore.swift deleted file mode 100644 index d8c4111e..00000000 --- a/EhPanda/DataFlow/AppRouteStore.swift +++ /dev/null @@ -1,204 +0,0 @@ -// -// AppRouteStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 4/01/08. -// - -import SwiftUI -import TTProgressHUD -import ComposableArchitecture - -struct AppRouteState: Equatable { - enum Route: Equatable, Hashable { - case hud - case setting - case detail(String) - case newDawn(Greeting) - } - - init() { - _detailState = .init(.init()) - } - - @BindingState var route: Route? - var hudConfig: TTProgressHUDConfig = .loading - - @Heap var detailState: DetailReducer.State! -} - -enum AppRouteAction: BindableAction { - case binding(BindingAction) - case setNavigation(AppRouteState.Route?) - case setHUDConfig(TTProgressHUDConfig) - case clearSubStates - - case detectClipboardURL - case handleDeepLink(URL) - case handleGalleryLink(URL) - - case updateReadingProgress(String, Int) - - case fetchGallery(URL, Bool) - case fetchGalleryDone(URL, Result) - case fetchGreetingDone(Result) - - case detail(DetailReducer.Action) -} - -struct AppRouteEnvironment { - let dfClient: DFClient - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let loggerClient: LoggerClient - let hapticsClient: HapticsClient - let libraryClient: LibraryClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let userDefaultsClient: UserDefaultsClient - let uiApplicationClient: UIApplicationClient - let authorizationClient: AuthorizationClient -} - -let appRouteReducer = Reducer.combine( - .init { state, action, environment in - switch action { - case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none - - case .binding: - return .none - - case .setNavigation(let route): - state.route = route - return route == nil ? .init(value: .clearSubStates) : .none - - case .setHUDConfig(let config): - state.hudConfig = config - return .none - - case .clearSubStates: - state.detailState = .init() - return .init(value: .detail(.teardown)) - - case .detectClipboardURL: - let currentChangeCount = environment.clipboardClient.changeCount() - guard currentChangeCount != environment.userDefaultsClient - .getValue(.clipboardChangeCount) else { return .none } - var effects: [EffectTask] = [ - environment.userDefaultsClient - .setValue(currentChangeCount, .clipboardChangeCount).fireAndForget() - ] - if let url = environment.clipboardClient.url() { - effects.append(.init(value: .handleDeepLink(url))) - } - return .merge(effects) - - case .handleDeepLink(let url): - var url = environment.urlClient.resolveAppSchemeURL(url) ?? url - guard environment.urlClient.checkIfHandleable(url) else { return .none } - var delay = 0 - if case .detail = state.route { - delay = 1000 - state.route = nil - state.detailState = .init() - } - let (isGalleryImageURL, _, _) = environment.urlClient.analyzeURL(url) - let gid = environment.urlClient.parseGalleryID(url) - guard environment.databaseClient.fetchGallery(gid: gid) == nil else { - return .init(value: .handleGalleryLink(url)) - .delay(for: .milliseconds(delay + 250), scheduler: DispatchQueue.main).eraseToEffect() - } - return .init(value: .fetchGallery(url, isGalleryImageURL)) - .delay(for: .milliseconds(delay), scheduler: DispatchQueue.main).eraseToEffect() - - case .handleGalleryLink(let url): - let (_, pageIndex, commentID) = environment.urlClient.analyzeURL(url) - let gid = environment.urlClient.parseGalleryID(url) - var effects = [EffectTask]() - state.detailState = .init() - effects.append(.init(value: .detail(.fetchDatabaseInfos(gid)))) - if let pageIndex = pageIndex { - effects.append(.init(value: .updateReadingProgress(gid, pageIndex))) - effects.append( - .init(value: .detail(.setNavigation(.reading))) - .delay(for: .milliseconds(500), scheduler: DispatchQueue.main).eraseToEffect() - ) - } else if let commentID = commentID { - state.detailState.commentsState?.scrollCommentID = commentID - effects.append( - .init(value: .detail(.setNavigation(.comments(url)))) - .delay(for: .milliseconds(500), scheduler: DispatchQueue.main).eraseToEffect() - ) - } - effects.append(.init(value: .setNavigation(.detail(gid)))) - return .merge(effects) - - case .updateReadingProgress(let gid, let progress): - guard !gid.isEmpty else { return .none } - return environment.databaseClient - .updateReadingProgress(gid: gid, progress: progress).fireAndForget() - - case .fetchGallery(let url, let isGalleryImageURL): - state.route = .hud - return GalleryReverseRequest(url: url, isGalleryImageURL: isGalleryImageURL) - .effect.map({ AppRouteAction.fetchGalleryDone(url, $0) }) - - case .fetchGalleryDone(let url, let result): - state.route = nil - switch result { - case .success(let gallery): - return .merge( - environment.databaseClient.cacheGalleries([gallery]).fireAndForget(), - .init(value: .handleGalleryLink(url)) - ) - case .failure: - return .init(value: .setHUDConfig(.error)) - .delay(for: .milliseconds(500), scheduler: DispatchQueue.main).eraseToEffect() - } - - case .fetchGreetingDone(let result): - if case .success(let greeting) = result, !greeting.gainedNothing { - return .init(value: .setNavigation(.newDawn(greeting))) - } - return .none - - case .detail: - return .none - } - } - .haptics( - unwrapping: \.route, - case: /AppRouteState.Route.newDawn, - hapticsClient: \.hapticsClient - ) - .haptics( - unwrapping: \.route, - case: /AppRouteState.Route.detail, - hapticsClient: \.hapticsClient - ) - .binding() -// , -// detailReducer.pullback( -// state: \.detailState, -// action: /AppRouteAction.detail, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ) -) diff --git a/EhPanda/DataFlow/AppStore.swift b/EhPanda/DataFlow/AppStore.swift index 5ccd95e8..1db0de17 100644 --- a/EhPanda/DataFlow/AppStore.swift +++ b/EhPanda/DataFlow/AppStore.swift @@ -10,7 +10,7 @@ import ComposableArchitecture struct AppState: Equatable { var appDelegateState = AppDelegateReducer.State() - var appRouteState = AppRouteState() + var appRouteState = AppRouteReducer.State() var appLockState = AppLockReducer.State() var tabBarState = TabBarReducer.State() var homeState = HomeReducer.State() @@ -24,7 +24,7 @@ enum AppAction: BindableAction { case onScenePhaseChange(ScenePhase) case appDelegate(AppDelegateReducer.Action) - case appRoute(AppRouteAction) + case appRoute(AppRouteReducer.Action) case appLock(AppLockReducer.Action) case tabBar(TabBarReducer.Action) @@ -205,31 +205,31 @@ let appReducerCore = Reducer { state, actio .binding() let appReducer = Reducer.combine( - appReducerCore, - appRouteReducer.pullback( - state: \.appRouteState, - action: /AppAction.appRoute, - environment: { - .init( - dfClient: $0.dfClient, - urlClient: $0.urlClient, - fileClient: $0.fileClient, - imageClient: $0.imageClient, - deviceClient: $0.deviceClient, - loggerClient: $0.loggerClient, - hapticsClient: $0.hapticsClient, - libraryClient: $0.libraryClient, - cookieClient: $0.cookieClient, - databaseClient: $0.databaseClient, - clipboardClient: $0.clipboardClient, - appDelegateClient: $0.appDelegateClient, - userDefaultsClient: $0.userDefaultsClient, - uiApplicationClient: $0.uiApplicationClient, - authorizationClient: $0.authorizationClient - ) - } - ) + appReducerCore // , +// appRouteReducer.pullback( +// state: \.appRouteState, +// action: /AppAction.appRoute, +// environment: { +// .init( +// dfClient: $0.dfClient, +// urlClient: $0.urlClient, +// fileClient: $0.fileClient, +// imageClient: $0.imageClient, +// deviceClient: $0.deviceClient, +// loggerClient: $0.loggerClient, +// hapticsClient: $0.hapticsClient, +// libraryClient: $0.libraryClient, +// cookieClient: $0.cookieClient, +// databaseClient: $0.databaseClient, +// clipboardClient: $0.clipboardClient, +// appDelegateClient: $0.appDelegateClient, +// userDefaultsClient: $0.userDefaultsClient, +// uiApplicationClient: $0.uiApplicationClient, +// authorizationClient: $0.authorizationClient +// ) +// } +// ), // appLockReducer.pullback( // state: \.appLockState, // action: /AppAction.appLock, diff --git a/EhPanda/View/TabBar/TabBarView.swift b/EhPanda/View/TabBar/TabBarView.swift index 29f435fd..95486b5a 100644 --- a/EhPanda/View/TabBar/TabBarView.swift +++ b/EhPanda/View/TabBar/TabBarView.swift @@ -73,11 +73,11 @@ struct TabBarView: View { } .font(.system(size: 80)).opacity(viewStore.appLockState.isAppLocked ? 1 : 0) } - .sheet(unwrapping: viewStore.binding(\.appRouteState.$route), case: /AppRouteState.Route.newDawn) { route in + .sheet(unwrapping: viewStore.binding(\.appRouteState.$route), case: /AppRouteReducer.Route.newDawn) { route in NewDawnView(greeting: route.wrappedValue) .autoBlur(radius: viewStore.appLockState.blurRadius) } - .sheet(unwrapping: viewStore.binding(\.appRouteState.$route), case: /AppRouteState.Route.setting) { _ in + .sheet(unwrapping: viewStore.binding(\.appRouteState.$route), case: /AppRouteReducer.Route.setting) { _ in SettingView( store: store.scope(state: \.settingState, action: AppAction.setting), blurRadius: viewStore.appLockState.blurRadius @@ -85,7 +85,7 @@ struct TabBarView: View { .accentColor(viewStore.settingState.setting.accentColor) .autoBlur(radius: viewStore.appLockState.blurRadius) } - .sheet(unwrapping: viewStore.binding(\.appRouteState.$route), case: /AppRouteState.Route.detail) { route in + .sheet(unwrapping: viewStore.binding(\.appRouteState.$route), case: /AppRouteReducer.Route.detail) { route in NavigationView { DetailView( store: store.scope(state: \.appRouteState.detailState, action: { AppAction.appRoute(.detail($0)) }), @@ -103,7 +103,7 @@ struct TabBarView: View { .progressHUD( config: viewStore.appRouteState.hudConfig, unwrapping: viewStore.binding(\.appRouteState.$route), - case: /AppRouteState.Route.hud + case: /AppRouteReducer.Route.hud ) .onChange(of: scenePhase) { viewStore.send(.onScenePhaseChange($0)) } .onOpenURL { viewStore.send(.appRoute(.handleDeepLink($0))) } From 33e2cb90ce3586570ac27801512eebff3c8bf17b Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 00:45:37 +0800 Subject: [PATCH 20/29] Refactor AppReducer --- EhPanda.xcodeproj/project.pbxproj | 8 +- EhPanda/App/EhPandaApp.swift | 2 +- .../Tools/Extensions/Reducer_Extension.swift | 18 + EhPanda/DataFlow/AppDelegateReducer.swift | 21 +- EhPanda/DataFlow/AppReducer.swift | 206 +++++++++++ EhPanda/DataFlow/AppStore.swift | 342 ------------------ EhPanda/View/TabBar/TabBarView.swift | 40 +- 7 files changed, 244 insertions(+), 393 deletions(-) create mode 100644 EhPanda/DataFlow/AppReducer.swift delete mode 100644 EhPanda/DataFlow/AppStore.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index cc6765cc..0707f561 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -104,7 +104,7 @@ AB41DB5127B760D700DD3604 /* PopularMinimalList.html in Resources */ = {isa = PBXBuildFile; fileRef = AB41DB3C27B760D700DD3604 /* PopularMinimalList.html */; }; AB4FD2C1268AB83300A95968 /* GalleryDetailMO+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4FD2C0268AB83300A95968 /* GalleryDetailMO+CoreDataProperties.swift */; }; AB58A5AC2776B2BC00C0D285 /* AppDelegateReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB58A5AB2776B2BC00C0D285 /* AppDelegateReducer.swift */; }; - AB58A5B22776B99000C0D285 /* AppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB58A5B12776B99000C0D285 /* AppStore.swift */; }; + AB58A5B22776B99000C0D285 /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB58A5B12776B99000C0D285 /* AppReducer.swift */; }; AB5BE67926B95FDD007D4A55 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5BE67826B95FDD007D4A55 /* ShareViewController.swift */; }; AB5BE68026B95FDD007D4A55 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AB5BE67626B95FDD007D4A55 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; AB60D0E9274C7ECE00F899AB /* WaterfallGrid in Frameworks */ = {isa = PBXBuildFile; productRef = AB60D0E8274C7ECE00F899AB /* WaterfallGrid */; }; @@ -404,7 +404,7 @@ AB4FD2C0268AB83300A95968 /* GalleryDetailMO+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GalleryDetailMO+CoreDataProperties.swift"; sourceTree = ""; }; AB543FF126DB7FD9009344C0 /* Model 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 3.xcdatamodel"; sourceTree = ""; }; AB58A5AB2776B2BC00C0D285 /* AppDelegateReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateReducer.swift; sourceTree = ""; }; - AB58A5B12776B99000C0D285 /* AppStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStore.swift; sourceTree = ""; }; + AB58A5B12776B99000C0D285 /* AppReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducer.swift; sourceTree = ""; }; AB5BE67626B95FDD007D4A55 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; AB5BE67826B95FDD007D4A55 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; AB5BE67D26B95FDD007D4A55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1231,7 +1231,7 @@ isa = PBXGroup; children = ( AB1EF25327AFA19200F507D6 /* Heap.swift */, - AB58A5B12776B99000C0D285 /* AppStore.swift */, + AB58A5B12776B99000C0D285 /* AppReducer.swift */, AB86AC1227856F2700E61E6A /* AppLockReducer.swift */, AB706F7827890A6C0025A48A /* AppRouteReducer.swift */, AB58A5AB2776B2BC00C0D285 /* AppDelegateReducer.swift */, @@ -1867,7 +1867,7 @@ AB706F92278A6E8C0025A48A /* WatchedReducer.swift in Sources */, AB706F80278981370025A48A /* AlertKit_Extension.swift in Sources */, ABA9A6C228EC7BD000EE28DE /* Strings.swift in Sources */, - AB58A5B22776B99000C0D285 /* AppStore.swift in Sources */, + AB58A5B22776B99000C0D285 /* AppReducer.swift in Sources */, AB24C566276758E30085C33A /* GalleryCardCell.swift in Sources */, ABBB2679279D454C007B6149 /* GalleryInfosReducer.swift in Sources */, AB7BF2B727A9652F001865A3 /* Greeting.swift in Sources */, diff --git a/EhPanda/App/EhPandaApp.swift b/EhPanda/App/EhPandaApp.swift index e418f347..5e10e9ea 100644 --- a/EhPanda/App/EhPandaApp.swift +++ b/EhPanda/App/EhPandaApp.swift @@ -23,7 +23,7 @@ import ComposableArchitecture MigrationView( store: appDelegate.store.scope( state: \.appDelegateState.migrationState, - action: { AppAction.appDelegate(.migration($0)) } + action: { AppReducer.Action.appDelegate(.migration($0)) } ) ) .opacity(viewStore.state != .idle ? 1 : 0) diff --git a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift index 6e2361dc..78c926a4 100644 --- a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift +++ b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift @@ -94,3 +94,21 @@ where State == Base.State, Action == Base.Action { return self } } + +// MARK: Logging +struct LoggingReducer: ReducerProtocol +where State == Base.State, Action == Base.Action { + let base: Base + + init(@ReducerBuilder base: () -> Base) { + self.base = base() + } + + @ReducerBuilder + var body: some ReducerProtocol { + Reduce { state, action in + Logger.info(action) + return base.reduce(into: &state, action: action) + } + } +} diff --git a/EhPanda/DataFlow/AppDelegateReducer.swift b/EhPanda/DataFlow/AppDelegateReducer.swift index 594e1f61..e10ba75d 100644 --- a/EhPanda/DataFlow/AppDelegateReducer.swift +++ b/EhPanda/DataFlow/AppDelegateReducer.swift @@ -53,25 +53,8 @@ struct AppDelegateReducer: ReducerProtocol { // MARK: AppDelegate class AppDelegate: UIResponder, UIApplicationDelegate { let store = Store( - initialState: AppState(), - reducer: appReducer, - environment: AppEnvironment( - dfClient: .live, - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - loggerClient: .live, - hapticsClient: .live, - libraryClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - userDefaultsClient: .live, - uiApplicationClient: .live, - authorizationClient: .live - ) + initialState: .init(), + reducer: AppReducer() ) lazy var viewStore = ViewStore(store) diff --git a/EhPanda/DataFlow/AppReducer.swift b/EhPanda/DataFlow/AppReducer.swift new file mode 100644 index 00000000..2e20b14a --- /dev/null +++ b/EhPanda/DataFlow/AppReducer.swift @@ -0,0 +1,206 @@ +// +// AppReducer.swift +// EhPanda +// +// Created by 荒木辰造 on R 3/12/25. +// + +import SwiftUI +import ComposableArchitecture + +struct AppReducer: ReducerProtocol { + struct State: Equatable { + var appDelegateState = AppDelegateReducer.State() + var appRouteState = AppRouteReducer.State() + var appLockState = AppLockReducer.State() + var tabBarState = TabBarReducer.State() + var homeState = HomeReducer.State() + var favoritesState = FavoritesReducer.State() + var searchRootState = SearchRootReducer.State() + var settingState = SettingReducer.State() + } + + enum Action: BindableAction { + case binding(BindingAction) + case onScenePhaseChange(ScenePhase) + + case appDelegate(AppDelegateReducer.Action) + case appRoute(AppRouteReducer.Action) + case appLock(AppLockReducer.Action) + + case tabBar(TabBarReducer.Action) + + case home(HomeReducer.Action) + case favorites(FavoritesReducer.Action) + case searchRoot(SearchRootReducer.Action) + case setting(SettingReducer.Action) + } + + @Dependency(\.hapticsClient) private var hapticsClient + @Dependency(\.cookieClient) private var cookieClient + @Dependency(\.deviceClient) private var deviceClient + + var body: some ReducerProtocol { + LoggingReducer { + Reduce { state, action in + switch action { + case .binding(\.appRouteState.$route): + return state.appRouteState.route == nil ? .init(value: .appRoute(.clearSubStates)) : .none + + case .binding(\.settingState.$setting): + return .init(value: .setting(.syncSetting)) + + case .binding: + return .none + + case .onScenePhaseChange(let scenePhase): + switch scenePhase { + case .active: + let threshold = state.settingState.setting.autoLockPolicy.rawValue + let blurRadius = state.settingState.setting.backgroundBlurRadius + return .init(value: .appLock(.onBecomeActive(threshold, blurRadius))) + case .inactive: + let blurRadius = state.settingState.setting.backgroundBlurRadius + return .init(value: .appLock(.onBecomeInactive(blurRadius))) + default: + break + } + return .none + + case .appDelegate(.migration(.onDatabasePreparationSuccess)): + return .merge( + .init(value: .appDelegate(.removeExpiredImageURLs)), + .init(value: .setting(.loadUserSettings)) + ) + + case .appDelegate: + return .none + + case .appRoute(.clearSubStates): + var effects = [EffectTask]() + if deviceClient.isPad() { + state.settingState.route = nil + effects.append(.init(value: .setting(.clearSubStates))) + } + return effects.isEmpty ? .none : .merge(effects) + + case .appRoute: + return .none + + case .appLock(.unlockApp): + var effects: [EffectTask] = [ + .init(value: .setting(.fetchGreeting)) + ] + if state.settingState.setting.detectsLinksFromClipboard { + effects.append(.init(value: .appRoute(.detectClipboardURL))) + } + return .merge(effects) + + case .appLock: + return .none + + case .tabBar(.setTabBarItemType(let type)): + var effects = [EffectTask]() + let hapticEffect: EffectTask = hapticsClient + .generateFeedback(.soft).fireAndForget() + if type == state.tabBarState.tabBarItemType { + switch type { + case .home: + if state.homeState.route != nil { + effects.append(.init(value: .home(.setNavigation(nil)))) + } else { + effects.append(.init(value: .home(.fetchAllGalleries))) + } + case .favorites: + if state.favoritesState.route != nil { + effects.append(.init(value: .favorites(.setNavigation(nil)))) + effects.append(hapticEffect) + } else if cookieClient.didLogin { + effects.append(.init(value: .favorites(.fetchGalleries()))) + effects.append(hapticEffect) + } + case .search: + if state.searchRootState.route != nil { + effects.append(.init(value: .searchRoot(.setNavigation(nil)))) + } else { + effects.append(.init(value: .searchRoot(.fetchDatabaseInfos))) + } + case .setting: + if state.settingState.route != nil { + effects.append(.init(value: .setting(.setNavigation(nil)))) + effects.append(hapticEffect) + } + } + if [.home, .search].contains(type) { + effects.append(hapticEffect) + } + } + if type == .setting && deviceClient.isPad() { + effects.append(.init(value: .appRoute(.setNavigation(.setting)))) + } + return effects.isEmpty ? .none : .merge(effects) + + case .tabBar: + return .none + + case .home(.watched(.onNotLoginViewButtonTapped)), .favorites(.onNotLoginViewButtonTapped): + var effects: [EffectTask] = [ + hapticsClient.generateFeedback(.soft).fireAndForget(), + .init(value: .tabBar(.setTabBarItemType(.setting))) + ] + effects.append(.init(value: .setting(.setNavigation(.account)))) + if !cookieClient.didLogin { + effects.append( + .init(value: .setting(.account(.setNavigation(.login)))) + .delay( + for: .milliseconds(deviceClient.isPad() ? 1200 : 200), + scheduler: DispatchQueue.main + ) + .eraseToEffect() + ) + } + return .merge(effects) + + case .home: + return .none + + case .favorites: + return .none + + case .searchRoot: + return .none + + case .setting(.loadUserSettingsDone): + var effects = [EffectTask]() + let threshold = state.settingState.setting.autoLockPolicy.rawValue + let blurRadius = state.settingState.setting.backgroundBlurRadius + if threshold >= 0 { + state.appLockState.becameInactiveDate = .distantPast + effects.append(.init(value: .appLock(.onBecomeActive(threshold, blurRadius)))) + } + if state.settingState.setting.detectsLinksFromClipboard { + effects.append(.init(value: .appRoute(.detectClipboardURL))) + } + return effects.isEmpty ? .none : .merge(effects) + + case .setting(.fetchGreetingDone(let result)): + return .init(value: .appRoute(.fetchGreetingDone(result))) + + case .setting: + return .none + } + } + + 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) + + BindingReducer() + } + } +} diff --git a/EhPanda/DataFlow/AppStore.swift b/EhPanda/DataFlow/AppStore.swift deleted file mode 100644 index 1db0de17..00000000 --- a/EhPanda/DataFlow/AppStore.swift +++ /dev/null @@ -1,342 +0,0 @@ -// -// AppStore.swift -// EhPanda -// -// Created by 荒木辰造 on R 3/12/25. -// - -import SwiftUI -import ComposableArchitecture - -struct AppState: Equatable { - var appDelegateState = AppDelegateReducer.State() - var appRouteState = AppRouteReducer.State() - var appLockState = AppLockReducer.State() - var tabBarState = TabBarReducer.State() - var homeState = HomeReducer.State() - var favoritesState = FavoritesReducer.State() - var searchRootState = SearchRootReducer.State() - var settingState = SettingReducer.State() -} - -enum AppAction: BindableAction { - case binding(BindingAction) - case onScenePhaseChange(ScenePhase) - - case appDelegate(AppDelegateReducer.Action) - case appRoute(AppRouteReducer.Action) - case appLock(AppLockReducer.Action) - - case tabBar(TabBarReducer.Action) - - case home(HomeReducer.Action) - case favorites(FavoritesReducer.Action) - case searchRoot(SearchRootReducer.Action) - case setting(SettingReducer.Action) -} - -struct AnyEnvironment {} -struct AppEnvironment { - let dfClient: DFClient - let urlClient: URLClient - let fileClient: FileClient - let imageClient: ImageClient - let deviceClient: DeviceClient - let loggerClient: LoggerClient - let hapticsClient: HapticsClient - let libraryClient: LibraryClient - let cookieClient: CookieClient - let databaseClient: DatabaseClient - let clipboardClient: ClipboardClient - let appDelegateClient: AppDelegateClient - let userDefaultsClient: UserDefaultsClient - let uiApplicationClient: UIApplicationClient - let authorizationClient: AuthorizationClient -} - -let appReducerCore = Reducer { state, action, environment in - switch action { - case .binding(\.appRouteState.$route): - return state.appRouteState.route == nil ? .init(value: .appRoute(.clearSubStates)) : .none - - case .binding(\.settingState.$setting): - return .init(value: .setting(.syncSetting)) - - case .binding: - return .none - - case .onScenePhaseChange(let scenePhase): - switch scenePhase { - case .active: - let threshold = state.settingState.setting.autoLockPolicy.rawValue - let blurRadius = state.settingState.setting.backgroundBlurRadius - return .init(value: .appLock(.onBecomeActive(threshold, blurRadius))) - case .inactive: - let blurRadius = state.settingState.setting.backgroundBlurRadius - return .init(value: .appLock(.onBecomeInactive(blurRadius))) - default: - break - } - return .none - - case .appDelegate(.migration(.onDatabasePreparationSuccess)): - return .merge( - .init(value: .appDelegate(.removeExpiredImageURLs)), - .init(value: .setting(.loadUserSettings)) - ) - - case .appDelegate: - return .none - - case .appRoute(.clearSubStates): - var effects = [EffectTask]() - if environment.deviceClient.isPad() { - state.settingState.route = nil - effects.append(.init(value: .setting(.clearSubStates))) - } - return effects.isEmpty ? .none : .merge(effects) - - case .appRoute: - return .none - - case .appLock(.unlockApp): - var effects: [EffectTask] = [ - .init(value: .setting(.fetchGreeting)) - ] - if state.settingState.setting.detectsLinksFromClipboard { - effects.append(.init(value: .appRoute(.detectClipboardURL))) - } - return .merge(effects) - - case .appLock: - return .none - - case .tabBar(.setTabBarItemType(let type)): - var effects = [EffectTask]() - let hapticEffect: EffectTask = environment.hapticsClient - .generateFeedback(.soft).fireAndForget() - if type == state.tabBarState.tabBarItemType { - switch type { - case .home: - if state.homeState.route != nil { - effects.append(.init(value: .home(.setNavigation(nil)))) - } else { - effects.append(.init(value: .home(.fetchAllGalleries))) - } - case .favorites: - if state.favoritesState.route != nil { - effects.append(.init(value: .favorites(.setNavigation(nil)))) - effects.append(hapticEffect) - } else if environment.cookieClient.didLogin { - effects.append(.init(value: .favorites(.fetchGalleries()))) - effects.append(hapticEffect) - } - case .search: - if state.searchRootState.route != nil { - effects.append(.init(value: .searchRoot(.setNavigation(nil)))) - } else { - effects.append(.init(value: .searchRoot(.fetchDatabaseInfos))) - } - case .setting: - if state.settingState.route != nil { - effects.append(.init(value: .setting(.setNavigation(nil)))) - effects.append(hapticEffect) - } - } - if [.home, .search].contains(type) { - effects.append(hapticEffect) - } - } - if type == .setting && environment.deviceClient.isPad() { - effects.append(.init(value: .appRoute(.setNavigation(.setting)))) - } - return effects.isEmpty ? .none : .merge(effects) - - case .tabBar: - return .none - - case .home(.watched(.onNotLoginViewButtonTapped)), .favorites(.onNotLoginViewButtonTapped): - var effects: [EffectTask] = [ - environment.hapticsClient.generateFeedback(.soft).fireAndForget(), - .init(value: .tabBar(.setTabBarItemType(.setting))) - ] - effects.append(.init(value: .setting(.setNavigation(.account)))) - if !environment.cookieClient.didLogin { - effects.append( - .init(value: .setting(.account(.setNavigation(.login)))) - .delay( - for: .milliseconds(environment.deviceClient.isPad() ? 1200 : 200), - scheduler: DispatchQueue.main - ) - .eraseToEffect() - ) - } - return .merge(effects) - - case .home: - return .none - - case .favorites: - return .none - - case .searchRoot: - return .none - - case .setting(.loadUserSettingsDone): - var effects = [EffectTask]() - let threshold = state.settingState.setting.autoLockPolicy.rawValue - let blurRadius = state.settingState.setting.backgroundBlurRadius - if threshold >= 0 { - state.appLockState.becameInactiveDate = .distantPast - effects.append(.init(value: .appLock(.onBecomeActive(threshold, blurRadius)))) - } - if state.settingState.setting.detectsLinksFromClipboard { - effects.append(.init(value: .appRoute(.detectClipboardURL))) - } - return effects.isEmpty ? .none : .merge(effects) - - case .setting(.fetchGreetingDone(let result)): - return .init(value: .appRoute(.fetchGreetingDone(result))) - - case .setting: - return .none - } -} -.binding() - -let appReducer = Reducer.combine( - appReducerCore -// , -// appRouteReducer.pullback( -// state: \.appRouteState, -// action: /AppAction.appRoute, -// environment: { -// .init( -// dfClient: $0.dfClient, -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// loggerClient: $0.loggerClient, -// hapticsClient: $0.hapticsClient, -// libraryClient: $0.libraryClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// userDefaultsClient: $0.userDefaultsClient, -// uiApplicationClient: $0.uiApplicationClient, -// authorizationClient: $0.authorizationClient -// ) -// } -// ), -// appLockReducer.pullback( -// state: \.appLockState, -// action: /AppAction.appLock, -// environment: { -// .init( -// authorizationClient: $0.authorizationClient -// ) -// } -// ), -// appDelegateReducer.pullback( -// state: \.appDelegateState, -// action: /AppAction.appDelegate, -// environment: { -// .init( -// dfClient: $0.dfClient, -// libraryClient: $0.libraryClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient -// ) -// } -// ), -// tabBarReducer.pullback( -// state: \.tabBarState, -// action: /AppAction.tabBar, -// environment: { -// .init( -// deviceClient: $0.deviceClient -// ) -// } -// ), -// homeReducer.pullback( -// state: \.homeState, -// action: /AppAction.home, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// libraryClient: $0.libraryClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ), -// favoritesReducer.pullback( -// state: \.favoritesState, -// action: /AppAction.favorites, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// userDefaultsClient: $0.userDefaultsClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ), -// searchRootReducer.pullback( -// state: \.searchRootState, -// action: /AppAction.searchRoot, -// environment: { -// .init( -// urlClient: $0.urlClient, -// fileClient: $0.fileClient, -// imageClient: $0.imageClient, -// deviceClient: $0.deviceClient, -// hapticsClient: $0.hapticsClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// uiApplicationClient: $0.uiApplicationClient -// ) -// } -// ) -// , -// settingReducer.pullback( -// state: \.settingState, -// action: /AppAction.setting, -// environment: { -// .init( -// dfClient: $0.dfClient, -// fileClient: $0.fileClient, -// deviceClient: $0.deviceClient, -// loggerClient: $0.loggerClient, -// hapticsClient: $0.hapticsClient, -// libraryClient: $0.libraryClient, -// cookieClient: $0.cookieClient, -// databaseClient: $0.databaseClient, -// clipboardClient: $0.clipboardClient, -// appDelegateClient: $0.appDelegateClient, -// userDefaultsClient: $0.userDefaultsClient, -// uiApplicationClient: $0.uiApplicationClient, -// authorizationClient: $0.authorizationClient -// ) -// } -// ) -) -.logging() diff --git a/EhPanda/View/TabBar/TabBarView.swift b/EhPanda/View/TabBar/TabBarView.swift index 95486b5a..5c4a83a3 100644 --- a/EhPanda/View/TabBar/TabBarView.swift +++ b/EhPanda/View/TabBar/TabBarView.swift @@ -11,10 +11,10 @@ import ComposableArchitecture struct TabBarView: View { @Environment(\.scenePhase) private var scenePhase - private let store: Store - @ObservedObject private var viewStore: ViewStore + private let store: StoreOf + @ObservedObject private var viewStore: ViewStoreOf - init(store: Store) { + init(store: StoreOf) { self.store = store viewStore = ViewStore(store) } @@ -32,7 +32,7 @@ struct TabBarView: View { switch type { case .home: HomeView( - store: store.scope(state: \.homeState, action: AppAction.home), + store: store.scope(state: \.homeState, action: AppReducer.Action.home), user: viewStore.settingState.user, setting: viewStore.binding(\.settingState.$setting), blurRadius: viewStore.appLockState.blurRadius, @@ -40,7 +40,7 @@ struct TabBarView: View { ) case .favorites: FavoritesView( - store: store.scope(state: \.favoritesState, action: AppAction.favorites), + store: store.scope(state: \.favoritesState, action: AppReducer.Action.favorites), user: viewStore.settingState.user, setting: viewStore.binding(\.settingState.$setting), blurRadius: viewStore.appLockState.blurRadius, @@ -48,7 +48,7 @@ struct TabBarView: View { ) case .search: SearchRootView( - store: store.scope(state: \.searchRootState, action: AppAction.searchRoot), + store: store.scope(state: \.searchRootState, action: AppReducer.Action.searchRoot), user: viewStore.settingState.user, setting: viewStore.binding(\.settingState.$setting), blurRadius: viewStore.appLockState.blurRadius, @@ -56,7 +56,7 @@ struct TabBarView: View { ) case .setting: SettingView( - store: store.scope(state: \.settingState, action: AppAction.setting), + store: store.scope(state: \.settingState, action: AppReducer.Action.setting), blurRadius: viewStore.appLockState.blurRadius ) } @@ -79,7 +79,7 @@ struct TabBarView: View { } .sheet(unwrapping: viewStore.binding(\.appRouteState.$route), case: /AppRouteReducer.Route.setting) { _ in SettingView( - store: store.scope(state: \.settingState, action: AppAction.setting), + store: store.scope(state: \.settingState, action: AppReducer.Action.setting), blurRadius: viewStore.appLockState.blurRadius ) .accentColor(viewStore.settingState.setting.accentColor) @@ -88,7 +88,10 @@ struct TabBarView: View { .sheet(unwrapping: viewStore.binding(\.appRouteState.$route), case: /AppRouteReducer.Route.detail) { route in NavigationView { DetailView( - store: store.scope(state: \.appRouteState.detailState, action: { AppAction.appRoute(.detail($0)) }), + store: store.scope( + state: \.appRouteState.detailState, + action: { AppReducer.Action.appRoute(.detail($0)) } + ), gid: route.wrappedValue, user: viewStore.settingState.user, setting: viewStore.binding(\.settingState.$setting), blurRadius: viewStore.appLockState.blurRadius, @@ -155,24 +158,7 @@ struct TabBarView_Previews: PreviewProvider { TabBarView( store: .init( initialState: .init(), - reducer: appReducer, - environment: AppEnvironment( - dfClient: .live, - urlClient: .live, - fileClient: .live, - imageClient: .live, - deviceClient: .live, - loggerClient: .live, - hapticsClient: .live, - libraryClient: .live, - cookieClient: .live, - databaseClient: .live, - clipboardClient: .live, - appDelegateClient: .live, - userDefaultsClient: .live, - uiApplicationClient: .live, - authorizationClient: .live - ) + reducer: AppReducer() ) ) } From 845b3764d1919f6ca94ffc2133d886a4a1ad7f1f Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 01:34:04 +0800 Subject: [PATCH 21/29] Some refactoring work --- .../xcshareddata/swiftpm/Package.resolved | 28 +++++------ EhPanda/App/EhPandaApp.swift | 2 +- .../App/Tools/Clients/AppDelegateClient.swift | 6 +-- EhPanda/App/Tools/Clients/HapticsClient.swift | 16 +++---- EhPanda/App/Tools/Extensions/Extensions.swift | 1 + .../Tools/Extensions/Reducer_Extension.swift | 48 +++---------------- EhPanda/DataFlow/AppDelegateReducer.swift | 3 +- EhPanda/DataFlow/AppReducer.swift | 5 +- .../Detail/Archives/ArchivesReducer.swift | 14 +++--- .../Detail/Comments/CommentsReducer.swift | 14 +++--- EhPanda/View/Detail/DetailReducer.swift | 30 ++++++------ .../DetailSearch/DetailSearchReducer.swift | 10 ++-- .../GalleryInfos/GalleryInfosReducer.swift | 2 +- .../Detail/Previews/PreviewsReducer.swift | 10 ++-- .../Detail/Torrents/TorrentsReducer.swift | 12 ++--- .../Home/Frontpage/FrontpageReducer.swift | 10 ++-- .../View/Home/Popular/PopularReducer.swift | 8 ++-- .../View/Home/Toplists/ToplistsReducer.swift | 14 +++--- .../View/Home/Watched/WatchedReducer.swift | 10 ++-- EhPanda/View/Reading/ReadingReducer.swift | 36 +++++++++----- EhPanda/View/Search/SearchReducer.swift | 10 ++-- .../Search/Support/QuickSearchReducer.swift | 8 ++-- .../AccountSettingReducer.swift | 2 +- .../Setting/EhSetting/EhSettingReducer.swift | 12 ++--- EhPanda/View/Setting/Login/LoginReducer.swift | 12 +++-- EhPanda/View/Setting/Logs/LogsReducer.swift | 8 ++-- EhPanda/View/Setting/SettingReducer.swift | 2 +- 27 files changed, 152 insertions(+), 181 deletions(-) diff --git a/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c1c2905d..1ccc3979 100644 --- a/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab", - "version" : "0.9.1" + "revision" : "0625932976b3ae23949f6b816d13bd97f3b40b7c", + "version" : "0.10.0" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Kingfisher.git", "state" : { - "revision" : "af4be924ad984cf4d16f4ae4df424e79a443d435", - "version" : "7.6.2" + "revision" : "e8625b80c413457b13ea9be75d07f6e9f5830c19", + "version" : "7.7.0" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "20b25ca0dd88ebfb9111ec937814ddc5a8880172", - "version" : "0.2.0" + "revision" : "f9acfa1a45f4483fe0f2c434a74e6f68f865d12d", + "version" : "0.3.0" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", "state" : { - "revision" : "3e8eee1efe99d06e99426d421733b858b332186b", - "version" : "0.52.0" + "revision" : "b6559103c7867821b3848afe29afc1a386ae83f1", + "version" : "0.53.2" } }, { @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "805c57f32a5934ee420f30ba129f00aa8c7575a1", - "version" : "0.10.0" + "revision" : "505aa98716275fbd045d8f934fee3337c82ffbd3", + "version" : "0.10.3" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "6bb1034e8a1bfbf46dfb766b6c09b7b17e1cba10", - "version" : "0.2.0" + "revision" : "25c9b6789b4b7ada649a3808e6d8de1489082a33", + "version" : "0.5.0" } }, { @@ -212,8 +212,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "ab8c9f45843694dd16be4297e6d44c0634fd9913", - "version" : "0.8.4" + "revision" : "4af50b38daf0037cfbab15514a241224c3f62f98", + "version" : "0.8.5" } } ], diff --git a/EhPanda/App/EhPandaApp.swift b/EhPanda/App/EhPandaApp.swift index 5e10e9ea..208f0d3a 100644 --- a/EhPanda/App/EhPandaApp.swift +++ b/EhPanda/App/EhPandaApp.swift @@ -14,7 +14,7 @@ import ComposableArchitecture var body: some Scene { WindowGroup { WithViewStore( - appDelegate.store.scope(state: \.appDelegateState.migrationState.databaseState) + appDelegate.store, observe: \.appDelegateState.migrationState.databaseState ) { viewStore in ZStack { if viewStore.state == .idle { diff --git a/EhPanda/App/Tools/Clients/AppDelegateClient.swift b/EhPanda/App/Tools/Clients/AppDelegateClient.swift index 739f8da7..a904cca7 100644 --- a/EhPanda/App/Tools/Clients/AppDelegateClient.swift +++ b/EhPanda/App/Tools/Clients/AppDelegateClient.swift @@ -9,15 +9,15 @@ import SwiftUI import ComposableArchitecture struct AppDelegateClient { - let setOrientation: (UIInterfaceOrientation) -> EffectTask + let setOrientation: (UIInterfaceOrientationMask) -> EffectTask let setOrientationMask: (UIInterfaceOrientationMask) -> EffectTask } extension AppDelegateClient { static let live: Self = .init( - setOrientation: { orientation in + setOrientation: { mask in .fireAndForget { - UIDevice.current.setValue(orientation.rawValue, forKey: "orientation") + DeviceUtil.keyWindow?.windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: mask)) } }, setOrientationMask: { mask in diff --git a/EhPanda/App/Tools/Clients/HapticsClient.swift b/EhPanda/App/Tools/Clients/HapticsClient.swift index 591ea4d9..4fe2a7d5 100644 --- a/EhPanda/App/Tools/Clients/HapticsClient.swift +++ b/EhPanda/App/Tools/Clients/HapticsClient.swift @@ -9,21 +9,17 @@ import SwiftUI import ComposableArchitecture struct HapticsClient { - let generateFeedback: (UIImpactFeedbackGenerator.FeedbackStyle) -> EffectTask - let generateNotificationFeedback: (UINotificationFeedbackGenerator.FeedbackType) -> EffectTask + let generateFeedback: (UIImpactFeedbackGenerator.FeedbackStyle) -> Void + let generateNotificationFeedback: (UINotificationFeedbackGenerator.FeedbackType) -> Void } extension HapticsClient { static let live: Self = .init( generateFeedback: { style in - .fireAndForget { - HapticsUtil.generateFeedback(style: style) - } + HapticsUtil.generateFeedback(style: style) }, generateNotificationFeedback: { style in - .fireAndForget { - HapticsUtil.generateNotificationFeedback(style: style) - } + HapticsUtil.generateNotificationFeedback(style: style) } ) } @@ -45,8 +41,8 @@ extension DependencyValues { // MARK: Test extension HapticsClient { static let noop: Self = .init( - generateFeedback: { _ in .none }, - generateNotificationFeedback: { _ in .none } + generateFeedback: { _ in }, + generateNotificationFeedback: { _ in } ) static let unimplemented: Self = .init( diff --git a/EhPanda/App/Tools/Extensions/Extensions.swift b/EhPanda/App/Tools/Extensions/Extensions.swift index 7b49ca9c..b82d300d 100644 --- a/EhPanda/App/Tools/Extensions/Extensions.swift +++ b/EhPanda/App/Tools/Extensions/Extensions.swift @@ -67,6 +67,7 @@ extension URL { } func appending(queryItems: [URLQueryItem]) -> URL { + guard !queryItems.isEmpty else { return self } var components: URLComponents = .init( url: self, resolvingAgainstBaseURL: false ) diff --git a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift index 78c926a4..03d157fa 100644 --- a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift +++ b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift @@ -8,46 +8,6 @@ import SwiftUI import ComposableArchitecture -// MARK: Logging -extension Reducer { - func logging() -> Self { - .init { state, action, environment in - Logger.info(action) - return run(&state, action, environment) - } - } -} - -// MARK: Haptic -extension Reducer { - func onBecomeNonNil( - unwrapping enum: @escaping (State) -> Enum?, - case casePath: CasePath, - perform additionalEffects: @escaping (inout State, Action, Environment) - -> EffectTask - ) -> Self { - .init { state, action, environment in - let previousCase = Binding.constant(`enum`(state)).case(casePath).wrappedValue - let effects = run(&state, action, environment) - let currentCase = Binding.constant(`enum`(state)).case(casePath).wrappedValue - - return previousCase == nil && currentCase != nil - ? .merge(effects, additionalEffects(&state, action, environment)) - : effects - } - } - func haptics( - unwrapping enum: @escaping (State) -> Enum?, - case casePath: CasePath, - hapticsClient: @escaping (Environment) -> HapticsClient, - style: UIImpactFeedbackGenerator.FeedbackStyle = .light - ) -> Self { - onBecomeNonNil(unwrapping: `enum`, case: casePath) { - hapticsClient($2).generateFeedback(style).fireAndForget() - } - } -} - extension ReducerProtocol { func haptics( unwrapping enum: @escaping (State) -> Enum?, @@ -60,7 +20,7 @@ extension ReducerProtocol { } } - func onBecomeNonNil( + private func onBecomeNonNil( unwrapping enum: @escaping (State) -> Enum?, case casePath: CasePath, perform additionalEffects: @escaping (inout State, Action) -> EffectTask @@ -107,7 +67,11 @@ where State == Base.State, Action == Base.Action { @ReducerBuilder var body: some ReducerProtocol { Reduce { state, action in - Logger.info(action) + if case .setting(.binding(let bindingAction)) = action as? AppReducer.Action { + Logger.info("setting(EhPanda.SettingReducer.Action.\(bindingAction.customDumpMirror.subjectType)") + } else { + Logger.info(action) + } return base.reduce(into: &state, action: action) } } diff --git a/EhPanda/DataFlow/AppDelegateReducer.swift b/EhPanda/DataFlow/AppDelegateReducer.swift index e10ba75d..ea44b11f 100644 --- a/EhPanda/DataFlow/AppDelegateReducer.swift +++ b/EhPanda/DataFlow/AppDelegateReducer.swift @@ -58,8 +58,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ) lazy var viewStore = ViewStore(store) - static var orientationMask: UIInterfaceOrientationMask = - DeviceUtil.isPad ? .all : [.portrait, .portraitUpsideDown] + static var orientationMask: UIInterfaceOrientationMask = DeviceUtil.isPad ? .all : [.portrait, .portraitUpsideDown] func application( _ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow? diff --git a/EhPanda/DataFlow/AppReducer.swift b/EhPanda/DataFlow/AppReducer.swift index 2e20b14a..417d225f 100644 --- a/EhPanda/DataFlow/AppReducer.swift +++ b/EhPanda/DataFlow/AppReducer.swift @@ -101,8 +101,7 @@ struct AppReducer: ReducerProtocol { case .tabBar(.setTabBarItemType(let type)): var effects = [EffectTask]() - let hapticEffect: EffectTask = hapticsClient - .generateFeedback(.soft).fireAndForget() + let hapticEffect: EffectTask = .fireAndForget({ hapticsClient.generateFeedback(.soft) }) if type == state.tabBarState.tabBarItemType { switch type { case .home: @@ -145,7 +144,7 @@ struct AppReducer: ReducerProtocol { case .home(.watched(.onNotLoginViewButtonTapped)), .favorites(.onNotLoginViewButtonTapped): var effects: [EffectTask] = [ - hapticsClient.generateFeedback(.soft).fireAndForget(), + .fireAndForget({ hapticsClient.generateFeedback(.soft) }), .init(value: .tabBar(.setTabBarItemType(.setting))) ] effects.append(.init(value: .setting(.setNavigation(.account)))) diff --git a/EhPanda/View/Detail/Archives/ArchivesReducer.swift b/EhPanda/View/Detail/Archives/ArchivesReducer.swift index a6b2e3df..b6e537d0 100644 --- a/EhPanda/View/Detail/Archives/ArchivesReducer.swift +++ b/EhPanda/View/Detail/Archives/ArchivesReducer.swift @@ -15,8 +15,8 @@ struct ArchivesReducer: ReducerProtocol { case communicatingHUD } - struct CancelID: Hashable { - let id = String(describing: ArchivesReducer.self) + private enum CancelID: CaseIterable { + case fetchArchive, fetchArchiveFunds, fetchDownloadResponse } struct State: Equatable { @@ -64,14 +64,14 @@ struct ArchivesReducer: ReducerProtocol { .updateGalleryFunds(galleryPoints: galleryPoints, credits: credits).fireAndForget() case .teardown: - return .cancel(id: CancelID()) + return .cancel(ids: CancelID.allCases) case .fetchArchive(let gid, let galleryURL, let archiveURL): guard state.loadingState != .loading else { return .none } state.loadingState = .loading return GalleryArchiveRequest(archiveURL: archiveURL) .effect.map({ Action.fetchArchiveDone(gid, galleryURL, $0) }) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchArchive) case .fetchArchiveDone(let gid, let galleryURL, let result): state.loadingState = .idle @@ -97,7 +97,7 @@ struct ArchivesReducer: ReducerProtocol { case .fetchArchiveFunds(let gid, let galleryURL): guard let galleryURL = galleryURL.replaceHost(to: Defaults.URL.ehentai.host) else { return .none } return GalleryArchiveFundsRequest(gid: gid, galleryURL: galleryURL) - .effect.map(Action.fetchArchiveFundsDone).cancellable(id: CancelID()) + .effect.map(Action.fetchArchiveFundsDone).cancellable(id: CancelID.fetchArchiveFunds) case .fetchArchiveFundsDone(let result): if case .success(let (galleryPoints, credits)) = result { @@ -113,7 +113,7 @@ struct ArchivesReducer: ReducerProtocol { return SendDownloadCommandRequest( archiveURL: archiveURL, resolution: selectedArchive.resolution.parameter ) - .effect.map(Action.fetchDownloadResponseDone).cancellable(id: CancelID()) + .effect.map(Action.fetchDownloadResponseDone).cancellable(id: CancelID.fetchDownloadResponse) case .fetchDownloadResponseDone(let result): state.route = .messageHUD @@ -138,7 +138,7 @@ struct ArchivesReducer: ReducerProtocol { state.messageHUDConfig = .error isSuccess = false } - return hapticsClient.generateNotificationFeedback(isSuccess ? .success : .error).fireAndForget() + return .fireAndForget({ hapticsClient.generateNotificationFeedback(isSuccess ? .success : .error) }) } } diff --git a/EhPanda/View/Detail/Comments/CommentsReducer.swift b/EhPanda/View/Detail/Comments/CommentsReducer.swift index 2f94d736..dc69968c 100644 --- a/EhPanda/View/Detail/Comments/CommentsReducer.swift +++ b/EhPanda/View/Detail/Comments/CommentsReducer.swift @@ -16,8 +16,8 @@ struct CommentsReducer: ReducerProtocol { case postComment(String) } - struct CancelID: Hashable { - let id = String(describing: CommentsReducer.self) + private enum CancelID: CaseIterable { + case postComment, voteComment, fetchGallery } struct State: Equatable { @@ -166,7 +166,7 @@ struct CommentsReducer: ReducerProtocol { .updateReadingProgress(gid: gid, progress: progress).fireAndForget() case .teardown: - return .cancel(id: CancelID()) + return .cancel(ids: CancelID.allCases) case .postComment(let galleryURL, let commentID): guard !state.commentContent.isEmpty else { return .none } @@ -174,10 +174,10 @@ struct CommentsReducer: ReducerProtocol { return EditGalleryCommentRequest( commentID: commentID, content: state.commentContent, galleryURL: galleryURL ) - .effect.map(Action.performCommentActionDone).cancellable(id: CancelID()) + .effect.map(Action.performCommentActionDone).cancellable(id: CancelID.postComment) } else { return CommentGalleryRequest(content: state.commentContent, galleryURL: galleryURL) - .effect.map(Action.performCommentActionDone).cancellable(id: CancelID()) + .effect.map(Action.performCommentActionDone).cancellable(id: CancelID.postComment) } case .voteComment(let gid, let token, let apiKey, let commentID, let vote): @@ -188,7 +188,7 @@ struct CommentsReducer: ReducerProtocol { apiuid: apiuid, apikey: apiKey, gid: gid, token: token, commentID: commentID, commentVote: vote ) - .effect.map(Action.performCommentActionDone).cancellable(id: CancelID()) + .effect.map(Action.performCommentActionDone).cancellable(id: CancelID.voteComment) case .performCommentActionDone: return .none @@ -196,7 +196,7 @@ struct CommentsReducer: ReducerProtocol { case .fetchGallery(let url, let isGalleryImageURL): state.route = .hud return GalleryReverseRequest(url: url, isGalleryImageURL: isGalleryImageURL) - .effect.map({ Action.fetchGalleryDone(url, $0) }).cancellable(id: CancelID()) + .effect.map({ Action.fetchGalleryDone(url, $0) }).cancellable(id: CancelID.fetchGallery) case .fetchGalleryDone(let url, let result): state.route = nil diff --git a/EhPanda/View/Detail/DetailReducer.swift b/EhPanda/View/Detail/DetailReducer.swift index 658f5bf7..2074f317 100644 --- a/EhPanda/View/Detail/DetailReducer.swift +++ b/EhPanda/View/Detail/DetailReducer.swift @@ -24,8 +24,8 @@ struct DetailReducer: ReducerProtocol { case galleryInfos(Gallery, GalleryDetail) } - struct CancelID: Hashable { - let id = String(describing: DetailReducer.self) + private enum CancelID: CaseIterable { + case fetchDatabaseInfos, fetchGalleryDetail, rateGallery, favorGallery, unfavorGallery, postComment, voteTag } struct State: Equatable { @@ -164,11 +164,11 @@ struct DetailReducer: ReducerProtocol { case .toggleShowFullTitle: state.showsFullTitle.toggle() - return hapticsClient.generateFeedback(.soft).fireAndForget() + return .fireAndForget({ hapticsClient.generateFeedback(.soft) }) case .toggleShowUserRating: state.showsUserRating.toggle() - return hapticsClient.generateFeedback(.soft).fireAndForget() + return .fireAndForget({ hapticsClient.generateFeedback(.soft) }) case .setCommentContent(let content): state.commentContent = content @@ -186,7 +186,7 @@ struct DetailReducer: ReducerProtocol { state.updateRating(value: value) return .merge( .init(value: .rateGallery), - hapticsClient.generateFeedback(.soft).fireAndForget(), + .fireAndForget({ hapticsClient.generateFeedback(.soft) }), .init(value: .confirmRatingDone).delay(for: 1, scheduler: DispatchQueue.main).eraseToEffect() ) @@ -225,7 +225,7 @@ struct DetailReducer: ReducerProtocol { .updateReadingProgress(gid: state.gallery.id, progress: progress).fireAndForget() case .teardown: - return .cancel(id: CancelID()) + return .cancel(ids: CancelID.allCases) case .fetchDatabaseInfos(let gid): guard let gallery = databaseClient.fetchGallery(gid: gid) else { return .none } @@ -236,7 +236,7 @@ struct DetailReducer: ReducerProtocol { return .merge( .init(value: .saveGalleryHistory), databaseClient.fetchGalleryState(gid: state.gallery.id) - .map(Action.fetchDatabaseInfosDone).cancellable(id: CancelID()) + .map(Action.fetchDatabaseInfosDone).cancellable(id: CancelID.fetchDatabaseInfos) ) case .fetchDatabaseInfosDone(let galleryState): @@ -251,7 +251,7 @@ struct DetailReducer: ReducerProtocol { else { return .none } state.loadingState = .loading return GalleryDetailRequest(gid: state.gallery.id, galleryURL: galleryURL) - .effect.map(Action.fetchGalleryDetailDone).cancellable(id: CancelID()) + .effect.map(Action.fetchGalleryDetailDone).cancellable(id: CancelID.fetchGalleryDetail) case .fetchGalleryDetailDone(let result): state.loadingState = .idle @@ -291,20 +291,20 @@ struct DetailReducer: ReducerProtocol { apiuid: apiuid, apikey: state.apiKey, gid: gid, token: state.gallery.token, rating: state.userRating ) - .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID()) + .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID.rateGallery) case .favorGallery(let favIndex): return FavorGalleryRequest(gid: state.gallery.id, token: state.gallery.token, favIndex: favIndex) - .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID()) + .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID.favorGallery) case .unfavorGallery: return UnfavorGalleryRequest(gid: state.gallery.id).effect.map(Action.anyGalleryOpsDone) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.unfavorGallery) case .postComment(let galleryURL): guard !state.commentContent.isEmpty else { return .none } return CommentGalleryRequest(content: state.commentContent, galleryURL: galleryURL) - .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID()) + .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID.postComment) case .voteTag(let tag, let vote): guard let apiuid = Int(cookieClient.apiuid), let gid = Int(state.gallery.id) @@ -312,16 +312,16 @@ struct DetailReducer: ReducerProtocol { return VoteGalleryTagRequest( apiuid: apiuid, apikey: state.apiKey, gid: gid, token: state.gallery.token, tag: tag, vote: vote ) - .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID()) + .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID.voteTag) case .anyGalleryOpsDone(let result): if case .success = result { return .merge( .init(value: .fetchGalleryDetail), - hapticsClient.generateNotificationFeedback(.success).fireAndForget() + .fireAndForget({ hapticsClient.generateNotificationFeedback(.success) }) ) } - return hapticsClient.generateNotificationFeedback(.error).fireAndForget() + return .fireAndForget({ hapticsClient.generateNotificationFeedback(.error) }) case .reading(.onPerformDismiss): return .init(value: .setNavigation(nil)) diff --git a/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift b/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift index d07fa724..cfc371ad 100644 --- a/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift +++ b/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift @@ -14,8 +14,8 @@ struct DetailSearchReducer: ReducerProtocol { case detail(String) } - struct CancelID: Hashable { - let id = String(describing: DetailSearchReducer.self) + private enum CancelID: CaseIterable { + case fetchGalleries, fetchMoreGalleries } struct State: Equatable { @@ -93,7 +93,7 @@ struct DetailSearchReducer: ReducerProtocol { ) case .teardown: - return .cancel(id: CancelID()) + return .cancel(ids: CancelID.allCases) case .fetchGalleries(let keyword): guard state.loadingState != .loading else { return .none } @@ -105,7 +105,7 @@ struct DetailSearchReducer: ReducerProtocol { state.pageNumber.resetPages() let filter = databaseClient.fetchFilterSynchronously(range: .search) return SearchGalleriesRequest(keyword: state.lastKeyword, filter: filter).effect - .map(Action.fetchGalleriesDone).cancellable(id: CancelID()) + .map(Action.fetchGalleriesDone).cancellable(id: CancelID.fetchGalleries) case .fetchGalleriesDone(let result): state.loadingState = .idle @@ -134,7 +134,7 @@ struct DetailSearchReducer: ReducerProtocol { let filter = databaseClient.fetchFilterSynchronously(range: .search) return MoreSearchGalleriesRequest(keyword: state.lastKeyword, filter: filter, lastID: lastID).effect .map(Action.fetchMoreGalleriesDone) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchMoreGalleries) case .fetchMoreGalleriesDone(let result): state.footerLoadingState = .idle diff --git a/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift b/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift index d7cbb2d2..f0da6117 100644 --- a/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift +++ b/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift @@ -36,7 +36,7 @@ struct GalleryInfosReducer: ReducerProtocol { state.route = .hud return .merge( clipboardClient.saveText(text).fireAndForget(), - hapticsClient.generateNotificationFeedback(.success).fireAndForget() + .fireAndForget({ hapticsClient.generateNotificationFeedback(.success) }) ) } } diff --git a/EhPanda/View/Detail/Previews/PreviewsReducer.swift b/EhPanda/View/Detail/Previews/PreviewsReducer.swift index 90ca28db..581982a8 100644 --- a/EhPanda/View/Detail/Previews/PreviewsReducer.swift +++ b/EhPanda/View/Detail/Previews/PreviewsReducer.swift @@ -13,8 +13,8 @@ struct PreviewsReducer: ReducerProtocol { case reading } - struct CancelID: Hashable { - let id = String(describing: PreviewsReducer.self) + private enum CancelID: CaseIterable { + case fetchDatabaseInfos, fetchPreviewURLs } struct State: Equatable { @@ -82,13 +82,13 @@ struct PreviewsReducer: ReducerProtocol { .updateReadingProgress(gid: state.gallery.id, progress: progress).fireAndForget() case .teardown: - return .cancel(id: CancelID()) + return .cancel(ids: CancelID.allCases) case .fetchDatabaseInfos(let gid): guard let gallery = databaseClient.fetchGallery(gid: gid) else { return .none } state.gallery = gallery return databaseClient.fetchGalleryState(gid: state.gallery.id) - .map(Action.fetchDatabaseInfosDone).cancellable(id: CancelID()) + .map(Action.fetchDatabaseInfosDone).cancellable(id: CancelID.fetchDatabaseInfos) case .fetchDatabaseInfosDone(let galleryState): if let previewConfig = galleryState.previewConfig { @@ -105,7 +105,7 @@ struct PreviewsReducer: ReducerProtocol { state.loadingState = .loading let pageNum = state.previewConfig.pageNumber(index: index) return GalleryPreviewURLsRequest(galleryURL: galleryURL, pageNum: pageNum) - .effect.map(Action.fetchPreviewURLsDone).cancellable(id: CancelID()) + .effect.map(Action.fetchPreviewURLsDone).cancellable(id: CancelID.fetchPreviewURLs) case .fetchPreviewURLsDone(let result): state.loadingState = .idle diff --git a/EhPanda/View/Detail/Torrents/TorrentsReducer.swift b/EhPanda/View/Detail/Torrents/TorrentsReducer.swift index b05a2faf..b6285b77 100644 --- a/EhPanda/View/Detail/Torrents/TorrentsReducer.swift +++ b/EhPanda/View/Detail/Torrents/TorrentsReducer.swift @@ -15,8 +15,8 @@ struct TorrentsReducer: ReducerProtocol { case share(URL) } - struct CancelID: Hashable { - let id = String(describing: TorrentsReducer.self) + private enum CancelID: CaseIterable { + case fetchTorrent, fetchGalleryTorrents } struct State: Equatable { @@ -58,7 +58,7 @@ struct TorrentsReducer: ReducerProtocol { state.route = .hud return .merge( clipboardClient.saveText(magnetURL).fireAndForget(), - hapticsClient.generateNotificationFeedback(.success).fireAndForget() + .fireAndForget({ hapticsClient.generateNotificationFeedback(.success) }) ) case .presentTorrentActivity(let hash, let data): @@ -69,10 +69,10 @@ struct TorrentsReducer: ReducerProtocol { case .fetchTorrent(let hash, let torrentURL): return DataRequest(url: torrentURL).effect.map({ Action.fetchTorrentDone(hash, $0) }) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchTorrent) case .teardown: - return .cancel(id: CancelID()) + return .cancel(ids: CancelID.allCases) case .fetchTorrentDone(let hash, let result): if case .success(let data) = result, !data.isEmpty { @@ -84,7 +84,7 @@ struct TorrentsReducer: ReducerProtocol { guard state.loadingState != .loading else { return .none } state.loadingState = .loading return GalleryTorrentsRequest(gid: gid, token: token) - .effect.map(Action.fetchGalleryTorrentsDone).cancellable(id: CancelID()) + .effect.map(Action.fetchGalleryTorrentsDone).cancellable(id: CancelID.fetchGalleryTorrents) case .fetchGalleryTorrentsDone(let result): state.loadingState = .idle diff --git a/EhPanda/View/Home/Frontpage/FrontpageReducer.swift b/EhPanda/View/Home/Frontpage/FrontpageReducer.swift index 99bd66ca..3c94b90b 100644 --- a/EhPanda/View/Home/Frontpage/FrontpageReducer.swift +++ b/EhPanda/View/Home/Frontpage/FrontpageReducer.swift @@ -13,8 +13,8 @@ struct FrontpageReducer: ReducerProtocol { case detail(String) } - struct CancelID: Hashable { - let id = String(describing: FrontpageReducer.self) + private enum CancelID: CaseIterable { + case fetchGalleries, fetchMoreGalleries } struct State: Equatable { @@ -83,7 +83,7 @@ struct FrontpageReducer: ReducerProtocol { return .init(value: .detail(.teardown)) case .teardown: - return .cancel(id: CancelID()) + return .cancel(ids: CancelID.allCases) case .fetchGalleries: guard state.loadingState != .loading else { return .none } @@ -92,7 +92,7 @@ struct FrontpageReducer: ReducerProtocol { let filter = databaseClient.fetchFilterSynchronously(range: .global) return FrontpageGalleriesRequest(filter: filter).effect .map(Action.fetchGalleriesDone) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchGalleries) case .fetchGalleriesDone(let result): state.loadingState = .idle @@ -121,7 +121,7 @@ struct FrontpageReducer: ReducerProtocol { let filter = databaseClient.fetchFilterSynchronously(range: .global) return MoreFrontpageGalleriesRequest(filter: filter, lastID: lastID).effect .map(Action.fetchMoreGalleriesDone) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchMoreGalleries) case .fetchMoreGalleriesDone(let result): state.footerLoadingState = .idle diff --git a/EhPanda/View/Home/Popular/PopularReducer.swift b/EhPanda/View/Home/Popular/PopularReducer.swift index 6996b01c..045d09f6 100644 --- a/EhPanda/View/Home/Popular/PopularReducer.swift +++ b/EhPanda/View/Home/Popular/PopularReducer.swift @@ -13,8 +13,8 @@ struct PopularReducer: ReducerProtocol { case detail(String) } - struct CancelID: Hashable { - let id = String(describing: PopularReducer.self) + private enum CancelID { + case fetchGalleries } struct State: Equatable { @@ -71,14 +71,14 @@ struct PopularReducer: ReducerProtocol { return .init(value: .detail(.teardown)) case .teardown: - return .cancel(id: CancelID()) + return .cancel(id: CancelID.fetchGalleries) case .fetchGalleries: guard state.loadingState != .loading else { return .none } state.loadingState = .loading let filter = databaseClient.fetchFilterSynchronously(range: .global) return PopularGalleriesRequest(filter: filter) - .effect.map(Action.fetchGalleriesDone).cancellable(id: CancelID()) + .effect.map(Action.fetchGalleriesDone).cancellable(id: CancelID.fetchGalleries) case .fetchGalleriesDone(let result): state.loadingState = .idle diff --git a/EhPanda/View/Home/Toplists/ToplistsReducer.swift b/EhPanda/View/Home/Toplists/ToplistsReducer.swift index a5fb30e2..f92631a3 100644 --- a/EhPanda/View/Home/Toplists/ToplistsReducer.swift +++ b/EhPanda/View/Home/Toplists/ToplistsReducer.swift @@ -12,8 +12,8 @@ struct ToplistsReducer: ReducerProtocol { case detail(String) } - struct CancelID: Hashable { - let id = String(describing: ToplistsReducer.self) + private enum CancelID: CaseIterable { + case fetchGalleries, fetchMoreGalleries } struct State: Equatable { @@ -117,20 +117,20 @@ struct ToplistsReducer: ReducerProtocol { guard let index = Int(state.jumpPageIndex), let pageNumber = state.pageNumber, index > 0, index <= pageNumber.maximum + 1 else { - return hapticsClient.generateNotificationFeedback(.error).fireAndForget() + return .fireAndForget({ hapticsClient.generateNotificationFeedback(.error) }) } return .init(value: .fetchGalleries(index - 1)) case .presentJumpPageAlert: state.jumpPageAlertPresented = true - return hapticsClient.generateFeedback(.light).fireAndForget() + return .fireAndForget({ hapticsClient.generateFeedback(.light) }) case .setJumpPageAlertFocused(let isFocused): state.jumpPageAlertFocused = isFocused return .none case .teardown: - return .cancel(id: CancelID()) + return .cancel(ids: CancelID.allCases) case .fetchGalleries(let pageNum): guard state.loadingState != .loading else { return .none } @@ -142,7 +142,7 @@ struct ToplistsReducer: ReducerProtocol { } return ToplistsGalleriesRequest(catIndex: state.type.categoryIndex, pageNum: pageNum) .effect.map({ [type = state.type] in Action.fetchGalleriesDone(type, $0) }) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchGalleries) case .fetchGalleriesDone(let type, let result): state.rawLoadingState[type] = .idle @@ -170,7 +170,7 @@ struct ToplistsReducer: ReducerProtocol { let pageNum = pageNumber.current + 1 return MoreToplistsGalleriesRequest(catIndex: state.type.categoryIndex, pageNum: pageNum) .effect.map({ [type = state.type] in Action.fetchMoreGalleriesDone(type, $0) }) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchMoreGalleries) case .fetchMoreGalleriesDone(let type, let result): state.rawFooterLoadingState[type] = .idle diff --git a/EhPanda/View/Home/Watched/WatchedReducer.swift b/EhPanda/View/Home/Watched/WatchedReducer.swift index 478fbfa9..e2edb13c 100644 --- a/EhPanda/View/Home/Watched/WatchedReducer.swift +++ b/EhPanda/View/Home/Watched/WatchedReducer.swift @@ -14,8 +14,8 @@ struct WatchedReducer: ReducerProtocol { case detail(String) } - struct CancelID: Hashable { - let id = String(describing: WatchedReducer.self) + private enum CancelID: CaseIterable { + case fetchGalleries, fetchMoreGalleries } struct State: Equatable { @@ -90,7 +90,7 @@ struct WatchedReducer: ReducerProtocol { return .none case .teardown: - return .cancel(id: CancelID()) + return .cancel(ids: CancelID.allCases) case .fetchGalleries(let keyword): guard state.loadingState != .loading else { return .none } @@ -101,7 +101,7 @@ struct WatchedReducer: ReducerProtocol { state.pageNumber.resetPages() let filter = databaseClient.fetchFilterSynchronously(range: .watched) return WatchedGalleriesRequest(filter: filter, keyword: state.keyword) - .effect.map(Action.fetchGalleriesDone).cancellable(id: CancelID()) + .effect.map(Action.fetchGalleriesDone).cancellable(id: CancelID.fetchGalleries) case .fetchGalleriesDone(let result): state.loadingState = .idle @@ -130,7 +130,7 @@ struct WatchedReducer: ReducerProtocol { let filter = databaseClient.fetchFilterSynchronously(range: .watched) return MoreWatchedGalleriesRequest(filter: filter, lastID: lastID, keyword: state.keyword).effect .map(Action.fetchMoreGalleriesDone) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchMoreGalleries) case .fetchMoreGalleriesDone(let result): state.footerLoadingState = .idle diff --git a/EhPanda/View/Reading/ReadingReducer.swift b/EhPanda/View/Reading/ReadingReducer.swift index c30d96dd..73b59bee 100644 --- a/EhPanda/View/Reading/ReadingReducer.swift +++ b/EhPanda/View/Reading/ReadingReducer.swift @@ -35,8 +35,15 @@ struct ReadingReducer: ReducerProtocol { case share(Bool) } - struct CancelID: Hashable { - let id = String(describing: ReadingReducer.self) + private enum CancelID: CaseIterable { + case fetchImage + case fetchDatabaseInfos + case fetchPreviewURLs + case fetchThumbnailURLs + case fetchNormalImageURLs + case refetchNormalImageURLs + case fetchMPVKeys + case fetchMPVImageURL } struct State: Equatable { @@ -181,7 +188,7 @@ struct ReadingReducer: ReducerProtocol { Reduce { state, action in switch action { case .binding(\.$showsSliderPreview): - return hapticsClient.generateFeedback(.soft).fireAndForget() + return .fireAndForget({ hapticsClient.generateFeedback(.soft) }) case .binding: return .none @@ -205,7 +212,7 @@ struct ReadingReducer: ReducerProtocol { return .merge(effects) case .onPerformDismiss: - return hapticsClient.generateFeedback(.light).fireAndForget() + return .fireAndForget({ hapticsClient.generateFeedback(.light) }) case .onAppear(let gid, let enablesLandscape): var effects: [EffectTask] = [ @@ -269,7 +276,7 @@ struct ReadingReducer: ReducerProtocol { case .fetchImage(let action, let imageURL): return imageClient.fetchImage(url: imageURL) .map({ Action.fetchImageDone(action, $0) }) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchImage) case .fetchImageDone(let action, let result): if case .success(let image) = result { @@ -314,7 +321,7 @@ struct ReadingReducer: ReducerProtocol { case .teardown: var effects: [EffectTask] = [ - .cancel(id: CancelID()) + .cancel(ids: CancelID.allCases) ] if !deviceClient.isPad() { effects.append(.init(value: .setOrientationPortrait(true))) @@ -326,7 +333,7 @@ struct ReadingReducer: ReducerProtocol { state.gallery = gallery state.galleryDetail = databaseClient.fetchGalleryDetail(gid: state.gallery.id) return databaseClient.fetchGalleryState(gid: state.gallery.id) - .map(Action.fetchDatabaseInfosDone).cancellable(id: CancelID()) + .map(Action.fetchDatabaseInfosDone).cancellable(id: CancelID.fetchDatabaseInfos) case .fetchDatabaseInfosDone(let galleryState): if let previewConfig = galleryState.previewConfig { @@ -347,7 +354,7 @@ struct ReadingReducer: ReducerProtocol { state.previewLoadingStates[index] = .loading let pageNum = state.previewConfig.pageNumber(index: index) return GalleryPreviewURLsRequest(galleryURL: galleryURL, pageNum: pageNum) - .effect.map({ Action.fetchPreviewURLsDone(index, $0) }).cancellable(id: CancelID()) + .effect.map({ Action.fetchPreviewURLsDone(index, $0) }).cancellable(id: CancelID.fetchPreviewURLs) case .fetchPreviewURLsDone(let index, let result): switch result { @@ -425,7 +432,8 @@ struct ReadingReducer: ReducerProtocol { } let pageNum = state.previewConfig.pageNumber(index: index) return ThumbnailURLsRequest(galleryURL: galleryURL, pageNum: pageNum) - .effect.map({ Action.fetchThumbnailURLsDone(index, $0) }).cancellable(id: CancelID()) + .effect.map({ Action.fetchThumbnailURLsDone(index, $0) }) + .cancellable(id: CancelID.fetchThumbnailURLs) case .fetchThumbnailURLsDone(let index, let result): let batchRange = state.previewConfig.batchRange(index: index) @@ -455,7 +463,8 @@ struct ReadingReducer: ReducerProtocol { case .fetchNormalImageURLs(let index, let thumbnailURLs): return GalleryNormalImageURLsRequest(thumbnailURLs: thumbnailURLs) - .effect.map({ Action.fetchNormalImageURLsDone(index, $0) }).cancellable(id: CancelID()) + .effect.map({ Action.fetchNormalImageURLsDone(index, $0) }) + .cancellable(id: CancelID.fetchNormalImageURLs) case .fetchNormalImageURLsDone(let index, let result): let batchRange = state.previewConfig.batchRange(index: index) @@ -492,7 +501,8 @@ struct ReadingReducer: ReducerProtocol { thumbnailURL: state.thumbnailURLs[index], storedImageURL: imageURL ) - .effect.map({ Action.refetchNormalImageURLsDone(index, $0) }).cancellable(id: CancelID()) + .effect.map({ Action.refetchNormalImageURLsDone(index, $0) }) + .cancellable(id: CancelID.refetchNormalImageURLs) case .refetchNormalImageURLsDone(let index, let result): switch result { @@ -516,7 +526,7 @@ struct ReadingReducer: ReducerProtocol { case .fetchMPVKeys(let index, let mpvURL): return MPVKeysRequest(mpvURL: mpvURL) - .effect.map({ Action.fetchMPVKeysDone(index, $0) }).cancellable(id: CancelID()) + .effect.map({ Action.fetchMPVKeysDone(index, $0) }).cancellable(id: CancelID.fetchMPVKeys) case .fetchMPVKeysDone(let index, let result): let batchRange = state.previewConfig.batchRange(index: index) @@ -557,7 +567,7 @@ struct ReadingReducer: ReducerProtocol { gid: gidInteger, index: index, mpvKey: mpvKey, mpvImageKey: mpvImageKey, skipServerIdentifier: skipServerIdentifier ) - .effect.map({ Action.fetchMPVImageURLDone(index, $0) }).cancellable(id: CancelID()) + .effect.map({ Action.fetchMPVImageURLDone(index, $0) }).cancellable(id: CancelID.fetchMPVImageURL) case .fetchMPVImageURLDone(let index, let result): switch result { diff --git a/EhPanda/View/Search/SearchReducer.swift b/EhPanda/View/Search/SearchReducer.swift index b8e2a1ab..2b35d35e 100644 --- a/EhPanda/View/Search/SearchReducer.swift +++ b/EhPanda/View/Search/SearchReducer.swift @@ -14,8 +14,8 @@ struct SearchReducer: ReducerProtocol { case detail(String) } - struct CancelID: Hashable { - let id = String(describing: SearchReducer.self) + private enum CancelID: CaseIterable { + case fetchGalleries, fetchMoreGalleries } struct State: Equatable { @@ -93,7 +93,7 @@ struct SearchReducer: ReducerProtocol { ) case .teardown: - return .cancel(id: CancelID()) + return .cancel(ids: CancelID.allCases) case .fetchGalleries(let keyword): guard state.loadingState != .loading else { return .none } @@ -106,7 +106,7 @@ struct SearchReducer: ReducerProtocol { let filter = databaseClient.fetchFilterSynchronously(range: .search) return SearchGalleriesRequest(keyword: state.lastKeyword, filter: filter).effect .map(Action.fetchGalleriesDone) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchGalleries) case .fetchGalleriesDone(let result): state.loadingState = .idle @@ -135,7 +135,7 @@ struct SearchReducer: ReducerProtocol { let filter = databaseClient.fetchFilterSynchronously(range: .search) return MoreSearchGalleriesRequest(keyword: state.lastKeyword, filter: filter, lastID: lastID).effect .map(Action.fetchMoreGalleriesDone) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchMoreGalleries) case .fetchMoreGalleriesDone(let result): state.footerLoadingState = .idle diff --git a/EhPanda/View/Search/Support/QuickSearchReducer.swift b/EhPanda/View/Search/Support/QuickSearchReducer.swift index ab3c13b3..2b448e89 100644 --- a/EhPanda/View/Search/Support/QuickSearchReducer.swift +++ b/EhPanda/View/Search/Support/QuickSearchReducer.swift @@ -20,8 +20,8 @@ struct QuickSearchReducer: ReducerProtocol { case content } - struct CancelID: Hashable { - let id = String(describing: QuickSearchReducer.self) + private enum CancelID { + case fetchQuickSearchWords } struct State: Equatable { @@ -114,13 +114,13 @@ struct QuickSearchReducer: ReducerProtocol { return .init(value: .syncQuickSearchWords) case .teardown: - return .cancel(id: CancelID()) + return .cancel(id: CancelID.fetchQuickSearchWords) case .fetchQuickSearchWords: state.loadingState = .loading return databaseClient.fetchQuickSearchWords() .map(Action.fetchQuickSearchWordsDone) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchQuickSearchWords) case .fetchQuickSearchWordsDone(let words): state.loadingState = .idle diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift index fc942e8d..8938624c 100644 --- a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift @@ -85,7 +85,7 @@ struct AccountSettingReducer: ReducerProtocol { return .merge( .init(value: .setNavigation(.hud)), clipboardClient.saveText(cookiesDescription).fireAndForget(), - hapticsClient.generateNotificationFeedback(.success).fireAndForget() + .fireAndForget({ hapticsClient.generateNotificationFeedback(.success) }) ) case .login(.loginDone): diff --git a/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift b/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift index 580ccd6d..51951970 100644 --- a/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift +++ b/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift @@ -14,8 +14,8 @@ struct EhSettingReducer: ReducerProtocol { case deleteProfile } - struct CancelID: Hashable { - let id = String(describing: EhSettingReducer.self) + private enum CancelID: CaseIterable { + case fetchEhSetting, submitChanges, performAction } struct State: Equatable { @@ -74,13 +74,13 @@ struct EhSettingReducer: ReducerProtocol { .fireAndForget() case .teardown: - return .cancel(id: CancelID()) + return .cancel(ids: CancelID.allCases) case .fetchEhSetting: guard state.loadingState != .loading else { return .none } state.loadingState = .loading return EhSettingRequest().effect.map(Action.fetchEhSettingDone) - .cancellable(id: CancelID()) + .cancellable(id: CancelID.fetchEhSetting) case .fetchEhSettingDone(let result): state.loadingState = .idle @@ -100,7 +100,7 @@ struct EhSettingReducer: ReducerProtocol { state.submittingState = .loading return SubmitEhSettingChangesRequest(ehSetting: ehSetting) - .effect.map(Action.submitChangesDone).cancellable(id: CancelID()) + .effect.map(Action.submitChangesDone).cancellable(id: CancelID.submitChanges) case .submitChangesDone(let result): state.submittingState = .idle @@ -117,7 +117,7 @@ struct EhSettingReducer: ReducerProtocol { guard state.submittingState != .loading else { return .none } state.submittingState = .loading return EhProfileRequest(action: action, name: name, set: set) - .effect.map(Action.performActionDone).cancellable(id: CancelID()) + .effect.map(Action.performActionDone).cancellable(id: CancelID.performAction) case .performActionDone(let result): state.submittingState = .idle diff --git a/EhPanda/View/Setting/Login/LoginReducer.swift b/EhPanda/View/Setting/Login/LoginReducer.swift index d9505a4c..735ba22f 100644 --- a/EhPanda/View/Setting/Login/LoginReducer.swift +++ b/EhPanda/View/Setting/Login/LoginReducer.swift @@ -9,7 +9,9 @@ import SwiftUI import ComposableArchitecture struct LoginReducer: ReducerProtocol { - private enum CancelID: Hashable {} + private enum CancelID: Hashable { + case login + } enum Route: Equatable { case webView(URL) @@ -66,9 +68,9 @@ struct LoginReducer: ReducerProtocol { state.focusedField = nil state.loginState = .loading return .merge( - hapticsClient.generateFeedback(.soft).fireAndForget(), + .fireAndForget({ hapticsClient.generateFeedback(.soft) }), LoginRequest(username: state.username, password: state.password) - .effect.map(Action.loginDone).cancellable(id: CancelID.self) + .effect.map(Action.loginDone).cancellable(id: CancelID.login) ) case .loginDone(let result): @@ -76,10 +78,10 @@ struct LoginReducer: ReducerProtocol { var effects = [EffectTask]() if cookieClient.didLogin { state.loginState = .idle - effects.append(hapticsClient.generateNotificationFeedback(.success).fireAndForget()) + effects.append(.fireAndForget({ hapticsClient.generateNotificationFeedback(.success) })) } else { state.loginState = .failed(.unknown) - effects.append(hapticsClient.generateNotificationFeedback(.error).fireAndForget()) + effects.append(.fireAndForget({ hapticsClient.generateNotificationFeedback(.error) })) } if case .success(let response) = result, let response = response { effects.append(cookieClient.setCredentials(response: response).fireAndForget()) diff --git a/EhPanda/View/Setting/Logs/LogsReducer.swift b/EhPanda/View/Setting/Logs/LogsReducer.swift index b3c03eba..5bfa9bd2 100644 --- a/EhPanda/View/Setting/Logs/LogsReducer.swift +++ b/EhPanda/View/Setting/Logs/LogsReducer.swift @@ -12,8 +12,8 @@ struct LogsReducer: ReducerProtocol { case log(Log) } - struct CancelID: Hashable { - let id = String(describing: LogsReducer.self) + private enum CancelID { + case fetchLogs } struct State: Equatable { @@ -51,12 +51,12 @@ struct LogsReducer: ReducerProtocol { return uiApplicationClient.openFileApp().fireAndForget() case .teardown: - return .cancel(id: CancelID()) + return .cancel(id: CancelID.fetchLogs) case .fetchLogs: guard state.loadingState != .loading else { return .none } state.loadingState = .loading - return fileClient.fetchLogs().map(Action.fetchLogsDone).cancellable(id: CancelID()) + return fileClient.fetchLogs().map(Action.fetchLogsDone).cancellable(id: CancelID.fetchLogs) case .fetchLogsDone(let result): switch result { diff --git a/EhPanda/View/Setting/SettingReducer.swift b/EhPanda/View/Setting/SettingReducer.swift index f4e57ae4..472fbbbd 100644 --- a/EhPanda/View/Setting/SettingReducer.swift +++ b/EhPanda/View/Setting/SettingReducer.swift @@ -179,7 +179,7 @@ struct SettingReducer: ReducerProtocol { case .binding(\.$setting.bypassesSNIFiltering): return .merge( .init(value: .syncSetting), - hapticsClient.generateFeedback(.soft).fireAndForget(), + .fireAndForget({ hapticsClient.generateFeedback(.soft) }), dfClient.setActive(state.setting.bypassesSNIFiltering).fireAndForget() ) From bc6c3b19c9f2c1e90b32adf8404d3272604d36e1 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 10:53:23 +0800 Subject: [PATCH 22/29] Fix ex previews --- EhPanda/App/Tools/Clients/CookieClient.swift | 25 +++++++++++++++++--- EhPanda/App/Tools/Defaults.swift | 1 + EhPanda/DataFlow/AppDelegateReducer.swift | 1 + EhPanda/Models/Persistent/Setting.swift | 9 +++++++ EhPanda/View/Setting/SettingReducer.swift | 1 + 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/EhPanda/App/Tools/Clients/CookieClient.swift b/EhPanda/App/Tools/Clients/CookieClient.swift index 4402d95c..c3d70b6c 100644 --- a/EhPanda/App/Tools/Clients/CookieClient.swift +++ b/EhPanda/App/Tools/Clients/CookieClient.swift @@ -141,8 +141,25 @@ extension CookieClient { func removeYay() -> EffectTask { .fireAndForget { removeCookie(Defaults.URL.exhentai, Defaults.Cookie.yay) + removeCookie(Defaults.URL.sexhentai, Defaults.Cookie.yay) } } + func syncExCookies() -> EffectTask { + .merge( + [ + Defaults.Cookie.ipbMemberId, + Defaults.Cookie.ipbPassHash, + Defaults.Cookie.igneous + ] + .map { + setOrEditCookie( + for: Defaults.URL.sexhentai, + key: $0, + value: getCookie(Defaults.URL.exhentai, $0).rawValue + ) + } + ) + } func ignoreOffensive() -> EffectTask { .merge( setOrEditCookie(for: Defaults.URL.ehentai, key: Defaults.Cookie.ignoreOffensive, value: "1"), @@ -202,9 +219,11 @@ extension CookieClient { // MARK: SetCookies extension CookieClient { func setCookies(state: CookiesState) -> EffectTask { - let effects: [EffectTask] = state.allCases.map { subState in - setOrEditCookie(for: state.host.url, key: subState.key, value: subState.editingText) - } + let effects: [EffectTask] = state.allCases + .flatMap { subState in + state.host.cookieURLs + .map({ setOrEditCookie(for: $0, key: subState.key, value: subState.editingText) }) + } return effects.isEmpty ? .none : .merge(effects) } func setCredentials(response: HTTPURLResponse) -> EffectTask { diff --git a/EhPanda/App/Tools/Defaults.swift b/EhPanda/App/Tools/Defaults.swift index 8d5070d5..889d25e8 100644 --- a/EhPanda/App/Tools/Defaults.swift +++ b/EhPanda/App/Tools/Defaults.swift @@ -71,6 +71,7 @@ struct Defaults { static var host: Foundation.URL { AppUtil.galleryHost == .exhentai ? exhentai : ehentai } static let ehentai: Foundation.URL = .init(string: "https://e-hentai.org/").forceUnwrapped static let exhentai: Foundation.URL = .init(string: "https://exhentai.org/").forceUnwrapped + static let sexhentai: Foundation.URL = .init(string: "https://s.exhentai.org/").forceUnwrapped static let torrentDownload: Foundation.URL = .init(string: "https://ehgt.org/g/t.png").forceUnwrapped static let torrentDownloadInvalid: Foundation.URL = .init(string: "https://ehgt.org/g/td.png").forceUnwrapped diff --git a/EhPanda/DataFlow/AppDelegateReducer.swift b/EhPanda/DataFlow/AppDelegateReducer.swift index ea44b11f..6c3f58cb 100644 --- a/EhPanda/DataFlow/AppDelegateReducer.swift +++ b/EhPanda/DataFlow/AppDelegateReducer.swift @@ -33,6 +33,7 @@ struct AppDelegateReducer: ReducerProtocol { libraryClient.initializeLogger().fireAndForget(), libraryClient.initializeWebImage().fireAndForget(), cookieClient.removeYay().fireAndForget(), + cookieClient.syncExCookies().fireAndForget(), cookieClient.ignoreOffensive().fireAndForget(), cookieClient.fulfillAnotherHostField().fireAndForget(), .init(value: .migration(.prepareDatabase)) diff --git a/EhPanda/Models/Persistent/Setting.swift b/EhPanda/Models/Persistent/Setting.swift index 2b1d5ef5..fd3c17e9 100644 --- a/EhPanda/Models/Persistent/Setting.swift +++ b/EhPanda/Models/Persistent/Setting.swift @@ -68,6 +68,15 @@ enum GalleryHost: String, Codable, Equatable, CaseIterable, Identifiable { return Defaults.URL.exhentai } } + var cookieURLs: [URL] { + switch self { + case .ehentai: + return [Defaults.URL.ehentai] + + case .exhentai: + return [Defaults.URL.exhentai, Defaults.URL.sexhentai] + } + } var abbr: String { switch self { case .ehentai: diff --git a/EhPanda/View/Setting/SettingReducer.swift b/EhPanda/View/Setting/SettingReducer.swift index 472fbbbd..8fc87f07 100644 --- a/EhPanda/View/Setting/SettingReducer.swift +++ b/EhPanda/View/Setting/SettingReducer.swift @@ -417,6 +417,7 @@ struct SettingReducer: ReducerProtocol { case .account(.login(.loginDone)): return .merge( cookieClient.removeYay().fireAndForget(), + cookieClient.syncExCookies().fireAndForget(), cookieClient.fulfillAnotherHostField().fireAndForget(), .init(value: .fetchIgneous), .init(value: .fetchUserInfo), From c5a2dca807809608a38cc7b0a28d8c5ebae7466a Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 11:41:30 +0800 Subject: [PATCH 23/29] Update EhSetting logics --- EhPanda/App/Tools/Parser.swift | 13 +-- EhPanda/App/de.lproj/Localizable.strings | 5 +- EhPanda/App/en.lproj/Localizable.strings | 5 +- EhPanda/App/ja.lproj/Localizable.strings | 5 +- EhPanda/App/ko.lproj/Localizable.strings | 5 +- EhPanda/App/zh-Hans.lproj/Localizable.strings | 5 +- .../App/zh-Hant-HK.lproj/Localizable.strings | 5 +- .../App/zh-Hant-TW.lproj/Localizable.strings | 5 +- EhPanda/App/zh-Hant.lproj/Localizable.strings | 5 +- EhPanda/Models/Support/EhSetting.swift | 6 +- EhPanda/Network/Request.swift | 8 +- .../Setting/EhSetting/EhSettingView.swift | 106 ++++-------------- 12 files changed, 47 insertions(+), 126 deletions(-) diff --git a/EhPanda/App/Tools/Parser.swift b/EhPanda/App/Tools/Parser.swift index fe1081cf..3250934d 100644 --- a/EhPanda/App/Tools/Parser.swift +++ b/EhPanda/App/Tools/Parser.swift @@ -1109,7 +1109,7 @@ extension Parser { let form = tmpForm else { throw AppError.parseFailed } // swiftlint:disable line_length - var ehProfiles = [EhProfile](); var isCapableOfCreatingNewProfile: Bool?; var capableLoadThroughHathSetting: EhSetting.LoadThroughHathSetting?; var capableImageResolution: EhSetting.ImageResolution?; var capableSearchResultCount: EhSetting.SearchResultCount?; var capableThumbnailConfigSize: EhSetting.ThumbnailSize?; var capableThumbnailConfigRowCount: EhSetting.ThumbnailRowCount?; var loadThroughHathSetting: EhSetting.LoadThroughHathSetting?; var browsingCountry: EhSetting.BrowsingCountry?; var imageResolution: EhSetting.ImageResolution?; var imageSizeWidth: Float?; var imageSizeHeight: Float?; var galleryName: EhSetting.GalleryName?; var literalBrowsingCountry: String?; var archiverBehavior: EhSetting.ArchiverBehavior?; var displayMode: EhSetting.DisplayMode?; var disabledCategories = [Bool](); var favoriteCategories = [String](); var favoritesSortOrder: EhSetting.FavoritesSortOrder?; var ratingsColor: String?; var excludedNamespaces = [Bool](); var tagFilteringThreshold: Float?; var tagWatchingThreshold: Float?; var showFilteredRemovalCount: Bool?; var excludedLanguages = [Bool](); var excludedUploaders: String?; var searchResultCount: EhSetting.SearchResultCount?; var thumbnailLoadTiming: EhSetting.ThumbnailLoadTiming?; var thumbnailConfigSize: EhSetting.ThumbnailSize?; var thumbnailConfigRows: EhSetting.ThumbnailRowCount?; var thumbnailScaleFactor: Float?; var viewportVirtualWidth: Float?; var commentsSortOrder: EhSetting.CommentsSortOrder?; var commentVotesShowTiming: EhSetting.CommentVotesShowTiming?; var tagsSortOrder: EhSetting.TagsSortOrder?; var galleryShowPageNumbers: Bool?; var useOriginalImages: Bool?; var useMultiplePageViewer: Bool?; var multiplePageViewerStyle: EhSetting.MultiplePageViewerStyle?; var multiplePageViewerShowThumbnailPane: Bool? + var ehProfiles = [EhProfile](); var isCapableOfCreatingNewProfile: Bool?; var capableLoadThroughHathSetting: EhSetting.LoadThroughHathSetting?; var capableImageResolution: EhSetting.ImageResolution?; var capableSearchResultCount: EhSetting.SearchResultCount?; var capableThumbnailConfigSize: EhSetting.ThumbnailSize?; var capableThumbnailConfigRowCount: EhSetting.ThumbnailRowCount?; var loadThroughHathSetting: EhSetting.LoadThroughHathSetting?; var browsingCountry: EhSetting.BrowsingCountry?; var imageResolution: EhSetting.ImageResolution?; var imageSizeWidth: Float?; var imageSizeHeight: Float?; var galleryName: EhSetting.GalleryName?; var literalBrowsingCountry: String?; var archiverBehavior: EhSetting.ArchiverBehavior?; var displayMode: EhSetting.DisplayMode?; var showSearchRangeIndicator: Bool?; var disabledCategories = [Bool](); var favoriteCategories = [String](); var favoritesSortOrder: EhSetting.FavoritesSortOrder?; var ratingsColor: String?; var tagFilteringThreshold: Float?; var tagWatchingThreshold: Float?; var showFilteredRemovalCount: Bool?; var excludedLanguages = [Bool](); var excludedUploaders: String?; var searchResultCount: EhSetting.SearchResultCount?; var thumbnailLoadTiming: EhSetting.ThumbnailLoadTiming?; var thumbnailConfigSize: EhSetting.ThumbnailSize?; var thumbnailConfigRows: EhSetting.ThumbnailRowCount?; var thumbnailScaleFactor: Float?; var viewportVirtualWidth: Float?; var commentsSortOrder: EhSetting.CommentsSortOrder?; var commentVotesShowTiming: EhSetting.CommentVotesShowTiming?; var tagsSortOrder: EhSetting.TagsSortOrder?; var galleryShowPageNumbers: Bool?; var useOriginalImages: Bool?; var useMultiplePageViewer: Bool?; var multiplePageViewerStyle: EhSetting.MultiplePageViewerStyle?; var multiplePageViewerShowThumbnailPane: Bool? // swiftlint:enable line_length ehProfiles = parseSelections(node: profileOuter, name: "profile_set") @@ -1166,6 +1166,9 @@ extension Parser { if optouter.at_xpath("//input [@name='dm']") != nil { displayMode = parseEnum(node: optouter, name: "dm") } + if optouter.at_xpath("//input [@name='pp']") != nil { + showSearchRangeIndicator = parseInt(node: optouter, name: "pp") == 0 + } if optouter.at_xpath("//div [@id='catsel']") != nil { disabledCategories = Array(0...9) .map { "ct_\(EhSetting.categoryNames[$0])" } @@ -1181,10 +1184,6 @@ extension Parser { if optouter.at_xpath("//input [@name='ru']") != nil { ratingsColor = parseString(node: optouter, name: "ru") ?? "" } - if optouter.at_xpath("//div [@id='nssel']") != nil { - excludedNamespaces = Array(1...11).map { "xn_\($0)" } - .compactMap { parseCheckBoxBool(node: optouter, name: $0) } - } if optouter.at_xpath("//input [@name='ft']") != nil { tagFilteringThreshold = Float(parseString(node: optouter, name: "ft") ?? "0") if tagFilteringThreshold == nil { tagFilteringThreshold = 0 } @@ -1254,10 +1253,10 @@ extension Parser { } // swiftlint:disable line_length - guard !ehProfiles.filter(\.isSelected).isEmpty, let isCapableOfCreatingNewProfile, let capableLoadThroughHathSetting, let capableImageResolution, let capableSearchResultCount, let capableThumbnailConfigSize, let capableThumbnailConfigRowCount, let loadThroughHathSetting, let browsingCountry, let literalBrowsingCountry, let imageResolution, let imageSizeWidth, let imageSizeHeight, let galleryName, let archiverBehavior, let displayMode, disabledCategories.count == 10, favoriteCategories.count == 10, let favoritesSortOrder, let ratingsColor, excludedNamespaces.count == 11, let tagFilteringThreshold, let tagWatchingThreshold, excludedLanguages.count == 50, let excludedUploaders, let searchResultCount, let thumbnailLoadTiming, let thumbnailConfigSize, let thumbnailConfigRows, let thumbnailScaleFactor, let viewportVirtualWidth, let commentsSortOrder, let commentVotesShowTiming, let tagsSortOrder, let galleryShowPageNumbers + guard !ehProfiles.filter(\.isSelected).isEmpty, let isCapableOfCreatingNewProfile, let capableLoadThroughHathSetting, let capableImageResolution, let capableSearchResultCount, let capableThumbnailConfigSize, let capableThumbnailConfigRowCount, let loadThroughHathSetting, let browsingCountry, let literalBrowsingCountry, let imageResolution, let imageSizeWidth, let imageSizeHeight, let galleryName, let archiverBehavior, let displayMode, let showSearchRangeIndicator, disabledCategories.count == 10, favoriteCategories.count == 10, let favoritesSortOrder, let ratingsColor, let tagFilteringThreshold, let tagWatchingThreshold, let showFilteredRemovalCount, excludedLanguages.count == 50, let excludedUploaders, let searchResultCount, let thumbnailLoadTiming, let thumbnailConfigSize, let thumbnailConfigRows, let thumbnailScaleFactor, let viewportVirtualWidth, let commentsSortOrder, let commentVotesShowTiming, let tagsSortOrder, let galleryShowPageNumbers else { throw AppError.parseFailed } - return EhSetting(ehProfiles: ehProfiles.sorted(), isCapableOfCreatingNewProfile: isCapableOfCreatingNewProfile, capableLoadThroughHathSetting: capableLoadThroughHathSetting, capableImageResolution: capableImageResolution, capableSearchResultCount: capableSearchResultCount, capableThumbnailConfigSize: capableThumbnailConfigSize, capableThumbnailConfigRowCount: capableThumbnailConfigRowCount, loadThroughHathSetting: loadThroughHathSetting, browsingCountry: browsingCountry, literalBrowsingCountry: literalBrowsingCountry, imageResolution: imageResolution, imageSizeWidth: imageSizeWidth, imageSizeHeight: imageSizeHeight, galleryName: galleryName, archiverBehavior: archiverBehavior, displayMode: displayMode, disabledCategories: disabledCategories, favoriteCategories: favoriteCategories, favoritesSortOrder: favoritesSortOrder, ratingsColor: ratingsColor, excludedNamespaces: excludedNamespaces, tagFilteringThreshold: tagFilteringThreshold, tagWatchingThreshold: tagWatchingThreshold, showFilteredRemovalCount: showFilteredRemovalCount, excludedLanguages: excludedLanguages, excludedUploaders: excludedUploaders, searchResultCount: searchResultCount, thumbnailLoadTiming: thumbnailLoadTiming, thumbnailConfigSize: thumbnailConfigSize, thumbnailConfigRows: thumbnailConfigRows, thumbnailScaleFactor: thumbnailScaleFactor, viewportVirtualWidth: viewportVirtualWidth, commentsSortOrder: commentsSortOrder, commentVotesShowTiming: commentVotesShowTiming, tagsSortOrder: tagsSortOrder, galleryShowPageNumbers: galleryShowPageNumbers, useOriginalImages: useOriginalImages, useMultiplePageViewer: useMultiplePageViewer, multiplePageViewerStyle: multiplePageViewerStyle, multiplePageViewerShowThumbnailPane: multiplePageViewerShowThumbnailPane + return EhSetting(ehProfiles: ehProfiles.sorted(), isCapableOfCreatingNewProfile: isCapableOfCreatingNewProfile, capableLoadThroughHathSetting: capableLoadThroughHathSetting, capableImageResolution: capableImageResolution, capableSearchResultCount: capableSearchResultCount, capableThumbnailConfigSize: capableThumbnailConfigSize, capableThumbnailConfigRowCount: capableThumbnailConfigRowCount, loadThroughHathSetting: loadThroughHathSetting, browsingCountry: browsingCountry, literalBrowsingCountry: literalBrowsingCountry, imageResolution: imageResolution, imageSizeWidth: imageSizeWidth, imageSizeHeight: imageSizeHeight, galleryName: galleryName, archiverBehavior: archiverBehavior, displayMode: displayMode, showSearchRangeIndicator: showSearchRangeIndicator, disabledCategories: disabledCategories, favoriteCategories: favoriteCategories, favoritesSortOrder: favoritesSortOrder, ratingsColor: ratingsColor, tagFilteringThreshold: tagFilteringThreshold, tagWatchingThreshold: tagWatchingThreshold, showFilteredRemovalCount: showFilteredRemovalCount, excludedLanguages: excludedLanguages, excludedUploaders: excludedUploaders, searchResultCount: searchResultCount, thumbnailLoadTiming: thumbnailLoadTiming, thumbnailConfigSize: thumbnailConfigSize, thumbnailConfigRows: thumbnailConfigRows, thumbnailScaleFactor: thumbnailScaleFactor, viewportVirtualWidth: viewportVirtualWidth, commentsSortOrder: commentsSortOrder, commentVotesShowTiming: commentVotesShowTiming, tagsSortOrder: tagsSortOrder, galleryShowPageNumbers: galleryShowPageNumbers, useOriginalImages: useOriginalImages, useMultiplePageViewer: useMultiplePageViewer, multiplePageViewerStyle: multiplePageViewerStyle, multiplePageViewerShowThumbnailPane: multiplePageViewerShowThumbnailPane ) // swiftlint:enable line_length } diff --git a/EhPanda/App/de.lproj/Localizable.strings b/EhPanda/App/de.lproj/Localizable.strings index 360ea078..f370f044 100644 --- a/EhPanda/App/de.lproj/Localizable.strings +++ b/EhPanda/App/de.lproj/Localizable.strings @@ -442,6 +442,8 @@ "eh_setting_view.section.title.front_page_settings" = "Front Page Settings"; "eh_setting_view.title.display_mode" = "Display mode"; "eh_setting_view.description.display_mode" = "Which display mode would you like to use on the front and search pages?"; +"eh_setting_view.section.title.show_search_range_indicator" = "Search Range Indicator"; +"eh_setting_view.title.show_search_range_indicator" = "Show search range indicator"; "eh_setting_view.description.gallery_category" = "What categories would you like to show by default on the front page and in searches?"; // EhSetting.DisplayMode "enum.eh_setting.display_mode.value.compact" = "Compact"; @@ -463,9 +465,6 @@ "eh_setting_view.promt.ratings_color" = "RRGGB"; "eh_setting_view.description.ratings_color" = "By default, galleries that you have rated will appear with red stars for ratings of 2 stars and below, green for ratings between 2.5 and 4 stars, and blue for ratings of 4.5 or 5 stars. You can customize this by entering your desired color combination below. Each letter represents one star. The default RRGGB means R(ed) for the first and second star, G(reen) for the third and fourth, and B(lue) for the fifth. You can also use (Y)ellow for the normal stars. Any five-letter R/G/B/Y combo works."; -"eh_setting_view.section.title.tags_namespaces" = "Tag Namespaces"; -"eh_setting_view.description.tags_namespaces" = "If you want to exclude certain namespaces from a default tag search, you can check those below. Note that this does not prevent galleries with tags in these namespaces from appearing, it just makes it so that when searching tags, it will forego those namespaces."; - "eh_setting_view.section.title.tag_filtering_threshold" = "Tag Filtering Threshold"; "eh_setting_view.title.tag_filtering_threshold" = "Tag Filtering Threshold"; "eh_setting_view.description.tag_filtering_threshold" = "You can soft filter tags by adding them to My Tags with a negative weight. If a gallery has tags that add up to weight below this value, it is filtered from view. This threshold can be set between 0 and -9999."; diff --git a/EhPanda/App/en.lproj/Localizable.strings b/EhPanda/App/en.lproj/Localizable.strings index 0cf29b6d..9a742b9a 100644 --- a/EhPanda/App/en.lproj/Localizable.strings +++ b/EhPanda/App/en.lproj/Localizable.strings @@ -442,6 +442,8 @@ "eh_setting_view.section.title.front_page_settings" = "Front Page Settings"; "eh_setting_view.title.display_mode" = "Display mode"; "eh_setting_view.description.display_mode" = "Which display mode would you like to use on the front and search pages?"; +"eh_setting_view.section.title.show_search_range_indicator" = "Search Range Indicator"; +"eh_setting_view.title.show_search_range_indicator" = "Show search range indicator"; "eh_setting_view.description.gallery_category" = "What categories would you like to show by default on the front page and in searches?"; // EhSetting.DisplayMode "enum.eh_setting.display_mode.value.compact" = "Compact"; @@ -463,9 +465,6 @@ "eh_setting_view.promt.ratings_color" = "RRGGB"; "eh_setting_view.description.ratings_color" = "By default, galleries that you have rated will appear with red stars for ratings of 2 stars and below, green for ratings between 2.5 and 4 stars, and blue for ratings of 4.5 or 5 stars. You can customize this by entering your desired color combination below. Each letter represents one star. The default RRGGB means R(ed) for the first and second star, G(reen) for the third and fourth, and B(lue) for the fifth. You can also use (Y)ellow for the normal stars. Any five-letter R/G/B/Y combo works."; -"eh_setting_view.section.title.tags_namespaces" = "Tag Namespaces"; -"eh_setting_view.description.tags_namespaces" = "If you want to exclude certain namespaces from a default tag search, you can check those below. Note that this does not prevent galleries with tags in these namespaces from appearing, it just makes it so that when searching tags, it will forego those namespaces."; - "eh_setting_view.section.title.tag_filtering_threshold" = "Tag Filtering Threshold"; "eh_setting_view.title.tag_filtering_threshold" = "Tag Filtering Threshold"; "eh_setting_view.description.tag_filtering_threshold" = "You can soft filter tags by adding them to My Tags with a negative weight. If a gallery has tags that add up to weight below this value, it is filtered from view. This threshold can be set between 0 and -9999."; diff --git a/EhPanda/App/ja.lproj/Localizable.strings b/EhPanda/App/ja.lproj/Localizable.strings index b39bf2d2..e1e1e5f2 100644 --- a/EhPanda/App/ja.lproj/Localizable.strings +++ b/EhPanda/App/ja.lproj/Localizable.strings @@ -442,6 +442,8 @@ "eh_setting_view.section.title.front_page_settings" = "フロントページ設定"; "eh_setting_view.title.display_mode" = "表示モード"; "eh_setting_view.description.display_mode" = "フロント・検索ページで使う表示モードはどれにしますか?"; +"eh_setting_view.section.title.show_search_range_indicator" = "検索範囲インジケーター"; +"eh_setting_view.title.show_search_range_indicator" = "検索範囲インジケーターを表示"; "eh_setting_view.description.gallery_category" = "フロント・検索ページでどれらのカテゴリーのギャラリーを表示しますか?"; // EhSetting.DisplayMode "enum.eh_setting.display_mode.value.compact" = "コンパクト"; @@ -463,9 +465,6 @@ "eh_setting_view.promt.ratings_color" = "RRGGB"; "eh_setting_view.description.ratings_color" = "デフォルトでは、評価済みのギャラリーは 2 以下の評価に赤い星を使う、2.5 ~ 4 には緑、4.5 以上には青。下に色の組み合わせを入れることでこのルールをカスタマイズできます。一つの星の色は一つの文字で指定します。デフォルトの「RRGGB」は「一番目と二番目の星は赤(Red)、三番と四番は緑(Green)、五番は青(Blue)」を意味します。黄色(Yellow)も使用可能です。R・G・B・Yで組み合わせた五文字はどれも機能します。"; -"eh_setting_view.section.title.tags_namespaces" = "タグの名前空間"; -"eh_setting_view.description.tags_namespaces" = "下にある名前空間を削除仕様にすることで通常のタグ検索結果から排除できます。注意:排除された名前空間のタグを持つギャラリーが表示されなくなることはありません。"; - "eh_setting_view.section.title.tag_filtering_threshold" = "タグフィルタリングしきい値"; "eh_setting_view.title.tag_filtering_threshold" = "タグフィルタリングしきい値"; "eh_setting_view.description.tag_filtering_threshold" = "負の重み付きでマイタグに追加することでタグをソフトフィルタリングすることができます。もしあるギャラリーが持つタグの重み総和がこのしきい値より低ければ、そのギャラリーはフィルタリングされます。このしきい値はゼロから -9999 まで設定できます。"; diff --git a/EhPanda/App/ko.lproj/Localizable.strings b/EhPanda/App/ko.lproj/Localizable.strings index ec09077d..a83fdc00 100644 --- a/EhPanda/App/ko.lproj/Localizable.strings +++ b/EhPanda/App/ko.lproj/Localizable.strings @@ -442,6 +442,8 @@ "eh_setting_view.section.title.front_page_settings" = "프론트 페이지 설정"; "eh_setting_view.title.display_mode" = "표시방식"; "eh_setting_view.description.display_mode" = "프론트와 검색 페이지에서 사용할 디스플레이 모드를 선택하세요."; +"eh_setting_view.section.title.show_search_range_indicator" = "Search Range Indicator"; +"eh_setting_view.title.show_search_range_indicator" = "Show search range indicator"; "eh_setting_view.description.gallery_category" = "프론트와 검색 페이지에서 어떤 카테고리가 보여지도록 할까요?"; // EhSetting.DisplayMode "enum.eh_setting.display_mode.value.compact" = "Compact"; @@ -463,9 +465,6 @@ "eh_setting_view.promt.ratings_color" = "RRGGB"; "eh_setting_view.description.ratings_color" = "기본적으로 등급을 매긴 갤러리는 별 2개 이하의 등급에 대해 빨간색, 2.5~4개의 등급에 대해 녹색, 4.5~5개의 등급에 대해 파란색 별로 표시되어요. 아래에 원하는 색상 조합을 입력하여 사용자 정의할 수 있어요. 각 문자는 별 하나를 표현해요. 기본 RRGGB는 첫 번째와 두 번째 별의 경우 R(ed), 세 번째와 네 번째 별의 경우 G(reen), 다섯 번째 별의 경우 B(lue)를 의미해요. 일반 별에 (Y)ellow를 사용할 수도 있어요. 모든 5글자의 R/G/B/Y 콤보가 작동해요."; -"eh_setting_view.section.title.tags_namespaces" = "태그 네임스페이스"; -"eh_setting_view.description.tags_namespaces" = "기본 태그 검색에서 특정 네임스페이스를 제외하려면 아래 네임스페이스들을 확인해주세요. 이렇게 해도 이러한 네임스페이스에 태그가 있는 갤러리가 나타나지 않고 태그를 검색할 때 해당 네임스페이스가 표시되지 않도록 할 수 있어요."; - "eh_setting_view.section.title.tag_filtering_threshold" = "태그 필터링 임계값"; "eh_setting_view.title.tag_filtering_threshold" = "태그 필터링 임계값"; "eh_setting_view.description.tag_filtering_threshold" = "마이너스 가중치로 My Tags에 추가하여 태그를 소프트 필터할 수 있어요. 갤러리에 이 값 이하의 가중치를 추가하는 태그가 있으면 보기에서 필터링되어요. 이 임계값은 0과 -9999 사이에서 설정할 수 있어요."; diff --git a/EhPanda/App/zh-Hans.lproj/Localizable.strings b/EhPanda/App/zh-Hans.lproj/Localizable.strings index 57f0bb98..0e11b221 100644 --- a/EhPanda/App/zh-Hans.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hans.lproj/Localizable.strings @@ -442,6 +442,8 @@ "eh_setting_view.section.title.front_page_settings" = "扉页设置"; "eh_setting_view.title.display_mode" = "显示样式"; "eh_setting_view.description.display_mode" = "你希望在扉页和搜索页显示哪种样式?"; +"eh_setting_view.section.title.show_search_range_indicator" = "搜索范围指示器"; +"eh_setting_view.title.show_search_range_indicator" = "显示搜索范围指示器"; "eh_setting_view.description.gallery_category" = "你希望在扉页和搜索页看到哪些类别?"; // EhSetting.DisplayMode "enum.eh_setting.display_mode.value.compact" = "紧凑"; @@ -463,9 +465,6 @@ "eh_setting_view.promt.ratings_color" = "RRGGB"; "eh_setting_view.description.ratings_color" = "默认设置下,你评为 2 星及以下的画廊显示为红星,2.5 ~ 4 星显示为绿星,4.5 ~ 5 星显示为蓝星。你可以将其设定为其它颜色组合。每一个字幕代表一颗星, 默认的 RRGGB 表示第一第二颗星显示为红色 R(ed),第三第四颗星显示是绿色 G(reen),第五颗星显示为蓝色 B(lue)。你也可以使用黄色 (Y)ellow,R/G/B/Y 任何五个组合都是有效的。"; -"eh_setting_view.section.title.tags_namespaces" = "标签命名空间"; -"eh_setting_view.description.tags_namespaces" = "如果要从默认标签搜索中排除某些命名空间,可以将以下内容标记为删除样式。注意:这不会阻止带有这些命名空间中标签的画廊出现,它只是在搜索标签时排除这些命名空间。"; - "eh_setting_view.section.title.tag_filtering_threshold" = "标签筛选阈值"; "eh_setting_view.title.tag_filtering_threshold" = "标签筛选阈值"; "eh_setting_view.description.tag_filtering_threshold" = "你可以通过将标签加入“我的标签”并设置一个负权重来软过滤它们。如果一个作品所有的标签权重之和低于设定值,此作品将从视图中被过滤。这个值可以设定为 0 ~ -9999。"; diff --git a/EhPanda/App/zh-Hant-HK.lproj/Localizable.strings b/EhPanda/App/zh-Hant-HK.lproj/Localizable.strings index 911614f2..1584449a 100644 --- a/EhPanda/App/zh-Hant-HK.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hant-HK.lproj/Localizable.strings @@ -442,6 +442,8 @@ "eh_setting_view.section.title.front_page_settings" = "Front Page Settings"; "eh_setting_view.title.display_mode" = "Display mode"; "eh_setting_view.description.display_mode" = "Which display mode would you like to use on the front and search pages?"; +"eh_setting_view.section.title.show_search_range_indicator" = "Search Range Indicator"; +"eh_setting_view.title.show_search_range_indicator" = "Show search range indicator"; "eh_setting_view.description.gallery_category" = "What categories would you like to show by default on the front page and in searches?"; // EhSetting.DisplayMode "enum.eh_setting.display_mode.value.compact" = "Compact"; @@ -463,9 +465,6 @@ "eh_setting_view.promt.ratings_color" = "RRGGB"; "eh_setting_view.description.ratings_color" = "By default, galleries that you have rated will appear with red stars for ratings of 2 stars and below, green for ratings between 2.5 and 4 stars, and blue for ratings of 4.5 or 5 stars. You can customize this by entering your desired color combination below. Each letter represents one star. The default RRGGB means R(ed) for the first and second star, G(reen) for the third and fourth, and B(lue) for the fifth. You can also use (Y)ellow for the normal stars. Any five-letter R/G/B/Y combo works."; -"eh_setting_view.section.title.tags_namespaces" = "Tag Namespaces"; -"eh_setting_view.description.tags_namespaces" = "If you want to exclude certain namespaces from a default tag search, you can check those below. Note that this does not prevent galleries with tags in these namespaces from appearing, it just makes it so that when searching tags, it will forego those namespaces."; - "eh_setting_view.section.title.tag_filtering_threshold" = "Tag Filtering Threshold"; "eh_setting_view.title.tag_filtering_threshold" = "Tag Filtering Threshold"; "eh_setting_view.description.tag_filtering_threshold" = "You can soft filter tags by adding them to My Tags with a negative weight. If a gallery has tags that add up to weight below this value, it is filtered from view. This threshold can be set between 0 and -9999."; diff --git a/EhPanda/App/zh-Hant-TW.lproj/Localizable.strings b/EhPanda/App/zh-Hant-TW.lproj/Localizable.strings index 43113e41..718f883a 100644 --- a/EhPanda/App/zh-Hant-TW.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hant-TW.lproj/Localizable.strings @@ -442,6 +442,8 @@ "eh_setting_view.section.title.front_page_settings" = "首頁設定"; "eh_setting_view.title.display_mode" = "顯示模式"; "eh_setting_view.description.display_mode" = "你想在首頁和搜尋結果中使用哪一種顯示方式?"; +"eh_setting_view.section.title.show_search_range_indicator" = "搜尋範圍指示器"; +"eh_setting_view.title.show_search_range_indicator" = "顯示搜尋範圍指示器"; "eh_setting_view.description.gallery_category" = "預設情況下你希望在首頁和搜尋結果中顯示哪些類別的結果?"; // EhSetting.DisplayMode "enum.eh_setting.display_mode.value.compact" = "緊湊(Compact)"; @@ -463,9 +465,6 @@ "eh_setting_view.promt.ratings_color" = "RRGGB"; "eh_setting_view.description.ratings_color" = "預設情況下,你評分的畫廊將 2 星及以下的評分顯示為紅色星,2.5 ~ 4 顆星的評分為綠色,4.5 ~ 5 顆星的評分為藍色。 通過在下面輸入顏色組合你可以自訂想顯示的顏色。 每個字母各代表一顆星(1~5),預設的 RRGGB 表示第一顆和第二顆星的 R(ed),第三顆和第四顆的 G(reen),第五顆的 B(lue)。 你也可以將 (Y)ellow 用於普通星星。 任何五個字母的 R/G/B/Y 組合都有效"; -"eh_setting_view.section.title.tags_namespaces" = "標籤類型"; -"eh_setting_view.description.tags_namespaces" = "如果你想從標籤搜尋時排除從下列某些類型標籤出現在搜尋結果中,可以從下面更改設定。 注意!!這不會使搜尋結果中不出現那些畫廊,僅是限定搜尋標籤所在的類型範圍!"; - "eh_setting_view.section.title.tag_filtering_threshold" = "過濾標籤閾值"; "eh_setting_view.title.tag_filtering_threshold" = "閾值"; "eh_setting_view.description.tag_filtering_threshold" = "你可以通過將標籤新增到具有負數權重的“我的標籤”清單中來過濾標籤。 如果畫廊的標籤加起來的權重低於此值,則會被從列表中過濾掉,此閾值可以設定在 0 ~ -9999 之間"; diff --git a/EhPanda/App/zh-Hant.lproj/Localizable.strings b/EhPanda/App/zh-Hant.lproj/Localizable.strings index 4d093dd6..ee429694 100644 --- a/EhPanda/App/zh-Hant.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hant.lproj/Localizable.strings @@ -442,6 +442,8 @@ "eh_setting_view.section.title.front_page_settings" = "首頁設定"; "eh_setting_view.title.display_mode" = "顯示模式"; "eh_setting_view.description.display_mode" = "你想在首頁和搜尋結果中使用哪一種顯示方式?"; +"eh_setting_view.section.title.show_search_range_indicator" = "搜尋範圍指示器"; +"eh_setting_view.title.show_search_range_indicator" = "顯示搜尋範圍指示器"; "eh_setting_view.description.gallery_category" = "預設情況下你希望在首頁和搜尋結果中顯示哪些類別的結果?"; // EhSetting.DisplayMode "enum.eh_setting.display_mode.value.compact" = "緊湊(Compact)"; @@ -463,9 +465,6 @@ "eh_setting_view.promt.ratings_color" = "RRGGB"; "eh_setting_view.description.ratings_color" = "預設情況下,你評分的畫廊將 2 星及以下的評分顯示為紅色星,2.5 ~ 4 顆星的評分為綠色,4.5 ~ 5 顆星的評分為藍色。 通過在下面輸入顏色組合你可以自訂想顯示的顏色。 每個字母各代表一顆星(1~5),預設的 RRGGB 表示第一顆和第二顆星的 R(ed),第三顆和第四顆的 G(reen),第五顆的 B(lue)。 你也可以將 (Y)ellow 用於普通星星。 任何五個字母的 R/G/B/Y 組合都有效"; -"eh_setting_view.section.title.tags_namespaces" = "標籤類型"; -"eh_setting_view.description.tags_namespaces" = "如果你想從標籤搜尋時排除從下列某些類型標籤出現在搜尋結果中,可以從下面更改設定。 注意!!這不會使搜尋結果中不出現那些畫廊,僅是限定搜尋標籤所在的類型範圍!"; - "eh_setting_view.section.title.tag_filtering_threshold" = "過濾標籤閾值"; "eh_setting_view.title.tag_filtering_threshold" = "閾值"; "eh_setting_view.description.tag_filtering_threshold" = "你可以通過將標籤新增到具有負數權重的“我的標籤”清單中來過濾標籤。 如果畫廊的標籤加起來的權重低於此值,則會被從列表中過濾掉,此閾值可以設定在 0 ~ -9999 之間"; diff --git a/EhPanda/Models/Support/EhSetting.swift b/EhPanda/Models/Support/EhSetting.swift index 855ebd2c..75c27730 100644 --- a/EhPanda/Models/Support/EhSetting.swift +++ b/EhPanda/Models/Support/EhSetting.swift @@ -8,7 +8,7 @@ // MARK: EhSetting struct EhSetting: Equatable { // swiftlint:disable line_length - static let empty: Self = .init(ehProfiles: [.empty], isCapableOfCreatingNewProfile: true, capableLoadThroughHathSetting: .anyClient, capableImageResolution: .auto, capableSearchResultCount: .fifty, capableThumbnailConfigSize: .normal, capableThumbnailConfigRowCount: .forty, loadThroughHathSetting: .anyClient, browsingCountry: .autoDetect, literalBrowsingCountry: "", imageResolution: .auto, imageSizeWidth: 0, imageSizeHeight: 0, galleryName: .default, archiverBehavior: .autoSelectOriginalAutoStart, displayMode: .compact, disabledCategories: Array(repeating: false, count: 10), favoriteCategories: Array(repeating: "", count: 10), favoritesSortOrder: .favoritedTime, ratingsColor: "", excludedNamespaces: Array(repeating: false, count: 11), tagFilteringThreshold: 0, tagWatchingThreshold: 0, excludedLanguages: Array(repeating: false, count: 50), excludedUploaders: "", searchResultCount: .fifty, thumbnailLoadTiming: .onPageLoad, thumbnailConfigSize: .normal, thumbnailConfigRows: .ten, thumbnailScaleFactor: 0, viewportVirtualWidth: 0, commentsSortOrder: .recent, commentVotesShowTiming: .always, tagsSortOrder: .alphabetical, galleryShowPageNumbers: true) + static let empty: Self = .init(ehProfiles: [.empty], isCapableOfCreatingNewProfile: true, capableLoadThroughHathSetting: .anyClient, capableImageResolution: .auto, capableSearchResultCount: .fifty, capableThumbnailConfigSize: .normal, capableThumbnailConfigRowCount: .forty, loadThroughHathSetting: .anyClient, browsingCountry: .autoDetect, literalBrowsingCountry: "", imageResolution: .auto, imageSizeWidth: 0, imageSizeHeight: 0, galleryName: .default, archiverBehavior: .autoSelectOriginalAutoStart, displayMode: .compact, showSearchRangeIndicator: true, disabledCategories: Array(repeating: false, count: 10), favoriteCategories: Array(repeating: "", count: 10), favoritesSortOrder: .favoritedTime, ratingsColor: "", tagFilteringThreshold: 0, tagWatchingThreshold: 0, showFilteredRemovalCount: true, excludedLanguages: Array(repeating: false, count: 50), excludedUploaders: "", searchResultCount: .fifty, thumbnailLoadTiming: .onPageLoad, thumbnailConfigSize: .normal, thumbnailConfigRows: .ten, thumbnailScaleFactor: 0, viewportVirtualWidth: 0, commentsSortOrder: .recent, commentVotesShowTiming: .always, tagsSortOrder: .alphabetical, galleryShowPageNumbers: true) // swiftlint:enable line_length static let categoryNames = Category.allFiltersCases.map(\.rawValue).map { value in @@ -76,14 +76,14 @@ struct EhSetting: Equatable { var galleryName: GalleryName var archiverBehavior: ArchiverBehavior var displayMode: DisplayMode + var showSearchRangeIndicator: Bool var disabledCategories: [Bool] var favoriteCategories: [String] var favoritesSortOrder: FavoritesSortOrder var ratingsColor: String - var excludedNamespaces: [Bool] var tagFilteringThreshold: Float var tagWatchingThreshold: Float - var showFilteredRemovalCount: Bool? + var showFilteredRemovalCount: Bool var excludedLanguages: [Bool] var excludedUploaders: String var searchResultCount: SearchResultCount diff --git a/EhPanda/Network/Request.swift b/EhPanda/Network/Request.swift index 5fb49897..c1275f8e 100644 --- a/EhPanda/Network/Request.swift +++ b/EhPanda/Network/Request.swift @@ -776,10 +776,12 @@ struct SubmitEhSettingChangesRequest: Request { "tl": String(ehSetting.galleryName.rawValue), "ar": String(ehSetting.archiverBehavior.rawValue), "dm": String(ehSetting.displayMode.rawValue), + "pp": ehSetting.showSearchRangeIndicator ? "0" : "1", "fs": String(ehSetting.favoritesSortOrder.rawValue), "ru": ehSetting.ratingsColor, "ft": String(Int(ehSetting.tagFilteringThreshold)), "wt": String(Int(ehSetting.tagWatchingThreshold)), + "tf": ehSetting.showFilteredRemovalCount ? "0" : "1", "xu": ehSetting.excludedUploaders, "rc": String(ehSetting.searchResultCount.rawValue), "lt": String(ehSetting.thumbnailLoadTiming.rawValue), @@ -800,18 +802,12 @@ struct SubmitEhSettingChangesRequest: Request { Array(0...9).forEach { index in params["favorite_\(index)"] = ehSetting.favoriteCategories[index] } - Array(0...10).forEach { index in - params["xn_\(index + 1)"] = ehSetting.excludedNamespaces[index] ? "1" : "0" - } ehSetting.excludedLanguages.enumerated().forEach { index, value in if value { params["xl_\(EhSetting.languageValues[index])"] = "on" } } - if let showFilteredRemovalCount = ehSetting.showFilteredRemovalCount { - params["tf"] = showFilteredRemovalCount ? "0" : "1" - } if let useOriginalImages = ehSetting.useOriginalImages { params["oi"] = useOriginalImages ? "1" : "0" } diff --git a/EhPanda/View/Setting/EhSetting/EhSettingView.swift b/EhPanda/View/Setting/EhSetting/EhSettingView.swift index 79147ff3..102c79b7 100644 --- a/EhPanda/View/Setting/EhSetting/EhSettingView.swift +++ b/EhPanda/View/Setting/EhSetting/EhSettingView.swift @@ -84,11 +84,10 @@ struct EhSettingView: View { FrontPageSettingsSection(ehSetting: ehSetting) FavoritesSection(ehSetting: ehSetting) RatingsSection(ehSetting: ehSetting) - TagNamespacesSection(ehSetting: ehSetting) TagFilteringThresholdSection(ehSetting: ehSetting) + TagWatchingThresholdSection(ehSetting: ehSetting) } Group { - TagWatchingThresholdSection(ehSetting: ehSetting) FilteredRemovalCountSection(ehSetting: ehSetting) ExcludedLanguagesSection(ehSetting: ehSetting) ExcludedUploadersSection(ehSetting: ehSetting) @@ -98,9 +97,9 @@ struct EhSettingView: View { ViewportOverrideSection(ehSetting: ehSetting) GalleryCommentsSection(ehSetting: ehSetting) GalleryTagsSection(ehSetting: ehSetting) + GalleryPageNumberingSection(ehSetting: ehSetting) } Group { - GalleryPageNumberingSection(ehSetting: ehSetting) OriginalImagesSection(ehSetting: ehSetting) MultiplePageViewerSection(ehSetting: ehSetting) } @@ -386,6 +385,16 @@ private struct FrontPageSettingsSection: View { } .textCase(nil) + Section { + Toggle( + L10n.Localizable.EhSettingView.Section.Title.showSearchRangeIndicator, + isOn: $ehSetting.showSearchRangeIndicator + ) + } header: { + Text(L10n.Localizable.EhSettingView.Section.Title.showSearchRangeIndicator) + } + .textCase(nil) + Section(L10n.Localizable.EhSettingView.Description.galleryCategory) { CategoryView(bindings: categoryBindings) } @@ -472,79 +481,6 @@ private struct RatingsSection: View { } } -// MARK: TagNamespacesSection -private struct TagNamespacesSection: View { - @Binding private var ehSetting: EhSetting - - init(ehSetting: Binding) { - _ehSetting = ehSetting - } - - private var tuples: [(String, Binding)] { - TagNamespace.allCases.dropLast().enumerated().map { index, namespace in - (namespace.value, $ehSetting.excludedNamespaces[index]) - } - } - - var body: some View { - Section { - ExcludeView(tuples: tuples) - } header: { - Text(L10n.Localizable.EhSettingView.Section.Title.tagsNamespaces) - .newlineBold() - .appending(L10n.Localizable.EhSettingView.Description.tagsNamespaces) - } - .textCase(nil) - } -} - -private struct ExcludeView: View { - private let tuples: [(String, Binding)] - - init(tuples: [(String, Binding)]) { - self.tuples = tuples - } - - private let gridItems = [ - GridItem( - .adaptive( - minimum: DeviceUtil.isPadWidth ? 100 : 80, - maximum: 100 - ) - ) - ] - - var body: some View { - LazyVGrid(columns: gridItems) { - ForEach(tuples, id: \.0) { text, isExcluded in - ZStack { - Text(text) - .bold() - .opacity(isExcluded.wrappedValue ? 0 : 1) - ZStack { - Text(text) - - let width = (CGFloat(text.count) * 8) + 8 - let line = Rectangle() - .frame(width: width, height: 1) - VStack(spacing: 2) { - line - line - } - } - .foregroundColor(.red) - .opacity(isExcluded.wrappedValue ? 1 : 0) - } - .onTapGesture { - HapticsUtil.generateFeedback(style: .soft) - withAnimation { isExcluded.wrappedValue.toggle() } - } - } - } - .padding(.vertical) - } -} - // MARK: TagFilteringThresholdSection private struct TagFilteringThresholdSection: View { @Binding private var ehSetting: EhSetting @@ -600,16 +536,14 @@ private struct FilteredRemovalCountSection: View { } var body: some View { - if let showFilteredRemovalCountBinding = Binding($ehSetting.showFilteredRemovalCount) { - Section { - Toggle( - L10n.Localizable.EhSettingView.Title.showFilteredRemovalCount, - isOn: showFilteredRemovalCountBinding - ) - } header: { - Text(L10n.Localizable.EhSettingView.Section.Title.filteredRemovalCount).newlineBold() - + Text(L10n.Localizable.EhSettingView.Description.filteredRemovalCount) - } + Section { + Toggle( + L10n.Localizable.EhSettingView.Title.showFilteredRemovalCount, + isOn: $ehSetting.showFilteredRemovalCount + ) + } header: { + Text(L10n.Localizable.EhSettingView.Section.Title.filteredRemovalCount).newlineBold() + + Text(L10n.Localizable.EhSettingView.Description.filteredRemovalCount) } } } From 80d88e80269acd9cb460f8b3176293d8e69f5736 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 11:54:24 +0800 Subject: [PATCH 24/29] Update tests --- EhPanda.xcodeproj/project.pbxproj | 28 - EhPanda/App/Tools/Utilities/AppUtil.swift | 9 + EhPanda/DataFlow/AppDelegateReducer.swift | 4 +- .../Resources/Parser/Other/EhSetting.html | 707 +++++++++--------- .../Parser/Other/EhSettingParserTests.swift | 14 +- .../Setting/AccountSettingStoreTests.swift | 239 ------ .../Setting/AppearanceSettingStoreTests.swift | 42 -- .../Setting/GeneralSettingStoreTests.swift | 111 --- 8 files changed, 377 insertions(+), 777 deletions(-) delete mode 100644 EhPandaTests/Tests/Store/Setting/AccountSettingStoreTests.swift delete mode 100644 EhPandaTests/Tests/Store/Setting/AppearanceSettingStoreTests.swift delete mode 100644 EhPandaTests/Tests/Store/Setting/GeneralSettingStoreTests.swift diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 0707f561..e96869f1 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -113,7 +113,6 @@ AB6505A026B0027800F91E9D /* SwiftUIPager in Frameworks */ = {isa = PBXBuildFile; productRef = AB65059F26B0027800F91E9D /* SwiftUIPager */; }; AB69CB8026B3DABC00699359 /* AdvancedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB69CB7F26B3DABC00699359 /* AdvancedList.swift */; }; AB69CB8226B3DAF400699359 /* ControlPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB69CB8126B3DAF400699359 /* ControlPanel.swift */; }; - AB6D106A27EBF890003A2245 /* GeneralSettingStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6D106927EBF890003A2245 /* GeneralSettingStoreTests.swift */; }; AB6DE897268822390087C579 /* LogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6DE896268822390087C579 /* LogsView.swift */; }; AB706F7927890A6C0025A48A /* AppRouteReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F7827890A6C0025A48A /* AppRouteReducer.swift */; }; AB706F7B278937500025A48A /* FrontpageReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB706F7A278937500025A48A /* FrontpageReducer.swift */; }; @@ -180,13 +179,11 @@ AB90276D291F548700697256 /* AppIcon_NotMyPresident_iPad_Pro@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB902768291F548700697256 /* AppIcon_NotMyPresident_iPad_Pro@2x.png */; }; AB90276E291F548700697256 /* AppIcon_NotMyPresident_iPad.png in Resources */ = {isa = PBXBuildFile; fileRef = AB902769291F548700697256 /* AppIcon_NotMyPresident_iPad.png */; }; AB90276F291F548700697256 /* AppIcon_NotMyPresident@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AB90276A291F548700697256 /* AppIcon_NotMyPresident@2x.png */; }; - ABA12F3227D49CEB0021922D /* AccountSettingStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA12F3127D49CEB0021922D /* AccountSettingStoreTests.swift */; }; ABA732D925A8018A00B3D9AB /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA732D825A8018A00B3D9AB /* Extensions.swift */; }; ABA732DF25A852D800B3D9AB /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA732DE25A852D800B3D9AB /* Filter.swift */; }; ABA9A6BC28EC786100EE28DE /* swiftgen.yml in Resources */ = {isa = PBXBuildFile; fileRef = ABA9A6BB28EC786100EE28DE /* swiftgen.yml */; }; ABA9A6C228EC7BD000EE28DE /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA9A6C128EC7BD000EE28DE /* Strings.swift */; }; ABAB5B9527EF023300198597 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABAB5B9427EF023300198597 /* Extensions.swift */; }; - ABAB5B9727EF03F600198597 /* AppearanceSettingStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABAB5B9627EF03F600198597 /* AppearanceSettingStoreTests.swift */; }; ABAC82FE26BC4A96009F5026 /* OpenCC in Frameworks */ = {isa = PBXBuildFile; productRef = ABAC82FD26BC4A96009F5026 /* OpenCC */; }; ABBB2631278E6EF3007B6149 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBB2630278E6EF3007B6149 /* SearchView.swift */; }; ABBB2636278FB888007B6149 /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = ABBB2635278FB888007B6149 /* SwiftUINavigation */; }; @@ -412,7 +409,6 @@ AB63EADC2699AC9100090535 /* AppEnvMO+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppEnvMO+CoreDataClass.swift"; sourceTree = ""; }; AB69CB7F26B3DABC00699359 /* AdvancedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedList.swift; sourceTree = ""; }; AB69CB8126B3DAF400699359 /* ControlPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlPanel.swift; sourceTree = ""; }; - AB6D106927EBF890003A2245 /* GeneralSettingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingStoreTests.swift; sourceTree = ""; }; AB6DE896268822390087C579 /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = ""; }; AB706F7827890A6C0025A48A /* AppRouteReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteReducer.swift; sourceTree = ""; }; AB706F7A278937500025A48A /* FrontpageReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrontpageReducer.swift; sourceTree = ""; }; @@ -482,14 +478,12 @@ AB902769291F548700697256 /* AppIcon_NotMyPresident_iPad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = AppIcon_NotMyPresident_iPad.png; sourceTree = ""; }; AB90276A291F548700697256 /* AppIcon_NotMyPresident@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon_NotMyPresident@2x.png"; sourceTree = ""; }; AB994DBB25986F7A00E9A367 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; - ABA12F3127D49CEB0021922D /* AccountSettingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingStoreTests.swift; sourceTree = ""; }; ABA732D825A8018A00B3D9AB /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; ABA732DE25A852D800B3D9AB /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; ABA9A6BB28EC786100EE28DE /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = SOURCE_ROOT; }; ABA9A6BE28EC7BA200EE28DE /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Constant.strings; sourceTree = ""; }; ABA9A6C128EC7BD000EE28DE /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; ABAB5B9427EF023300198597 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; - ABAB5B9627EF03F600198597 /* AppearanceSettingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingStoreTests.swift; sourceTree = ""; }; ABB5013026A41EBA00B542D9 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; ABB5013126A41EBA00B542D9 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = ""; }; ABBB2630278E6EF3007B6149 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; @@ -1074,30 +1068,11 @@ ABA12F2E27D49AD10021922D /* Tests */ = { isa = PBXGroup; children = ( - ABA12F2F27D49B610021922D /* Store */, ABD9770C27B65A5300983DE7 /* Parser */, ); path = Tests; sourceTree = ""; }; - ABA12F2F27D49B610021922D /* Store */ = { - isa = PBXGroup; - children = ( - ABA12F3027D49CDA0021922D /* Setting */, - ); - path = Store; - sourceTree = ""; - }; - ABA12F3027D49CDA0021922D /* Setting */ = { - isa = PBXGroup; - children = ( - ABA12F3127D49CEB0021922D /* AccountSettingStoreTests.swift */, - AB6D106927EBF890003A2245 /* GeneralSettingStoreTests.swift */, - ABAB5B9627EF03F600198597 /* AppearanceSettingStoreTests.swift */, - ); - path = Setting; - sourceTree = ""; - }; ABA9A6C028EC7BD000EE28DE /* Generated */ = { isa = PBXGroup; children = ( @@ -1963,14 +1938,11 @@ AB0CFB8227BBBFCE004BD372 /* EhSettingParserTests.swift in Sources */, AB31CD4327B676C300F40E0A /* GalleryMPVKeysParserTests.swift in Sources */, AB31CD3027B666E200F40E0A /* TestError.swift in Sources */, - ABAB5B9727EF03F600198597 /* AppearanceSettingStoreTests.swift in Sources */, ABD9771027B65E3400983DE7 /* GalleryDetailParserTests.swift in Sources */, AB31CD3227B6671400F40E0A /* BanIntervalParserTests.swift in Sources */, ABD9771327B6612400983DE7 /* GreetingParserTests.swift in Sources */, AB31CD3727B6695800F40E0A /* HTMLFilename.swift in Sources */, - ABA12F3227D49CEB0021922D /* AccountSettingStoreTests.swift in Sources */, AB3E9E7426D210B1008FE518 /* TestHelper.swift in Sources */, - AB6D106A27EBF890003A2245 /* GeneralSettingStoreTests.swift in Sources */, AB31CD3B27B66E0300F40E0A /* ListParserTestType.swift in Sources */, ABD9770E27B65A7300983DE7 /* ListParserTests.swift in Sources */, ABAB5B9527EF023300198597 /* Extensions.swift in Sources */, diff --git a/EhPanda/App/Tools/Utilities/AppUtil.swift b/EhPanda/App/Tools/Utilities/AppUtil.swift index 66729c4f..79f1b9a9 100644 --- a/EhPanda/App/Tools/Utilities/AppUtil.swift +++ b/EhPanda/App/Tools/Utilities/AppUtil.swift @@ -15,6 +15,15 @@ struct AppUtil { Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "null" } + private static let internalIsTesting = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil + public static var isTesting: Bool { + #if DEBUG + internalIsTesting + #else + false + #endif + } + static var galleryHost: GalleryHost { let rawValue: String? = UserDefaultsUtil.value(forKey: .galleryHost) return GalleryHost(rawValue: rawValue ?? "") ?? .ehentai diff --git a/EhPanda/DataFlow/AppDelegateReducer.swift b/EhPanda/DataFlow/AppDelegateReducer.swift index 6c3f58cb..53efa716 100644 --- a/EhPanda/DataFlow/AppDelegateReducer.swift +++ b/EhPanda/DataFlow/AppDelegateReducer.swift @@ -69,7 +69,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { - viewStore.send(.appDelegate(.onLaunchFinish)) + if !AppUtil.isTesting { + viewStore.send(.appDelegate(.onLaunchFinish)) + } return true } } diff --git a/EhPandaTests/Resources/Parser/Other/EhSetting.html b/EhPandaTests/Resources/Parser/Other/EhSetting.html index eb38b168..3cd3dee4 100644 --- a/EhPandaTests/Resources/Parser/Other/EhSetting.html +++ b/EhPandaTests/Resources/Parser/Other/EhSetting.html @@ -1,14 +1,14 @@ - + E-Hentai Galleries - The Free Hentai Doujinshi, Manga and Image Gallery System - + - - - + + + @@ -16,8 +16,8 @@ @@ -40,401 +40,411 @@
-
-
Selected Profile:
-
-
- - - -
-
-
-
+
+
Selected Profile:
+
+
+ + + +
+
+
+
-
+
-
+ +

Image Load Settings

-

Do you wish to load images through the Hentai@Home Network, if available?

-
-

-

-

-

-
+

Do you wish to load images through the Hentai@Home Network, if available?

+
+

+

+

+

+
-
-

You appear to be browsing the site from Japan or use a VPN or proxy in this country, which means the site will try to load images from H@H clients in this general geographic region. If this is incorrect, or if you want to use a different region for any reason (like if you are using a split tunneling VPN), you can select a different country below.

-
- Browsing Country: -
+

You appear to be browsing the site from Japan or use a VPN or proxy in this country, which means the site will try to load images from H@H clients in this general geographic region. If this is incorrect, or if you want to use a different region for any reason (like if you are using a split tunneling VPN), you can select a different country below.

+
+ Browsing Country: +

Image Size Settings

-
-

Normally, images are resampled to 1280 pixels of horizontal resolution for online viewing. You can alternatively select one of the following resample resolutions. To avoid murdering the staging servers, resolutions above 1280x are temporarily restricted to donators, people with any hath perk, and people with a UID below 3,000,000.

-
-
-
-
-
-
-
-
+

Normally, images are resampled to 1280 pixels of horizontal resolution for online viewing. You can alternatively select one of the following resample resolutions. To avoid murdering the staging servers, resolutions above 1280x are temporarily restricted to donators, people with any hath perk, and people with a UID below 3,000,000.

+
+
+
+
+
+
+
+
-
-

While the site will automatically scale down images to fit your screen width, you can also manually restrict the maximum display size of an image. Like the automatic scaling, this does not resample the image, as the resizing is done browser-side. (0 = no limit)

-
- - - - - - - -
Horizontal: pixels
Vertical: pixels
-
+

While the site will automatically scale down images to fit your screen width, you can also manually restrict the maximum display size of an image. Like the automatic scaling, this does not resample the image, as the resizing is done browser-side. (0 = no limit)

+
+ + + + + + + +
Horizontal: pixels
Vertical: pixels
+
+

Gallery Name Display

-

Many galleries have both an English/Romanized title and a title in Japanese script. Which gallery name would you like as default?

-
-

-

-
+

Many galleries have both an English/Romanized title and a title in Japanese script. Which gallery name would you like as default?

+
+

+

+

Archiver Settings

-

The default behavior for the Archiver is to confirm the cost and selection for original or resampled archive, then present a link that can be clicked or copied elsewhere. You can change this behavior here.

-
-

-

-

-

-

-

-
+

The default behavior for the Archiver is to confirm the cost and selection for original or resampled archive, then present a link that can be clicked or copied elsewhere. You can change this behavior here.

+
+

+

+

+

+

+

+
-

Front Page Settings

+

Front Page / Search Settings

+ +
+

Which display mode would you like to use on the front and search pages?

+
+

+

+

+

+

+
+
-

Which display mode would you like to use on the front and search pages?

-
-

-

-

-

-

-
+

Which display style would you like for the search range indicator?

+
+

+

+
-

What categories would you like to show by default on the front page and in searches?

-
Doujinshi
Manga
Artist CG
Game CG
Western
Non-H
Image Set
Cosplay
Asian Porn
Misc
-
+

What categories would you like to show by default on the front page and in searches?

+
Doujinshi
Manga
Artist CG
Game CG
Western
Non-H
Image Set
Cosplay
Asian Porn
Misc
+

Favorites

-

Here you can choose and rename your favorite categories.

-
+

Here you can choose and rename your favorite categories.

+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+

You can also select your default sort order for galleries on your favorites page. Note that favorites added prior to the March 2016 revamp did not store a timestamp, and will use the gallery posted time regardless of this setting.

-
-

-

-
+
+

+

+

Ratings

-

By default, galleries that you have rated will appear with red stars for ratings of 2 stars and below, green for ratings between 2.5 and 4 stars, and blue for ratings of 4.5 or 5 stars. You can customize this by entering your desired color combination below.

- - - - - -
Each letter represents one star. The default RRGGB means R(ed) for the first and second star, G(reen) for the third and fourth, and B(lue) for the fifth. You can also use (Y)ellow for the normal stars. Any five-letter R/G/B/Y combo works.
+

By default, galleries that you have rated will appear with red stars for ratings of 2 stars and below, green for ratings between 2.5 and 4 stars, and blue for ratings of 4.5 or 5 stars. You can customize this by entering your desired color combination below.

+ + + + + +
Each letter represents one star. The default RRGGB means R(ed) for the first and second star, G(reen) for the third and fourth, and B(lue) for the fifth. You can also use (Y)ellow for the normal stars. Any five-letter R/G/B/Y combo works.
- -

Tag Namespaces

+ +

Tag Watching Threshold

-

If you want to exclude certain namespaces from a default tag search, you can check those below. Note that this does not prevent galleries with tags in these namespaces from appearing, it just makes it so that when searching tags, it will forego those namespaces.

-
-
+ + + + + +
Recently uploaded galleries will be included on the watched screen if it has at least one watched tag with positive weight, and the sum of weights on its watched tags add up to this value or higher. This threshold can be set between 0 and 9999.
+

Tag Filtering Threshold

- - - - - -
You can soft filter tags by adding them to My Tags with a negative weight. If a gallery has tags that add up to weight below this value, it is filtered from view. This threshold can be set between 0 and -9999.
+ + + + + +
You can soft filter tags by adding them to My Tags with a negative weight. If a gallery has tags that add up to weight below this value, it is filtered from view. This threshold can be set between 0 and -9999.
- -

Tag Watching Threshold

+ +

Show Filtered Removal Count

- - - - - -
Recently uploaded galleries will be included on the watched screen if it has at least one watched tag with positive weight, and the sum of weights on its watched tags add up to this value or higher. This threshold can be set between 0 and 9999.
+

Show the "Your custom filters removed XX galleries from this page" readout?

+
+

+

+

Excluded Languages

-

If you wish to hide galleries in certain languages from the gallery list and searches, select them from the list below.

-

Note that matching galleries will never appear regardless of your search query.

-
- +

If you wish to hide galleries in certain languages from the gallery list and searches, select them from the list below.

+

Note that matching galleries will never appear regardless of your search query.

+
+
- - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + -
OriginalTranslatedRewriteAllOriginalTranslatedRewriteAll
Japanese
Japanese
English
English
Chinese
Chinese
Dutch
Dutch
French
French
German
German
Hungarian
Hungarian
Italian
Italian
Korean
Korean
Polish
Polish
Portuguese
Portuguese
Russian
Russian
Spanish
Spanish
Thai
Thai
Vietnamese
Vietnamese
N/A
N/A
Other
Other
-
+ +
@@ -442,12 +452,12 @@

Excluded Languages

Excluded Uploaders

-

If you wish to hide galleries from certain uploaders from the gallery list and searches, add them below. Put one username per line.

-

Note that galleries from these uploaders will never appear regardless of your search query.

-
- -
-

You are currently using 0 of 1000 exclusion slots. +

If you wish to hide galleries from certain uploaders from the gallery list and searches, add them below. Put one username per line.

+

Note that galleries from these uploaders will never appear regardless of your search query.

+
+ +
+

You are currently using 0 of 1000 exclusion slots.

@@ -455,149 +465,148 @@

Excluded Uploaders

Search Result Count

-

How many results would you like per page for the index/search page and torrent search pages? (Hath Perk: Paging Enlargement Required)

-
-

-

-

-

-
+

How many results would you like per page for the index/search page and torrent search pages? (Hath Perk: Paging Enlargement Required)

+
+

+

+

+

Thumbnail Settings

-

How would you like the mouse-over thumbnails on the front page to load when using List Mode?

-
-

-

-
+

How would you like the mouse-over thumbnails on the front page to load when using List Mode?

+
+

+

+
-

You can set a default thumbnail configuration for all galleries you visit.

-
- +

You can set a default thumbnail configuration for all galleries you visit.

+
+
- - + + - - + + -
Size: -
-
-
Size: +
+
+
Rows: -
-
-
Rows: +
+
+
-
+ +

Thumbnail Scaling

- - - - - -
%Thumbnails on the thumbnail and extended gallery list views can be scaled to a custom value between 75% and 150%.
+ + + + + +
%Thumbnails on the thumbnail and extended gallery list views can be scaled to a custom value between 75% and 150%.

Viewport Override

- - - - - -
pxAllows you to override the virtual width of the site for mobile devices. This is normally determined automatically by your device based on its DPI. Sensible values at 100% thumbnail scale are between 640 and 1400.
+ + + + + +
pxAllows you to override the virtual width of the site for mobile devices. This is normally determined automatically by your device based on its DPI. Sensible values at 100% thumbnail scale are between 640 and 1400.

Gallery Comments

-

Sort order for gallery comments:

-
-

-

-

-
+

Sort order for gallery comments:

+
+

+

+

+
-

Show gallery comment votes:

-
-

-

-
+

Show gallery comment votes:

+
+

+

+

Gallery Tags

-

Sort order for gallery tags:

-
-

-

-
+

Sort order for gallery tags:

+
+

+

+

Gallery Page Numbering

-

Show gallery page numbers: -

-

-

-
+

Show gallery page numbers: +

+

+

+

Original Images

-

Use original images instead of the resampled versions where applicable?

-
-

-

-
+

Use original images instead of the resampled versions where available?

+
+

+

+

Multi-Page Viewer

-

Always use the Multi-Page Viewer? There will still be a link to manually start it if this is left disabled.

-
-

-

-
+

Always use the Multi-Page Viewer? There will still be a link to manually start it if this is left disabled.

+
+

+

+
-

Multi-Page Viewer Display Style:

-
-

-

-

-
+

Multi-Page Viewer Display Style:

+
+

+

+

+
-

Multi-Page Viewer Thumbnail Pane:

-
-

-

-
+

Multi-Page Viewer Thumbnail Pane:

+
+

+

+
- + diff --git a/EhPandaTests/Tests/Parser/Other/EhSettingParserTests.swift b/EhPandaTests/Tests/Parser/Other/EhSettingParserTests.swift index be16ed23..79a670f8 100644 --- a/EhPandaTests/Tests/Parser/Other/EhSettingParserTests.swift +++ b/EhPandaTests/Tests/Parser/Other/EhSettingParserTests.swift @@ -24,13 +24,13 @@ class EhSettingParserTests: XCTestCase, TestHelper { let ehProfile1 = profiles[0] XCTAssertEqual(ehProfile1.value, 1) XCTAssertEqual(ehProfile1.name, "Default Profile") - XCTAssertEqual(ehProfile1.isSelected, true) + XCTAssertEqual(ehProfile1.isSelected, false) XCTAssertEqual(ehProfile1.isDefault, true) let ehProfile2 = profiles[1] XCTAssertEqual(ehProfile2.value, 2) XCTAssertEqual(ehProfile2.name, "EhPanda") - XCTAssertEqual(ehProfile2.isSelected, false) + XCTAssertEqual(ehProfile2.isSelected, true) XCTAssertEqual(ehProfile2.isDefault, false) XCTAssertTrue(EhSetting.verifyEhPandaProfileName(with: ehProfile2.name)) } @@ -42,8 +42,8 @@ class EhSettingParserTests: XCTestCase, TestHelper { XCTAssertEqual(ehSetting.capableImageResolution, .x2400) XCTAssertEqual(ehSetting.capableImageResolutions, EhSetting.ImageResolution.allCases) - XCTAssertEqual(ehSetting.capableSearchResultCount, .twoHundred) - XCTAssertEqual(ehSetting.capableSearchResultCounts, EhSetting.SearchResultCount.allCases) + XCTAssertEqual(ehSetting.capableSearchResultCount, .oneHundred) + XCTAssertEqual(ehSetting.capableSearchResultCounts, [.twentyFive, .fifty, .oneHundred]) XCTAssertEqual(ehSetting.capableThumbnailConfigSize, .large) XCTAssertEqual(ehSetting.capableThumbnailConfigSizes, EhSetting.ThumbnailSize.allCases) @@ -62,6 +62,7 @@ class EhSettingParserTests: XCTestCase, TestHelper { XCTAssertEqual(ehSetting.galleryName, .japanese) XCTAssertEqual(ehSetting.archiverBehavior, .manualSelectManualStart) XCTAssertEqual(ehSetting.displayMode, .compact) + XCTAssertEqual(ehSetting.showSearchRangeIndicator, true) XCTAssertEqual(ehSetting.disabledCategories, .init(repeating: false, count: 10)) XCTAssertEqual(ehSetting.favoriteCategories, [ "Favorites 0", "Favorites 1", "Favorites 2", "Favorites 3", "Favorites 4", @@ -69,14 +70,13 @@ class EhSettingParserTests: XCTestCase, TestHelper { ]) XCTAssertEqual(ehSetting.favoritesSortOrder, .favoritedTime) XCTAssertEqual(ehSetting.ratingsColor, "") - XCTAssertEqual(ehSetting.excludedNamespaces, .init(repeating: false, count: 11)) XCTAssertEqual(ehSetting.tagFilteringThreshold, 0) XCTAssertEqual(ehSetting.tagWatchingThreshold, 0) XCTAssertEqual(ehSetting.excludedLanguages, .init(repeating: false, count: 50)) XCTAssertEqual(ehSetting.excludedUploaders, "") - XCTAssertEqual(ehSetting.searchResultCount, .twoHundred) + XCTAssertEqual(ehSetting.searchResultCount, .twentyFive) XCTAssertEqual(ehSetting.thumbnailLoadTiming, .onMouseOver) - XCTAssertEqual(ehSetting.thumbnailConfigSize, .normal) + XCTAssertEqual(ehSetting.thumbnailConfigSize, .large) XCTAssertEqual(ehSetting.thumbnailConfigRows, .four) XCTAssertEqual(ehSetting.thumbnailScaleFactor, 100) XCTAssertEqual(ehSetting.viewportVirtualWidth, 0) diff --git a/EhPandaTests/Tests/Store/Setting/AccountSettingStoreTests.swift b/EhPandaTests/Tests/Store/Setting/AccountSettingStoreTests.swift deleted file mode 100644 index 18fa8969..00000000 --- a/EhPandaTests/Tests/Store/Setting/AccountSettingStoreTests.swift +++ /dev/null @@ -1,239 +0,0 @@ -// -// AccountSettingStoreTests.swift -// EhPandaTests -// -// Created by 荒木辰造 on R 4/03/06. -// - -import XCTest -@testable import EhPanda -import ComposableArchitecture - -class AccountSettingStoreTests: XCTestCase { - private var noopEnvironment: AccountSettingEnvironment { - .init( - hapticsClient: .noop, - cookieClient: .noop, - clipboardClient: .noop, - uiApplicationClient: .noop - ) - } - private static func getCookiesState(with value: String) -> (CookiesState, CookiesState) { - let ehCookiesState = CookiesState( - host: .ehentai, - igneous: .init(key: Defaults.Cookie.igneous, value: .init(rawValue: value, localizedString: .init()), editingText: value), - memberID: .init(key: Defaults.Cookie.ipbMemberId, value: .init(rawValue: value, localizedString: .init()), editingText: value), - passHash: .init(key: Defaults.Cookie.ipbPassHash, value: .init(rawValue: value, localizedString: .init()), editingText: value) - ) - let exCookiesState = CookiesState( - host: .exhentai, - igneous: .init(key: Defaults.Cookie.igneous, value: .init(rawValue: value, localizedString: .init()), editingText: value), - memberID: .init(key: Defaults.Cookie.ipbMemberId, value: .init(rawValue: value, localizedString: .init()), editingText: value), - passHash: .init(key: Defaults.Cookie.ipbPassHash, value: .init(rawValue: value, localizedString: .init()), editingText: value) - ) - return (ehCookiesState, exCookiesState) - } - private static func setCookies(with state: CookiesState) { - _ = CookieClient.live.setCookies(state: state).sink(receiveValue: { _ in }) - } - @discardableResult private static func teardownCookies(value: String? = nil) -> String { - let initialValue = UUID().uuidString - let (initialEhCookiesState, initialExCookiesState) = getCookiesState(with: value ?? initialValue) - setCookies(with: initialEhCookiesState) - setCookies(with: initialExCookiesState) - return value ?? initialValue - } - - override class func tearDown() { - super.tearDown() - - teardownCookies(value: .init()) - } - - func testCookies(with value: String) throws { - [Defaults.Cookie.igneous, Defaults.Cookie.ipbMemberId, Defaults.Cookie.ipbPassHash] - .flatMap({ key in [Defaults.URL.ehentai, Defaults.URL.exhentai].map({ ($0, key) }) }) - .map(CookieClient.live.getCookie) - .forEach({ XCTAssertEqual($0, .init(rawValue: value, localizedString: .init())) }) - } - - func testBinding() throws { - let store = TestStore( - initialState: AccountSettingState( - route: .ehSetting, - ehCookiesState: .empty(.exhentai), - exCookiesState: .empty(.ehentai), - loginState: .init(route: .webView(.mock)), - ehSettingState: .init(route: .deleteProfile) - ), - reducer: accountSettingReducer, - environment: AccountSettingEnvironment( - hapticsClient: .noop, - cookieClient: .live, - clipboardClient: .noop, - uiApplicationClient: .noop - ) - ) - - store.send(.set(\.$route, nil)) { - $0.route = nil - } - store.receive(.clearSubStates) { - $0.loginState = .init() - $0.ehSettingState = .init() - } - store.receive(.login(.teardown)) - store.receive(.ehSetting(.teardown)) - store.send(.set(\.$route, .login)) { - $0.route = .login - } - - AccountSettingStoreTests.teardownCookies() - - let testValue = UUID().uuidString - let (ehCookiesState, exCookiesState) = AccountSettingStoreTests.getCookiesState(with: testValue) - store.send(.set(\.$ehCookiesState, ehCookiesState)) { - $0.ehCookiesState = ehCookiesState - } - store.send(.set(\.$exCookiesState, exCookiesState)) { - $0.exCookiesState = exCookiesState - } - try testCookies(with: testValue) - } - - func testSetNavigation() throws { - let store = TestStore( - initialState: AccountSettingState( - route: .ehSetting, - loginState: .init(route: .webView(.mock)), - ehSettingState: .init(route: .deleteProfile) - ), - reducer: accountSettingReducer, - environment: noopEnvironment - ) - - store.send(.setNavigation(nil)) { - $0.route = nil - } - store.receive(.clearSubStates) { - $0.loginState = .init() - $0.ehSettingState = .init() - } - store.receive(.login(.teardown)) - store.receive(.ehSetting(.teardown)) - store.send(.setNavigation(.webView(.mock))) { - $0.route = .webView(.mock) - } - } - - func testOnLogoutConfirmButtonTapped() throws { - let store = TestStore( - initialState: AccountSettingState( - ehCookiesState: .empty(.exhentai), - exCookiesState: .empty(.ehentai) - ), - reducer: accountSettingReducer, - environment: AccountSettingEnvironment( - hapticsClient: .noop, - cookieClient: .live, - clipboardClient: .noop, - uiApplicationClient: .noop - ) - ) - - let initialValue = AccountSettingStoreTests.teardownCookies() - let (ehCookiesState, exCookiesState) = AccountSettingStoreTests.getCookiesState(with: initialValue) - store.send(.onLogoutConfirmButtonTapped) - store.receive(.loadCookies) { - $0.ehCookiesState = ehCookiesState - $0.exCookiesState = exCookiesState - } - try testCookies(with: initialValue) - } - - func testClearSubStates() throws { - let store = TestStore( - initialState: AccountSettingState( - loginState: .init(route: .webView(.mock)), - ehSettingState: .init(route: .deleteProfile) - ), - reducer: accountSettingReducer, - environment: noopEnvironment - ) - - store.send(.clearSubStates) { - $0.loginState = .init() - $0.ehSettingState = .init() - } - store.receive(.login(.teardown)) - store.receive(.ehSetting(.teardown)) - } - - func testLoadCookies() throws { - let store = TestStore( - initialState: AccountSettingState( - ehCookiesState: .empty(.exhentai), - exCookiesState: .empty(.ehentai) - ), - reducer: accountSettingReducer, - environment: noopEnvironment - ) - - store.send(.loadCookies) { - $0.ehCookiesState = .init( - host: .ehentai, - igneous: .init(key: Defaults.Cookie.igneous, value: .empty), - memberID: .init(key: Defaults.Cookie.ipbMemberId, value: .empty), - passHash: .init(key: Defaults.Cookie.ipbPassHash, value: .empty) - ) - $0.exCookiesState = .init( - host: .exhentai, - igneous: .init(key: Defaults.Cookie.igneous, value: .empty), - memberID: .init(key: Defaults.Cookie.ipbMemberId, value: .empty), - passHash: .init(key: Defaults.Cookie.ipbPassHash, value: .empty) - ) - } - } - - func testCopyCookies() throws { - let store = TestStore( - initialState: AccountSettingState( - route: .ehSetting - ), - reducer: accountSettingReducer, - environment: noopEnvironment - ) - - store.send(.copyCookies(.ehentai)) - store.receive(.setNavigation(.hud)) { - $0.route = .hud - } - } - - func testLoginLoginDone() throws { - let store = TestStore( - initialState: AccountSettingState( - route: .ehSetting, - loginState: .init(route: .webView(.mock), loginState: .loading), - ehSettingState: .init(route: .deleteProfile) - ), - reducer: accountSettingReducer, - environment: noopEnvironment - ) - - store.send(.login(.loginDone(.success(nil)))) { - $0.loginState = .init(route: nil, loginState: .idle) - } - if noopEnvironment.cookieClient.didLogin { - store.receive(.setNavigation(nil)) { - $0.route = nil - } - store.receive(.clearSubStates) { - $0.loginState = .init() - $0.ehSettingState = .init() - } - store.receive(.login(.teardown)) - store.receive(.ehSetting(.teardown)) - } - } -} diff --git a/EhPandaTests/Tests/Store/Setting/AppearanceSettingStoreTests.swift b/EhPandaTests/Tests/Store/Setting/AppearanceSettingStoreTests.swift deleted file mode 100644 index 1c072520..00000000 --- a/EhPandaTests/Tests/Store/Setting/AppearanceSettingStoreTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// AppearanceSettingStoreTests.swift -// EhPandaTests -// -// Created by 荒木辰造 on R 4/03/26. -// - -import XCTest -@testable import EhPanda -import ComposableArchitecture - -class AppearanceSettingStoreTests: XCTestCase { - private var noopEnvironment: AppearanceSettingEnvironment { - .init() - } - - func testBinding() throws { - let store = TestStore( - initialState: AppearanceSettingState( - route: .appIcon - ), - reducer: appearanceSettingReducer, - environment: noopEnvironment - ) - store.send(.set(\.$route, nil)) { - $0.route = nil - } - } - - func testSetNavigation() throws { - let store = TestStore( - initialState: AppearanceSettingState( - route: .appIcon - ), - reducer: appearanceSettingReducer, - environment: noopEnvironment - ) - store.send(.setNavigation(nil)) { - $0.route = nil - } - } -} diff --git a/EhPandaTests/Tests/Store/Setting/GeneralSettingStoreTests.swift b/EhPandaTests/Tests/Store/Setting/GeneralSettingStoreTests.swift deleted file mode 100644 index 08d5281a..00000000 --- a/EhPandaTests/Tests/Store/Setting/GeneralSettingStoreTests.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// GeneralSettingStoreTests.swift -// EhPandaTests -// -// Created by 荒木辰造 on R 4/03/24. -// - -import XCTest -@testable import EhPanda -import ComposableArchitecture - -class GeneralSettingStoreTests: XCTestCase { - private var noopEnvironment: GeneralSettingEnvironment { - .init( - fileClient: .noop, - loggerClient: .noop, - libraryClient: .noop, - databaseClient: .noop, - uiApplicationClient: .noop, - authorizationClient: .noop - ) - } - - func testBinding() throws { - let store = TestStore( - initialState: GeneralSettingState( - route: .clearCache, - logsState: .init(route: .log( - .init(fileName: .init(), contents: .init()) - )) - ), - reducer: generalSettingReducer, - environment: noopEnvironment - ) - - store.send(.set(\.$route, nil)) { - $0.route = nil - } - store.receive(.clearSubStates) { - $0.logsState = .init() - } - store.receive(.logs(.teardown)) - store.send(.set(\.$route, .logs)) { - $0.route = .logs - } - } - - func testSetNavigation() throws { - let store = TestStore( - initialState: GeneralSettingState( - route: .clearCache, - logsState: .init(route: .log( - .init(fileName: .init(), contents: .init()) - )) - ), - reducer: generalSettingReducer, - environment: noopEnvironment - ) - - store.send(.setNavigation(nil)) { - $0.route = nil - } - store.receive(.clearSubStates) { - $0.logsState = .init() - } - store.receive(.logs(.teardown)) - store.send(.set(\.$route, .logs)) { - $0.route = .logs - } - } - - func testClearSubStates() throws { - let store = TestStore( - initialState: GeneralSettingState( - logsState: .init(route: .log( - .init(fileName: .init(), contents: .init()) - )) - ), - reducer: generalSettingReducer, - environment: noopEnvironment - ) - - store.send(.clearSubStates) { - $0.logsState = .init() - } - store.receive(.logs(.teardown)) - } - - func testClearWebImageCache() throws { - let store = TestStore( - initialState: GeneralSettingState(), - reducer: generalSettingReducer, - environment: GeneralSettingEnvironment( - fileClient: .noop, - loggerClient: .noop, - libraryClient: .live, - databaseClient: .live, - uiApplicationClient: .noop, - authorizationClient: .noop - ) - ) - store.send(.clearWebImageCache) - XCTWaiter.wait(timeout: 5) - store.receive(.calculateWebImageDiskCache) - store.receive(.calculateWebImageDiskCacheDone(0)) { - let formatter = ByteCountFormatter() - formatter.allowedUnits = .useAll - $0.diskImageCacheSize = formatter.string(fromByteCount: 0) - } - } -} From 8d1be229df3f87c96a4fe87323e68865625fa907 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 12:26:31 +0800 Subject: [PATCH 25/29] Handle binding actions first --- EhPanda/DataFlow/AppReducer.swift | 4 ++-- EhPanda/DataFlow/AppRouteReducer.swift | 4 ++-- EhPanda/View/Detail/Archives/ArchivesReducer.swift | 4 ++-- EhPanda/View/Detail/Comments/CommentsReducer.swift | 4 ++-- EhPanda/View/Detail/DetailReducer.swift | 4 ++-- EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift | 4 ++-- EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift | 4 ++-- EhPanda/View/Detail/Previews/PreviewsReducer.swift | 4 ++-- EhPanda/View/Detail/Torrents/TorrentsReducer.swift | 4 ++-- EhPanda/View/Favorites/FavoritesReducer.swift | 4 ++-- EhPanda/View/Home/Frontpage/FrontpageReducer.swift | 4 ++-- EhPanda/View/Home/History/HistoryReducer.swift | 4 ++-- EhPanda/View/Home/HomeReducer.swift | 4 ++-- EhPanda/View/Home/Popular/PopularReducer.swift | 4 ++-- EhPanda/View/Home/Toplists/ToplistsReducer.swift | 4 ++-- EhPanda/View/Home/Watched/WatchedReducer.swift | 4 ++-- EhPanda/View/Migration/MigrationReducer.swift | 4 ++-- EhPanda/View/Reading/ReadingReducer.swift | 4 ++-- EhPanda/View/Search/SearchReducer.swift | 4 ++-- EhPanda/View/Search/SearchRootReducer.swift | 4 ++-- EhPanda/View/Search/Support/QuickSearchReducer.swift | 4 ++-- .../View/Setting/AccountSetting/AccountSettingReducer.swift | 4 ++-- .../Setting/AppearanceSetting/AppearanceSettingReducer.swift | 4 ++-- EhPanda/View/Setting/EhSetting/EhSettingReducer.swift | 4 ++-- .../View/Setting/GeneralSetting/GeneralSettingReducer.swift | 4 ++-- EhPanda/View/Setting/Login/LoginReducer.swift | 4 ++-- EhPanda/View/Setting/Logs/LogsReducer.swift | 4 ++-- EhPanda/View/Setting/SettingReducer.swift | 4 ++-- EhPanda/View/Support/FiltersReducer.swift | 4 ++-- 29 files changed, 58 insertions(+), 58 deletions(-) diff --git a/EhPanda/DataFlow/AppReducer.swift b/EhPanda/DataFlow/AppReducer.swift index 417d225f..684c0121 100644 --- a/EhPanda/DataFlow/AppReducer.swift +++ b/EhPanda/DataFlow/AppReducer.swift @@ -42,6 +42,8 @@ struct AppReducer: ReducerProtocol { var body: some ReducerProtocol { LoggingReducer { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.appRouteState.$route): @@ -198,8 +200,6 @@ struct AppReducer: ReducerProtocol { 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) - - BindingReducer() } } } diff --git a/EhPanda/DataFlow/AppRouteReducer.swift b/EhPanda/DataFlow/AppRouteReducer.swift index 404d117d..9b410b04 100644 --- a/EhPanda/DataFlow/AppRouteReducer.swift +++ b/EhPanda/DataFlow/AppRouteReducer.swift @@ -54,6 +54,8 @@ struct AppRouteReducer: ReducerProtocol { @Dependency(\.urlClient) private var urlClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -172,7 +174,5 @@ struct AppRouteReducer: ReducerProtocol { ) Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Detail/Archives/ArchivesReducer.swift b/EhPanda/View/Detail/Archives/ArchivesReducer.swift index b6e537d0..3e180958 100644 --- a/EhPanda/View/Detail/Archives/ArchivesReducer.swift +++ b/EhPanda/View/Detail/Archives/ArchivesReducer.swift @@ -50,6 +50,8 @@ struct ArchivesReducer: ReducerProtocol { @Dependency(\.cookieClient) private var cookieClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding: @@ -141,7 +143,5 @@ struct ArchivesReducer: ReducerProtocol { return .fireAndForget({ hapticsClient.generateNotificationFeedback(isSuccess ? .success : .error) }) } } - - BindingReducer() } } diff --git a/EhPanda/View/Detail/Comments/CommentsReducer.swift b/EhPanda/View/Detail/Comments/CommentsReducer.swift index dc69968c..98ebffaf 100644 --- a/EhPanda/View/Detail/Comments/CommentsReducer.swift +++ b/EhPanda/View/Detail/Comments/CommentsReducer.swift @@ -71,6 +71,8 @@ struct CommentsReducer: ReducerProtocol { @Dependency(\.urlClient) private var urlClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -220,7 +222,5 @@ struct CommentsReducer: ReducerProtocol { case: /Route.postComment, hapticsClient: hapticsClient ) - - BindingReducer() } } diff --git a/EhPanda/View/Detail/DetailReducer.swift b/EhPanda/View/Detail/DetailReducer.swift index 2074f317..0cb4c5d0 100644 --- a/EhPanda/View/Detail/DetailReducer.swift +++ b/EhPanda/View/Detail/DetailReducer.swift @@ -117,6 +117,8 @@ struct DetailReducer: ReducerProtocol { var body: some ReducerProtocol { RecurseReducer { (self) in + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -414,7 +416,5 @@ struct DetailReducer: ReducerProtocol { 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) - - BindingReducer() } } diff --git a/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift b/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift index cfc371ad..d1aa1d7b 100644 --- a/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift +++ b/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift @@ -65,6 +65,8 @@ struct DetailSearchReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -181,7 +183,5 @@ struct DetailSearchReducer: ReducerProtocol { Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) Scope(state: \.quickDetailSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift b/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift index f0da6117..9433f90b 100644 --- a/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift +++ b/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift @@ -27,6 +27,8 @@ struct GalleryInfosReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding: @@ -40,7 +42,5 @@ struct GalleryInfosReducer: ReducerProtocol { ) } } - - BindingReducer() } } diff --git a/EhPanda/View/Detail/Previews/PreviewsReducer.swift b/EhPanda/View/Detail/Previews/PreviewsReducer.swift index 581982a8..ec25a37b 100644 --- a/EhPanda/View/Detail/Previews/PreviewsReducer.swift +++ b/EhPanda/View/Detail/Previews/PreviewsReducer.swift @@ -57,6 +57,8 @@ struct PreviewsReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -137,7 +139,5 @@ struct PreviewsReducer: ReducerProtocol { ) Scope(state: \.readingState, action: /Action.reading, child: ReadingReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Detail/Torrents/TorrentsReducer.swift b/EhPanda/View/Detail/Torrents/TorrentsReducer.swift index b6285b77..24e2d304 100644 --- a/EhPanda/View/Detail/Torrents/TorrentsReducer.swift +++ b/EhPanda/View/Detail/Torrents/TorrentsReducer.swift @@ -45,6 +45,8 @@ struct TorrentsReducer: ReducerProtocol { @Dependency(\.fileClient) private var fileClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding: @@ -106,7 +108,5 @@ struct TorrentsReducer: ReducerProtocol { case: /Route.share, hapticsClient: hapticsClient ) - - BindingReducer() } } diff --git a/EhPanda/View/Favorites/FavoritesReducer.swift b/EhPanda/View/Favorites/FavoritesReducer.swift index c592d4d2..c2c694cf 100644 --- a/EhPanda/View/Favorites/FavoritesReducer.swift +++ b/EhPanda/View/Favorites/FavoritesReducer.swift @@ -76,6 +76,8 @@ struct FavoritesReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -188,7 +190,5 @@ struct FavoritesReducer: ReducerProtocol { Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Home/Frontpage/FrontpageReducer.swift b/EhPanda/View/Home/Frontpage/FrontpageReducer.swift index 3c94b90b..b4574355 100644 --- a/EhPanda/View/Home/Frontpage/FrontpageReducer.swift +++ b/EhPanda/View/Home/Frontpage/FrontpageReducer.swift @@ -65,6 +65,8 @@ struct FrontpageReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -160,7 +162,5 @@ struct FrontpageReducer: ReducerProtocol { Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Home/History/HistoryReducer.swift b/EhPanda/View/Home/History/HistoryReducer.swift index 55bc8f02..547275c7 100644 --- a/EhPanda/View/Home/History/HistoryReducer.swift +++ b/EhPanda/View/Home/History/HistoryReducer.swift @@ -49,6 +49,8 @@ struct HistoryReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -92,7 +94,5 @@ struct HistoryReducer: ReducerProtocol { } Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Home/HomeReducer.swift b/EhPanda/View/Home/HomeReducer.swift index ce8c559b..0089bd72 100644 --- a/EhPanda/View/Home/HomeReducer.swift +++ b/EhPanda/View/Home/HomeReducer.swift @@ -94,6 +94,8 @@ struct HomeReducer: ReducerProtocol { @Dependency(\.libraryClient) private var libraryClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -253,7 +255,5 @@ struct HomeReducer: ReducerProtocol { Scope(state: \.watchedState, action: /Action.watched, child: WatchedReducer.init) Scope(state: \.historyState, action: /Action.history, child: HistoryReducer.init) Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Home/Popular/PopularReducer.swift b/EhPanda/View/Home/Popular/PopularReducer.swift index 045d09f6..a6141703 100644 --- a/EhPanda/View/Home/Popular/PopularReducer.swift +++ b/EhPanda/View/Home/Popular/PopularReducer.swift @@ -53,6 +53,8 @@ struct PopularReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -110,7 +112,5 @@ struct PopularReducer: ReducerProtocol { Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Home/Toplists/ToplistsReducer.swift b/EhPanda/View/Home/Toplists/ToplistsReducer.swift index f92631a3..6497de5b 100644 --- a/EhPanda/View/Home/Toplists/ToplistsReducer.swift +++ b/EhPanda/View/Home/Toplists/ToplistsReducer.swift @@ -86,6 +86,8 @@ struct ToplistsReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -200,7 +202,5 @@ struct ToplistsReducer: ReducerProtocol { } Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Home/Watched/WatchedReducer.swift b/EhPanda/View/Home/Watched/WatchedReducer.swift index e2edb13c..6f583d64 100644 --- a/EhPanda/View/Home/Watched/WatchedReducer.swift +++ b/EhPanda/View/Home/Watched/WatchedReducer.swift @@ -65,6 +65,8 @@ struct WatchedReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -178,7 +180,5 @@ struct WatchedReducer: ReducerProtocol { Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Migration/MigrationReducer.swift b/EhPanda/View/Migration/MigrationReducer.swift index 97889fe5..3488ed04 100644 --- a/EhPanda/View/Migration/MigrationReducer.swift +++ b/EhPanda/View/Migration/MigrationReducer.swift @@ -32,6 +32,8 @@ struct MigrationReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding: @@ -72,7 +74,5 @@ struct MigrationReducer: ReducerProtocol { } } } - - BindingReducer() } } diff --git a/EhPanda/View/Reading/ReadingReducer.swift b/EhPanda/View/Reading/ReadingReducer.swift index 73b59bee..96b5560b 100644 --- a/EhPanda/View/Reading/ReadingReducer.swift +++ b/EhPanda/View/Reading/ReadingReducer.swift @@ -185,6 +185,8 @@ struct ReadingReducer: ReducerProtocol { @Dependency(\.urlClient) private var urlClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$showsSliderPreview): @@ -597,7 +599,5 @@ struct ReadingReducer: ReducerProtocol { case: /Route.share, hapticsClient: hapticsClient ) - - BindingReducer() } } diff --git a/EhPanda/View/Search/SearchReducer.swift b/EhPanda/View/Search/SearchReducer.swift index 2b35d35e..f4ea5499 100644 --- a/EhPanda/View/Search/SearchReducer.swift +++ b/EhPanda/View/Search/SearchReducer.swift @@ -65,6 +65,8 @@ struct SearchReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -183,7 +185,5 @@ struct SearchReducer: ReducerProtocol { Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Search/SearchRootReducer.swift b/EhPanda/View/Search/SearchRootReducer.swift index da3f97bd..a3e23634 100644 --- a/EhPanda/View/Search/SearchRootReducer.swift +++ b/EhPanda/View/Search/SearchRootReducer.swift @@ -87,6 +87,8 @@ struct SearchRootReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -186,7 +188,5 @@ struct SearchRootReducer: ReducerProtocol { Scope(state: \.filtersState, action: /Action.filters, child: FiltersReducer.init) Scope(state: \.quickSearchState, action: /Action.quickSearch, child: QuickSearchReducer.init) Scope(state: \.detailState, action: /Action.detail, child: DetailReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Search/Support/QuickSearchReducer.swift b/EhPanda/View/Search/Support/QuickSearchReducer.swift index 2b448e89..a1449c90 100644 --- a/EhPanda/View/Search/Support/QuickSearchReducer.swift +++ b/EhPanda/View/Search/Support/QuickSearchReducer.swift @@ -62,6 +62,8 @@ struct QuickSearchReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -128,7 +130,5 @@ struct QuickSearchReducer: ReducerProtocol { return .none } } - - BindingReducer() } } diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift index 8938624c..78e025a8 100644 --- a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift @@ -46,6 +46,8 @@ struct AccountSettingReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -106,8 +108,6 @@ struct AccountSettingReducer: ReducerProtocol { Scope(state: \.loginState, action: /Action.login, child: LoginReducer.init) Scope(state: \.ehSettingState, action: /Action.ehSetting, child: EhSettingReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift index 6f285ca3..6eaa6b3d 100644 --- a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift +++ b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift @@ -22,6 +22,8 @@ struct AppearanceSettingReducer: ReducerProtocol { } var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding: @@ -32,7 +34,5 @@ struct AppearanceSettingReducer: ReducerProtocol { return .none } } - - BindingReducer() } } diff --git a/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift b/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift index 51951970..c2ece436 100644 --- a/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift +++ b/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift @@ -55,6 +55,8 @@ struct EhSettingReducer: ReducerProtocol { @Dependency(\.cookieClient) private var cookieClient public var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding: @@ -136,7 +138,5 @@ struct EhSettingReducer: ReducerProtocol { case: /Route.webView, hapticsClient: hapticsClient ) - - BindingReducer() } } diff --git a/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift b/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift index 2aa40761..1fcb65d4 100644 --- a/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift +++ b/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift @@ -48,6 +48,8 @@ struct GeneralSettingReducer: ReducerProtocol { @Dependency(\.libraryClient) private var libraryClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$route): @@ -101,7 +103,5 @@ struct GeneralSettingReducer: ReducerProtocol { } Scope(state: \.logsState, action: /Action.logs, child: LogsReducer.init) - - BindingReducer() } } diff --git a/EhPanda/View/Setting/Login/LoginReducer.swift b/EhPanda/View/Setting/Login/LoginReducer.swift index 735ba22f..fe2c7803 100644 --- a/EhPanda/View/Setting/Login/LoginReducer.swift +++ b/EhPanda/View/Setting/Login/LoginReducer.swift @@ -51,6 +51,8 @@ struct LoginReducer: ReducerProtocol { @Dependency(\.cookieClient) private var cookieClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding: @@ -94,7 +96,5 @@ struct LoginReducer: ReducerProtocol { case: /Route.webView, hapticsClient: hapticsClient ) - - BindingReducer() } } diff --git a/EhPanda/View/Setting/Logs/LogsReducer.swift b/EhPanda/View/Setting/Logs/LogsReducer.swift index 5bfa9bd2..2b9e72d1 100644 --- a/EhPanda/View/Setting/Logs/LogsReducer.swift +++ b/EhPanda/View/Setting/Logs/LogsReducer.swift @@ -38,6 +38,8 @@ struct LogsReducer: ReducerProtocol { @Dependency(\.fileClient) private var fileClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding: @@ -78,7 +80,5 @@ struct LogsReducer: ReducerProtocol { return .none } } - - BindingReducer() } } diff --git a/EhPanda/View/Setting/SettingReducer.swift b/EhPanda/View/Setting/SettingReducer.swift index 8fc87f07..1e56cabd 100644 --- a/EhPanda/View/Setting/SettingReducer.swift +++ b/EhPanda/View/Setting/SettingReducer.swift @@ -108,6 +108,8 @@ struct SettingReducer: ReducerProtocol { @Dependency(\.dfClient) private var dfClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$setting.galleryHost): @@ -456,7 +458,5 @@ struct SettingReducer: ReducerProtocol { 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) - - BindingReducer() } } diff --git a/EhPanda/View/Support/FiltersReducer.swift b/EhPanda/View/Support/FiltersReducer.swift index cde3c73a..83121f01 100644 --- a/EhPanda/View/Support/FiltersReducer.swift +++ b/EhPanda/View/Support/FiltersReducer.swift @@ -41,6 +41,8 @@ struct FiltersReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient var body: some ReducerProtocol { + BindingReducer() + Reduce { state, action in switch action { case .binding(\.$searchFilter): @@ -108,7 +110,5 @@ struct FiltersReducer: ReducerProtocol { return .none } } - - BindingReducer() } } From 63eaaef14abbc306ea480d21be71f777359f9d11 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 19:59:21 +0800 Subject: [PATCH 26/29] Fix MPV parsing --- EhPanda/Network/Request.swift | 6 +++--- EhPanda/View/Reading/ReadingReducer.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/EhPanda/Network/Request.swift b/EhPanda/Network/Request.swift index c1275f8e..768031bb 100644 --- a/EhPanda/Network/Request.swift +++ b/EhPanda/Network/Request.swift @@ -612,9 +612,9 @@ struct GalleryMPVImageURLRequest: Request { let index: Int let mpvKey: String let mpvImageKey: String - let skipServerIdentifier: String? + let skipServerIdentifier: Int? - var publisher: AnyPublisher<(URL, URL?, String), AppError> { + var publisher: AnyPublisher<(URL, URL?, Int), AppError> { var params: [String: Any] = [ "method": "imagedispatch", "gid": gid, @@ -638,7 +638,7 @@ struct GalleryMPVImageURLRequest: Request { .jsonObject(with: data) as? [String: Any], let imageURLString = dict["i"] as? String, let imageURL = URL(string: imageURLString), - let skipServerIdentifier = dict["s"] as? String + let skipServerIdentifier = dict["s"] as? Int else { throw AppError.parseFailed } if let originalImageURLStringSlice = dict["lf"] as? String { diff --git a/EhPanda/View/Reading/ReadingReducer.swift b/EhPanda/View/Reading/ReadingReducer.swift index 96b5560b..15bf9682 100644 --- a/EhPanda/View/Reading/ReadingReducer.swift +++ b/EhPanda/View/Reading/ReadingReducer.swift @@ -69,7 +69,7 @@ struct ReadingReducer: ReducerProtocol { var mpvKey: String? var mpvImageKeys = [Int: String]() - var mpvSkipServerIdentifiers = [Int: String]() + var mpvSkipServerIdentifiers = [Int: Int]() @BindingState var showsPanel = false @BindingState var showsSliderPreview = false @@ -172,7 +172,7 @@ struct ReadingReducer: ReducerProtocol { case fetchMPVKeys(Int, URL) case fetchMPVKeysDone(Int, Result<(String, [Int: String]), AppError>) case fetchMPVImageURL(Int, Bool) - case fetchMPVImageURLDone(Int, Result<(URL, URL?, String), AppError>) + case fetchMPVImageURLDone(Int, Result<(URL, URL?, Int), AppError>) } @Dependency(\.appDelegateClient) private var appDelegateClient From ef9fef5727ad4c4753e872c6956fc1eb8d74e7f7 Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 20:08:38 +0800 Subject: [PATCH 27/29] Remove spaces when setting cookies --- EhPanda/App/Tools/Clients/CookieClient.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/EhPanda/App/Tools/Clients/CookieClient.swift b/EhPanda/App/Tools/Clients/CookieClient.swift index c3d70b6c..86143d11 100644 --- a/EhPanda/App/Tools/Clients/CookieClient.swift +++ b/EhPanda/App/Tools/Clients/CookieClient.swift @@ -218,11 +218,18 @@ extension CookieClient { // MARK: SetCookies extension CookieClient { - func setCookies(state: CookiesState) -> EffectTask { + func setCookies(state: CookiesState, trimsSpaces: Bool = true) -> EffectTask { let effects: [EffectTask] = state.allCases .flatMap { subState in state.host.cookieURLs - .map({ setOrEditCookie(for: $0, key: subState.key, value: subState.editingText) }) + .map { + setOrEditCookie( + for: $0, + key: subState.key, + value: trimsSpaces + ? subState.editingText .trimmingCharacters(in: .whitespaces) : subState.editingText + ) + } } return effects.isEmpty ? .none : .merge(effects) } From 157d1003f018f16f62c7b9aa9bbd4df92f1d179c Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 23:42:48 +0800 Subject: [PATCH 28/29] Fix tag suggestion feature --- EhPanda/App/de.lproj/Localizable.strings | 3 +- EhPanda/App/en.lproj/Localizable.strings | 3 +- EhPanda/App/ja.lproj/Localizable.strings | 3 +- EhPanda/App/ko.lproj/Localizable.strings | 3 +- EhPanda/App/zh-Hans.lproj/Localizable.strings | 3 +- .../App/zh-Hant-HK.lproj/Localizable.strings | 3 +- .../App/zh-Hant-TW.lproj/Localizable.strings | 3 +- EhPanda/App/zh-Hant.lproj/Localizable.strings | 3 +- .../DetailSearch/DetailSearchView.swift | 3 +- EhPanda/View/Favorites/FavoritesView.swift | 3 +- EhPanda/View/Home/Watched/WatchedView.swift | 3 +- EhPanda/View/Search/SearchRootView.swift | 7 +- EhPanda/View/Search/SearchView.swift | 3 +- .../Components/TagSuggestionView.swift | 113 +++++++----------- 14 files changed, 71 insertions(+), 85 deletions(-) diff --git a/EhPanda/App/de.lproj/Localizable.strings b/EhPanda/App/de.lproj/Localizable.strings index f370f044..4d8c07ea 100644 --- a/EhPanda/App/de.lproj/Localizable.strings +++ b/EhPanda/App/de.lproj/Localizable.strings @@ -139,8 +139,9 @@ "search_view.section.title.recently_searched" = "Recently searched"; "search_view.section.title.recently_seen" = "Recently seen"; "search_view.section.title.quick_search" = "Quick search"; -// Searchable prompt +// Searchable "searchable.prompt.filter" = "Filter"; +"searchable.title.matches_count" = "Found %d matches."; // MARK: QuickSearchView "quick_search_view.title.quick_search" = "Quick search"; diff --git a/EhPanda/App/en.lproj/Localizable.strings b/EhPanda/App/en.lproj/Localizable.strings index 9a742b9a..f01943a1 100644 --- a/EhPanda/App/en.lproj/Localizable.strings +++ b/EhPanda/App/en.lproj/Localizable.strings @@ -139,8 +139,9 @@ "search_view.section.title.recently_searched" = "Recently searched"; "search_view.section.title.recently_seen" = "Recently seen"; "search_view.section.title.quick_search" = "Quick search"; -// Searchable prompt +// Searchable "searchable.prompt.filter" = "Filter"; +"searchable.title.matches_count" = "Found %d matches."; // MARK: QuickSearchView "quick_search_view.title.quick_search" = "Quick search"; diff --git a/EhPanda/App/ja.lproj/Localizable.strings b/EhPanda/App/ja.lproj/Localizable.strings index e1e1e5f2..6f5dff7e 100644 --- a/EhPanda/App/ja.lproj/Localizable.strings +++ b/EhPanda/App/ja.lproj/Localizable.strings @@ -139,8 +139,9 @@ "search_view.section.title.recently_searched" = "最近検索した項目"; "search_view.section.title.recently_seen" = "最近閲覧した項目"; "search_view.section.title.quick_search" = "クイック検索"; -// Searchable prompt +// Searchable "searchable.prompt.filter" = "フィルター"; +"searchable.title.matches_count" = "%d 件の該当項目"; // MARK: QuickSearchView "quick_search_view.title.quick_search" = "クイック検索"; diff --git a/EhPanda/App/ko.lproj/Localizable.strings b/EhPanda/App/ko.lproj/Localizable.strings index a83fdc00..6f42c533 100644 --- a/EhPanda/App/ko.lproj/Localizable.strings +++ b/EhPanda/App/ko.lproj/Localizable.strings @@ -139,8 +139,9 @@ "search_view.section.title.recently_searched" = "Recently searched"; "search_view.section.title.recently_seen" = "Recently seen"; "search_view.section.title.quick_search" = "빠른 검색"; -// Searchable prompt +// Searchable "searchable.prompt.filter" = "Filter"; +"searchable.title.matches_count" = "Found %d matches."; // MARK: QuickSearchView "quick_search_view.title.quick_search" = "빠른 검색"; diff --git a/EhPanda/App/zh-Hans.lproj/Localizable.strings b/EhPanda/App/zh-Hans.lproj/Localizable.strings index 0e11b221..75a7e1b7 100644 --- a/EhPanda/App/zh-Hans.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hans.lproj/Localizable.strings @@ -139,8 +139,9 @@ "search_view.section.title.recently_searched" = "最近搜索"; "search_view.section.title.recently_seen" = "最近看过"; "search_view.section.title.quick_search" = "快速搜索"; -// Searchable prompt +// Searchable "searchable.prompt.filter" = "筛选"; +"searchable.title.matches_count" = "找到 %d 项结果"; // MARK: QuickSearchView "quick_search_view.title.quick_search" = "快速搜索"; diff --git a/EhPanda/App/zh-Hant-HK.lproj/Localizable.strings b/EhPanda/App/zh-Hant-HK.lproj/Localizable.strings index 1584449a..13fecb7d 100644 --- a/EhPanda/App/zh-Hant-HK.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hant-HK.lproj/Localizable.strings @@ -139,8 +139,9 @@ "search_view.section.title.recently_searched" = "Recently searched"; "search_view.section.title.recently_seen" = "Recently seen"; "search_view.section.title.quick_search" = "快速搜尋"; -// Searchable prompt +// Searchable "searchable.prompt.filter" = "篩選"; +"searchable.title.matches_count" = "找到 %d 項結果"; // MARK: QuickSearchView "quick_search_view.title.quick_search" = "快速搜尋"; diff --git a/EhPanda/App/zh-Hant-TW.lproj/Localizable.strings b/EhPanda/App/zh-Hant-TW.lproj/Localizable.strings index 718f883a..722e382e 100644 --- a/EhPanda/App/zh-Hant-TW.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hant-TW.lproj/Localizable.strings @@ -139,8 +139,9 @@ "search_view.section.title.recently_searched" = "最近搜尋"; "search_view.section.title.recently_seen" = "最近閱讀"; "search_view.section.title.quick_search" = "快速搜尋"; -// Searchable prompt +// Searchable "searchable.prompt.filter" = "過濾"; +"searchable.title.matches_count" = "找到 %d 項結果"; // MARK: QuickSearchView "quick_search_view.title.quick_search" = "快速搜尋"; diff --git a/EhPanda/App/zh-Hant.lproj/Localizable.strings b/EhPanda/App/zh-Hant.lproj/Localizable.strings index ee429694..e317ade9 100644 --- a/EhPanda/App/zh-Hant.lproj/Localizable.strings +++ b/EhPanda/App/zh-Hant.lproj/Localizable.strings @@ -139,8 +139,9 @@ "search_view.section.title.recently_searched" = "最近搜尋"; "search_view.section.title.recently_seen" = "最近閱讀"; "search_view.section.title.quick_search" = "快速搜尋"; -// Searchable prompt +// Searchable "searchable.prompt.filter" = "過濾"; +"searchable.title.matches_count" = "找到 %d 項結果"; // MARK: QuickSearchView "quick_search_view.title.quick_search" = "快速搜尋"; diff --git a/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift b/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift index 8f535b6c..f8567c73 100644 --- a/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift +++ b/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift @@ -72,7 +72,8 @@ struct DetailSearchView: View { FiltersView(store: store.scope(state: \.filtersState, action: DetailSearchReducer.Action.filters)) .accentColor(setting.accentColor).autoBlur(radius: blurRadius) } - .searchable(text: viewStore.binding(\.$keyword)) { + .searchable(text: viewStore.binding(\.$keyword)) + .searchSuggestions { TagSuggestionView( keyword: viewStore.binding(\.$keyword), translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion diff --git a/EhPanda/View/Favorites/FavoritesView.swift b/EhPanda/View/Favorites/FavoritesView.swift index 0887c65a..fea27eea 100644 --- a/EhPanda/View/Favorites/FavoritesView.swift +++ b/EhPanda/View/Favorites/FavoritesView.swift @@ -79,7 +79,8 @@ struct FavoritesView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .searchable(text: viewStore.binding(\.$keyword)) { + .searchable(text: viewStore.binding(\.$keyword)) + .searchSuggestions { TagSuggestionView( keyword: viewStore.binding(\.$keyword), translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion diff --git a/EhPanda/View/Home/Watched/WatchedView.swift b/EhPanda/View/Home/Watched/WatchedView.swift index 416f95d2..1612608b 100644 --- a/EhPanda/View/Home/Watched/WatchedView.swift +++ b/EhPanda/View/Home/Watched/WatchedView.swift @@ -76,7 +76,8 @@ struct WatchedView: View { FiltersView(store: store.scope(state: \.filtersState, action: WatchedReducer.Action.filters)) .autoBlur(radius: blurRadius).environment(\.inSheet, true) } - .searchable(text: viewStore.binding(\.$keyword)) { + .searchable(text: viewStore.binding(\.$keyword)) + .searchSuggestions { TagSuggestionView( keyword: viewStore.binding(\.$keyword), translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion diff --git a/EhPanda/View/Search/SearchRootView.swift b/EhPanda/View/Search/SearchRootView.swift index 095a6041..52e0a7f0 100644 --- a/EhPanda/View/Search/SearchRootView.swift +++ b/EhPanda/View/Search/SearchRootView.swift @@ -28,10 +28,6 @@ struct SearchRootView: View { self.tagTranslator = tagTranslator } - private var searchFieldPlacement: SearchFieldPlacement { - DeviceUtil.isPad ? .toolbar : .navigationBarDrawer(displayMode: .always) - } - var body: some View { NavigationView { ScrollView(showsIndicators: false) { @@ -79,7 +75,8 @@ struct SearchRootView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .searchable(text: viewStore.binding(\.$keyword), placement: searchFieldPlacement) { + .searchable(text: viewStore.binding(\.$keyword)) + .searchSuggestions { TagSuggestionView( keyword: viewStore.binding(\.$keyword), translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion diff --git a/EhPanda/View/Search/SearchView.swift b/EhPanda/View/Search/SearchView.swift index eeee29b8..9a637652 100644 --- a/EhPanda/View/Search/SearchView.swift +++ b/EhPanda/View/Search/SearchView.swift @@ -72,7 +72,8 @@ struct SearchView: View { FiltersView(store: store.scope(state: \.filtersState, action: SearchReducer.Action.filters)) .accentColor(setting.accentColor).autoBlur(radius: blurRadius) } - .searchable(text: viewStore.binding(\.$keyword)) { + .searchable(text: viewStore.binding(\.$keyword)) + .searchSuggestions { TagSuggestionView( keyword: viewStore.binding(\.$keyword), translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion diff --git a/EhPanda/View/Support/Components/TagSuggestionView.swift b/EhPanda/View/Support/Components/TagSuggestionView.swift index e2db0b59..df103b58 100644 --- a/EhPanda/View/Support/Components/TagSuggestionView.swift +++ b/EhPanda/View/Support/Components/TagSuggestionView.swift @@ -25,63 +25,22 @@ struct TagSuggestionView: View { var body: some View { if isEnabled { - DoubleHorizontalSuggestionsStack( - suggestions: translationHandler.suggestions, showsImages: showsImages, - action: { translationHandler.autoComplete(suggestion: $0, keyword: &keyword) } - ) - .onChange(of: keyword) { _ in translationHandler.analyze(text: &keyword, translations: translations) } - } - } -} - -// MARK: DoubleHorizontalSuggestionsStack -private struct DoubleHorizontalSuggestionsStack: View { - private let suggestions: [TagSuggestion] - private let showsImages: Bool - private let action: (TagSuggestion) -> Void - - init(suggestions: [TagSuggestion], showsImages: Bool, action: @escaping (TagSuggestion) -> Void) { - self.suggestions = suggestions - self.showsImages = showsImages - self.action = action - } - - var singleSuggestions: [TagSuggestion] { - .init(suggestions.prefix(min(suggestions.count, 10))) - } - var doubleSuggestions: [(TagSuggestion, TagSuggestion?)] { - suggestions.enumerated().compactMap { (index, suggestion) in - if index < 20, index % 2 == 0 { - if index + 1 < suggestions.count { - return (suggestion, suggestions[index + 1]) - } else { - return (suggestion, nil) - } - } else { - return nil + if DeviceUtil.isPhone { + Text(L10n.Localizable.Searchable.Title.matchesCount(translationHandler.suggestions.count)) + .foregroundColor(.secondary) + .font(.subheadline) } - } - } - var body: some View { - if !DeviceUtil.isPad { - ForEach(singleSuggestions) { suggestion in - SuggestionCell(suggestion: suggestion, showsImages: showsImages) { - action(suggestion) - } + let suggestions = translationHandler.suggestions + ForEach(suggestions.prefix(min(suggestions.count, 10))) { suggestion in + SuggestionCell( + suggestion: suggestion, + showsImages: showsImages, + action: { translationHandler.autoComplete(suggestion: suggestion, keyword: &keyword) } + ) } - } else { - ForEach(doubleSuggestions, id: \.0) { leadingSuggestion, trailingSuggestion in - HStack(spacing: 30) { - SuggestionCell(suggestion: leadingSuggestion, showsImages: showsImages) { - action(leadingSuggestion) - } - if let trailingSuggestion = trailingSuggestion { - SuggestionCell(suggestion: trailingSuggestion, showsImages: showsImages) { - action(trailingSuggestion) - } - } - } + .onChange(of: keyword) { _ in + translationHandler.analyze(text: &keyword, translations: translations) } } } @@ -105,24 +64,42 @@ private struct SuggestionCell: View { } var body: some View { - HStack(spacing: 20) { - Image(systemSymbol: .magnifyingglass) - VStack(alignment: .leading) { - HStack(spacing: 2) { - Text(displayValue.localizedKey) - if let imageURL = suggestion.tag.valueImageURL, showsImages { - Image(systemSymbol: .photo).opacity(0) - .overlay(KFImage(imageURL).resizable().scaledToFit()) + if DeviceUtil.isPhone { + HStack(spacing: 20) { + Image(systemSymbol: .magnifyingglass) + + VStack(alignment: .leading) { + HStack(spacing: 2) { + Text(displayValue.localizedKey) + + if let imageURL = suggestion.tag.valueImageURL, showsImages { + Image(systemSymbol: .photo) + .opacity(0) + .overlay( + KFImage(imageURL) + .resizable() + .scaledToFit() + ) + } } + .font(.callout) + .lineLimit(1) + + Text(suggestion.displayKey.localizedKey) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(1) } - .font(.callout).lineLimit(1) - Text(suggestion.displayKey.localizedKey).font(.caption).foregroundColor(.secondary).lineLimit(1) + .allowsHitTesting(false) + + Spacer() } - .allowsHitTesting(false) - Spacer() + .contentShape(Rectangle()) + .onTapGesture(perform: action) + } else { + (Text(displayValue.localizedKey) + Text("\n") + Text(suggestion.displayKey.localizedKey)) + .searchCompletion(suggestion.tag.searchKeyword) } - .contentShape(Rectangle()) - .onTapGesture(perform: action) } } From 0a4a294975fb030829d44558eb48bafc83e416ce Mon Sep 17 00:00:00 2001 From: Chihchy Date: Sat, 27 May 2023 23:49:24 +0800 Subject: [PATCH 29/29] Update CI --- .github/workflows/dependencies.yml | 30 ++++++++++++++++++++++++++++++ .github/workflows/deploy.yml | 6 +++--- .github/workflows/test.yml | 5 +++-- 3 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/dependencies.yml diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml new file mode 100644 index 00000000..751ff046 --- /dev/null +++ b/.github/workflows/dependencies.yml @@ -0,0 +1,30 @@ +name: Xcode Dependencies +on: + schedule: + - cron: '0 0 * * 1' + push: + branches: + - main +permissions: + contents: write + pull-requests: write +jobs: + dependencies: + runs-on: macos-13 + if: ${{ contains(github.event.head_commit.message, '[update dependencies]') || github.event_name == 'schedule' }} + steps: + - uses: actions/checkout@v2 + - name: Resolve Dependencies + id: resolution + uses: GetSidetrack/action-xcodeproj-spm-update@main + with: + forceResolution: true + failWhenOutdated: false + - name: Create Pull Request + if: steps.resolution.outputs.dependenciesChanged == 'true' + uses: peter-evans/create-pull-request@v3 + with: + branch: 'update-dependencies' + delete-branch: true + commit-message: 'Update dependencies [skip test]' + title: 'Updated Dependencies' diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4c4cfbdc..11ad7064 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,8 +5,8 @@ on: - main types: [closed] env: - DEVELOPER_DIR: /Applications/Xcode_14.2.app - APP_VERSION: '2.6.1' + DEVELOPER_DIR: /Applications/Xcode_14.3.app + APP_VERSION: '2.7.0' SCHEME_NAME: 'EhPanda' ALTSTORE_JSON_PATH: './AltStore.json' BUILDS_PATH: '/tmp/action-builds' @@ -19,7 +19,7 @@ env: jobs: Deploy: - runs-on: macos-12 + runs-on: macos-13 if: github.event.pull_request.merged == true && github.event.pull_request.user.login == 'chihchy' steps: - name: Checkout diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 744fa29f..e83cb865 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,10 +2,11 @@ name: Test on: [push] env: SCHEME_NAME: 'EhPanda' - DEVELOPER_DIR: /Applications/Xcode_14.2.app + DEVELOPER_DIR: /Applications/Xcode_14.3.app jobs: Test: - runs-on: macos-12 + runs-on: macos-13 + if: ${{ !contains(github.event.head_commit.message, '[skip test]') }} steps: - name: Checkout uses: actions/checkout@v2