diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index 598c4232..0b38e16a 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -2,16 +2,13 @@ name: Xcode Dependencies on: schedule: - cron: '0 0 * * 1' - push: - branches: - - main + workflow_dispatch: 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' }} + runs-on: macos-14 steps: - uses: actions/checkout@v4 with: @@ -22,9 +19,10 @@ jobs: with: forceResolution: true failWhenOutdated: false + xcodePath: '/Applications/Xcode_15.4.app' - name: Create Pull Request if: steps.resolution.outputs.dependenciesChanged == 'true' - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: branch: 'update-dependencies' delete-branch: true diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1d5f8b25..8a9acc45 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,8 +5,8 @@ on: - main types: [closed] env: - DEVELOPER_DIR: /Applications/Xcode_15.0.1.app - APP_VERSION: '2.7.4' + DEVELOPER_DIR: /Applications/Xcode_15.4.app + APP_VERSION: '2.7.5' SCHEME_NAME: 'EhPanda' ALTSTORE_JSON_PATH: './AltStore.json' BUILDS_PATH: '/tmp/action-builds' @@ -19,7 +19,7 @@ env: jobs: Deploy: - runs-on: macos-13 + runs-on: macos-14 if: github.event.pull_request.merged == true && github.event.pull_request.user.login == 'chihchy' steps: - name: Checkout @@ -28,12 +28,13 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - name: Install dependencies - run: brew install swiftgen - name: Show Xcode version run: xcodebuild -version - name: Run tests - run: xcodebuild clean test -scheme ${{ env.SCHEME_NAME }} -sdk iphonesimulator + run: xcodebuild clean test + -skipMacroValidation + -sdk iphonesimulator + -scheme ${{ env.SCHEME_NAME }} -destination 'platform=iOS Simulator,name=iPhone 15 Pro' - name: Bump version id: bump-version @@ -41,8 +42,17 @@ jobs: with: version: ${{ env.APP_VERSION }} - name: Xcode archive - run: xcodebuild archive -destination 'generic/platform=iOS' - -scheme ${{ env.SCHEME_NAME }} -archivePath ${{ env.ARCHIVE_PATH }} CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY= CODE_SIGN_ENTITLEMENTS= GCC_OPTIMIZATION_LEVEL=s SWIFT_OPTIMIZATION_LEVEL=-O + run: xcodebuild archive + -skipMacroValidation + -scheme ${{ env.SCHEME_NAME }} + -destination 'generic/platform=iOS' + -archivePath ${{ env.ARCHIVE_PATH }} + CODE_SIGN_IDENTITY= + CODE_SIGN_ENTITLEMENTS= + CODE_SIGNING_ALLOWED=NO + CODE_SIGNING_REQUIRED=NO + GCC_OPTIMIZATION_LEVEL=s + SWIFT_OPTIMIZATION_LEVEL=-O - name: Export .ipa file run: | mkdir -p ${{ env.PAYLOAD_PATH }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 080eaa10..56f0cb02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,20 +1,20 @@ name: Test -on: [push] +on: [push, workflow_dispatch] env: SCHEME_NAME: 'EhPanda' - DEVELOPER_DIR: /Applications/Xcode_15.0.1.app + DEVELOPER_DIR: /Applications/Xcode_15.4.app jobs: Test: - runs-on: macos-13 + runs-on: macos-14 if: ${{ !contains(github.event.head_commit.message, '[skip test]') }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Install dependencies - run: brew install swiftgen - name: Show Xcode version run: xcodebuild -version - name: Run tests run: xcodebuild clean test - -scheme ${{ env.SCHEME_NAME }} -sdk iphonesimulator + -skipMacroValidation + -sdk iphonesimulator + -scheme ${{ env.SCHEME_NAME }} -destination 'platform=iOS Simulator,name=iPhone 15 Pro' diff --git a/.gitignore b/.gitignore index 2f7dea2b..ee8327b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .DS_Store -EhPanda/App/Generated EhPanda.xcodeproj/xcuserdata EhPanda.xcodeproj/project.xcworkspace/xcuserdata \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index 22adb80e..65bd698e 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -15,3 +15,4 @@ identifier_name: excluded: - EhPandaTests + - EhPanda/App/Generated diff --git a/EhPanda.xcodeproj/project.pbxproj b/EhPanda.xcodeproj/project.pbxproj index 3cbb43a0..759134c1 100644 --- a/EhPanda.xcodeproj/project.pbxproj +++ b/EhPanda.xcodeproj/project.pbxproj @@ -269,6 +269,14 @@ 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 */; }; + EA0C92452C3EB42300D211F6 /* ISSUE_TEMPLATE in Resources */ = {isa = PBXBuildFile; fileRef = EA0C92432C3EB42300D211F6 /* ISSUE_TEMPLATE */; }; + EA0C92462C3EB42300D211F6 /* workflows in Resources */ = {isa = PBXBuildFile; fileRef = EA0C92442C3EB42300D211F6 /* workflows */; }; + EA0C92592C3EB49500D211F6 /* README.cht.md in Resources */ = {isa = PBXBuildFile; fileRef = EA0C92532C3EB49500D211F6 /* README.cht.md */; }; + EA0C925A2C3EB49500D211F6 /* README.ko.md in Resources */ = {isa = PBXBuildFile; fileRef = EA0C92542C3EB49500D211F6 /* README.ko.md */; }; + EA0C925B2C3EB49500D211F6 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = EA0C92552C3EB49500D211F6 /* README.md */; }; + EA0C925C2C3EB49500D211F6 /* README.de.md in Resources */ = {isa = PBXBuildFile; fileRef = EA0C92562C3EB49500D211F6 /* README.de.md */; }; + EA0C925D2C3EB49500D211F6 /* README.chs.md in Resources */ = {isa = PBXBuildFile; fileRef = EA0C92572C3EB49500D211F6 /* README.chs.md */; }; + EA0C925E2C3EB49500D211F6 /* README.jpn.md in Resources */ = {isa = PBXBuildFile; fileRef = EA0C92582C3EB49500D211F6 /* README.jpn.md */; }; EA2E2E7F2A1F7E500038A261 /* SettingReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2E2E7E2A1F7E500038A261 /* SettingReducer.swift */; }; EA2E2E822A1FA1060038A261 /* SearchReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2E2E812A1FA1050038A261 /* SearchReducer.swift */; }; EAE63E2129E2A6330048C601 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = EAE63E2029E2A6330048C601 /* SwiftyBeaver */; }; @@ -579,6 +587,19 @@ 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 = ""; }; + EA0C92432C3EB42300D211F6 /* ISSUE_TEMPLATE */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ISSUE_TEMPLATE; path = .github/ISSUE_TEMPLATE; sourceTree = ""; }; + EA0C92442C3EB42300D211F6 /* workflows */ = {isa = PBXFileReference; lastKnownFileType = folder; name = workflows; path = .github/workflows; sourceTree = ""; }; + EA0C92482C3EB45E00D211F6 /* AltStore.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = AltStore.json; sourceTree = ""; }; + EA0C92492C3EB45E00D211F6 /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = ""; }; + EA0C924A2C3EB45E00D211F6 /* .gitattributes */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .gitattributes; sourceTree = ""; }; + EA0C924B2C3EB45E00D211F6 /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; + EA0C924C2C3EB45E00D211F6 /* .gitignore */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; + EA0C92532C3EB49500D211F6 /* README.cht.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.cht.md; path = READMEs/README.cht.md; sourceTree = ""; }; + EA0C92542C3EB49500D211F6 /* README.ko.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.ko.md; path = READMEs/README.ko.md; sourceTree = ""; }; + EA0C92552C3EB49500D211F6 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + EA0C92562C3EB49500D211F6 /* README.de.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.de.md; path = READMEs/README.de.md; sourceTree = ""; }; + EA0C92572C3EB49500D211F6 /* README.chs.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.chs.md; path = READMEs/README.chs.md; sourceTree = ""; }; + EA0C92582C3EB49500D211F6 /* README.jpn.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.jpn.md; path = READMEs/README.jpn.md; sourceTree = ""; }; EA2E2E7E2A1F7E500038A261 /* SettingReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingReducer.swift; sourceTree = ""; }; EA2E2E812A1FA1050038A261 /* SearchReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchReducer.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1087,6 +1108,9 @@ ABC3C7562593696C00E0C11B /* EhPanda */, AB5BE67726B95FDD007D4A55 /* ShareExtension */, AB3E9E6126D210B1008FE518 /* EhPandaTests */, + EA0C92472C3EB44300D211F6 /* Config */, + EA0C92422C3EB40100D211F6 /* GitHub */, + EA0C92522C3EB47F00D211F6 /* READMEs */, ABC3C7552593696C00E0C11B /* Products */, AB82819926B1C39B00A80CFA /* Frameworks */, ); @@ -1305,6 +1329,40 @@ path = Setting; sourceTree = ""; }; + EA0C92422C3EB40100D211F6 /* GitHub */ = { + isa = PBXGroup; + children = ( + EA0C92432C3EB42300D211F6 /* ISSUE_TEMPLATE */, + EA0C92442C3EB42300D211F6 /* workflows */, + ); + name = GitHub; + sourceTree = ""; + }; + EA0C92472C3EB44300D211F6 /* Config */ = { + isa = PBXGroup; + children = ( + EA0C924A2C3EB45E00D211F6 /* .gitattributes */, + EA0C924C2C3EB45E00D211F6 /* .gitignore */, + EA0C924B2C3EB45E00D211F6 /* .swiftlint.yml */, + EA0C92482C3EB45E00D211F6 /* AltStore.json */, + EA0C92492C3EB45E00D211F6 /* swiftgen.yml */, + ); + name = Config; + sourceTree = ""; + }; + EA0C92522C3EB47F00D211F6 /* READMEs */ = { + isa = PBXGroup; + children = ( + EA0C92572C3EB49500D211F6 /* README.chs.md */, + EA0C92532C3EB49500D211F6 /* README.cht.md */, + EA0C92562C3EB49500D211F6 /* README.de.md */, + EA0C92582C3EB49500D211F6 /* README.jpn.md */, + EA0C92542C3EB49500D211F6 /* README.ko.md */, + EA0C92552C3EB49500D211F6 /* README.md */, + ); + name = READMEs; + sourceTree = ""; + }; EA2B9B042A0A89C900E7BA07 /* AccountSetting */ = { isa = PBXGroup; children = ( @@ -1630,16 +1688,21 @@ AB90276E291F548700697256 /* AppIcon_NotMyPresident_iPad.png in Resources */, AB90276D291F548700697256 /* AppIcon_NotMyPresident_iPad_Pro@2x.png in Resources */, AB0CFB7A27BAB9D0004BD372 /* AppIcon_Default@2x.png in Resources */, + EA0C92592C3EB49500D211F6 /* README.cht.md in Resources */, AB0CFB7527BAB9D0004BD372 /* AppIcon_Default_iPad@2x.png in Resources */, AB0CFB9227BBD323004BD372 /* AppIcon_Ukiyoe_iPad@2x.png in Resources */, ABC3C7852593699B00E0C11B /* Assets.xcassets in Resources */, AB0CFB7B27BAB9D0004BD372 /* AppIcon_Default_iPad.png in Resources */, AB90276C291F548700697256 /* AppIcon_NotMyPresident_iPad@2x.png in Resources */, + EA0C925D2C3EB49500D211F6 /* README.chs.md in Resources */, AB0CFB9527BBD323004BD372 /* AppIcon_Ukiyoe_iPad.png in Resources */, + EA0C925C2C3EB49500D211F6 /* README.de.md in Resources */, ABE9012427F722D100F3651D /* AppIcon_StandWithUkraine2022@3x.png in Resources */, AB90276B291F548700697256 /* AppIcon_NotMyPresident@3x.png in Resources */, + EA0C92452C3EB42300D211F6 /* ISSUE_TEMPLATE in Resources */, AB0CFB8827BBD2D7004BD372 /* AppIcon_Developer_iPad@2x.png in Resources */, ABE9012227F722D100F3651D /* AppIcon_StandWithUkraine2022@2x.png in Resources */, + EA0C925B2C3EB49500D211F6 /* README.md in Resources */, AB0CFB7427BAB9D0004BD372 /* AppIcon_Default@3x.png in Resources */, AB7E6B3025D24FE00035CC68 /* InfoPlist.strings in Resources */, ABA9A6BC28EC786100EE28DE /* swiftgen.yml in Resources */, @@ -1647,11 +1710,14 @@ AB0CFB8927BBD2D7004BD372 /* AppIcon_Developer@2x.png in Resources */, ABEE0AFA2595C6F800C997AE /* Localizable.strings in Resources */, AB90276F291F548700697256 /* AppIcon_NotMyPresident@2x.png in Resources */, + EA0C92462C3EB42300D211F6 /* workflows in Resources */, + EA0C925A2C3EB49500D211F6 /* README.ko.md in Resources */, ABD5FDD4263D05110021A4C6 /* .swiftlint.yml in Resources */, ABE9012527F722D100F3651D /* AppIcon_StandWithUkraine2022_iPad_Pro@2x.png in Resources */, ABE9012327F722D100F3651D /* AppIcon_StandWithUkraine2022_iPad.png in Resources */, AB0CFB7827BAB9D0004BD372 /* AppIcon_Default_iPad_Pro@2x.png in Resources */, AB0CFB8B27BBD2D7004BD372 /* AppIcon_Developer_iPad_Pro@2x.png in Resources */, + EA0C925E2C3EB49500D211F6 /* README.jpn.md in Resources */, AB0CFB8A27BBD2D7004BD372 /* AppIcon_Developer_iPad.png in Resources */, ABE9012627F722D100F3651D /* AppIcon_StandWithUkraine2022_iPad@2x.png in Resources */, AB0CFB9427BBD323004BD372 /* AppIcon_Ukiyoe_iPad_Pro@2x.png in Resources */, @@ -2408,8 +2474,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.0.0; + kind = exactVersion; + version = 1.0.0; }; }; ABAC82FC26BC4866009F5026 /* XCRemoteSwiftPackageReference "SwiftyOpenCC" */ = { @@ -2424,8 +2490,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pointfreeco/swiftui-navigation.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.0.0; + kind = exactVersion; + version = 1.0.0; }; }; ABC4A0772751B40E00968A4F /* XCRemoteSwiftPackageReference "Kingfisher" */ = { diff --git a/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index eec6aaa7..e8458bdf 100644 --- a/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/EhPanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "93e2ed5e29c203f999b061bca8bfe01e97d8d679589199b88cec774e80de86b8", "pins" : [ { "identity" : "alertkit", @@ -23,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "ec62f32d21584214a4b27c8cee2b2ad70ab2c38a", - "version" : "0.11.0" + "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version" : "1.0.0" } }, { @@ -50,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tid-kijyun/Kanna.git", "state" : { - "revision" : "f9e4922223dd0d3dfbf02ca70812cf5531fc0593", - "version" : "5.2.7" + "revision" : "41c3d28ea0eac07e4551b28def9de1ede702e739", + "version" : "5.3.0" } }, { @@ -59,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Kingfisher.git", "state" : { - "revision" : "3ec0ab0bca4feb56e8b33e289c9496e89059dd08", - "version" : "7.10.2" + "revision" : "5b92f029fab2cce44386d28588098b5be0824ef5", + "version" : "7.11.0" } }, { @@ -77,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984", - "version" : "0.14.1" + "revision" : "e593aba2c6222daad7c4f2732a431eed2c09bb07", + "version" : "1.3.0" } }, { @@ -86,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "0fbaebfc013715dab44d715a4d350ba37f297e4d", - "version" : "0.4.0" + "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", + "version" : "1.0.2" } }, { @@ -95,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", - "version" : "1.0.6" + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" } }, { @@ -104,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", "state" : { - "revision" : "9f4202ab5b8422aa90f0ed983bf7652c3af7abf0", - "version" : "0.59.0" + "revision" : "195284b94b799b326729640453f547f08892293a", + "version" : "1.0.0" } }, { @@ -113,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "479750bd98fac2e813fffcf2af0728b5b0085795", - "version" : "0.1.1" + "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version" : "1.1.0" } }, { @@ -122,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "4a87bb75be70c983a9548597e8783236feb3401e", - "version" : "0.11.1" + "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", + "version" : "1.3.0" } }, { @@ -131,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "16fd42ae04c6e7f74a6a86395d04722c641cccee", - "version" : "0.6.0" + "revision" : "d3a5af3038a09add4d7682f66555d6212058a3c0", + "version" : "1.2.2" } }, { @@ -140,8 +141,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-identified-collections", "state" : { - "revision" : "d01446a78fb768adc9a78cbb6df07767c8ccfc29", - "version" : "0.8.0" + "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax", + "state" : { + "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", + "version" : "510.0.1" } }, { @@ -158,8 +168,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swiftui-navigation.git", "state" : { - "revision" : "2aa885e719087ee19df251c08a5980ad3e787f12", - "version" : "0.8.0" + "revision" : "f5bcdac5b6bb3f826916b14705f37a3937c2fd34", + "version" : "1.0.0" } }, { @@ -221,10 +231,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865", - "version" : "0.9.0" + "revision" : "b13b1d1a8e787a5ffc71ac19dcaf52183ab27ba2", + "version" : "1.1.1" } } ], - "version" : 2 + "version" : 3 } diff --git a/EhPanda/App/Generated/Strings.swift b/EhPanda/App/Generated/Strings.swift new file mode 100644 index 00000000..ea82faca --- /dev/null +++ b/EhPanda/App/Generated/Strings.swift @@ -0,0 +1,2225 @@ +// swiftlint:disable all +// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen + +import Foundation + +// swiftlint:disable superfluous_disable_command file_length implicit_return prefer_self_in_static_references + +// MARK: - Strings + +// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces +internal enum L10n { + internal enum Constant { + internal enum App { + /// Copyright © 2023 EhPanda Team + internal static let copyright = L10n.tr("Constant", "app.copyright", fallback: "Copyright © 2023 EhPanda Team") + internal enum Acknowledgement { + internal enum Link { + /// https://github.com/rebeloper/AlertKit + internal static let alertKit = L10n.tr("Constant", "app.acknowledgement.link.alertKit", fallback: "https://github.com/rebeloper/AlertKit") + /// https://github.com/Co2333/Colorful + internal static let colorful = L10n.tr("Constant", "app.acknowledgement.link.colorful", fallback: "https://github.com/Co2333/Colorful") + /// https://github.com/EhTagTranslation/Database + internal static let ehTagTranslationDatabase = L10n.tr("Constant", "app.acknowledgement.link.ehTagTranslationDatabase", fallback: "https://github.com/EhTagTranslation/Database") + /// https://github.com/markrenaud/FilePicker + internal static let filePicker = L10n.tr("Constant", "app.acknowledgement.link.filePicker", fallback: "https://github.com/markrenaud/FilePicker") + /// https://github.com/tid-kijyun/Kanna + internal static let kanna = L10n.tr("Constant", "app.acknowledgement.link.kanna", fallback: "https://github.com/tid-kijyun/Kanna") + /// https://github.com/onevcat/Kingfisher + internal static let kingfisher = L10n.tr("Constant", "app.acknowledgement.link.kingfisher", fallback: "https://github.com/onevcat/Kingfisher") + /// https://github.com/SFSafeSymbols/SFSafeSymbols + internal static let sfSafeSymbols = L10n.tr("Constant", "app.acknowledgement.link.sfSafeSymbols", fallback: "https://github.com/SFSafeSymbols/SFSafeSymbols") + /// https://github.com/gonzalezreal/SwiftCommonMark + internal static let swiftCommonMark = L10n.tr("Constant", "app.acknowledgement.link.swiftCommonMark", fallback: "https://github.com/gonzalezreal/SwiftCommonMark") + /// https://github.com/SwiftGen/SwiftGen + internal static let swiftGen = L10n.tr("Constant", "app.acknowledgement.link.swiftGen", fallback: "https://github.com/SwiftGen/SwiftGen") + /// https://github.com/pointfreeco/swiftui-navigation + internal static let swiftUINavigation = L10n.tr("Constant", "app.acknowledgement.link.swiftUINavigation", fallback: "https://github.com/pointfreeco/swiftui-navigation") + /// https://github.com/fermoya/SwiftUIPager + internal static let swiftUIPager = L10n.tr("Constant", "app.acknowledgement.link.swiftUIPager", fallback: "https://github.com/fermoya/SwiftUIPager") + /// https://github.com/SwiftyBeaver/SwiftyBeaver + internal static let swiftyBeaver = L10n.tr("Constant", "app.acknowledgement.link.swiftyBeaver", fallback: "https://github.com/SwiftyBeaver/SwiftyBeaver") + /// https://github.com/ddddxxx/SwiftyOpenCC + internal static let swiftyOpenCC = L10n.tr("Constant", "app.acknowledgement.link.swiftyOpenCC", fallback: "https://github.com/ddddxxx/SwiftyOpenCC") + /// https://github.com/pointfreeco/swift-composable-architecture + internal static let tca = L10n.tr("Constant", "app.acknowledgement.link.tca", fallback: "https://github.com/pointfreeco/swift-composable-architecture") + /// https://github.com/honkmaster/TTProgressHUD + internal static let ttProgressHUD = L10n.tr("Constant", "app.acknowledgement.link.ttProgressHUD", fallback: "https://github.com/honkmaster/TTProgressHUD") + /// https://github.com/jathu/UIImageColors + internal static let uiImageColors = L10n.tr("Constant", "app.acknowledgement.link.uiImageColors", fallback: "https://github.com/jathu/UIImageColors") + /// https://github.com/paololeonardi/WaterfallGrid + internal static let waterfallGrid = L10n.tr("Constant", "app.acknowledgement.link.waterfallGrid", fallback: "https://github.com/paololeonardi/WaterfallGrid") + } + internal enum Text { + /// AlertKit + internal static let alertKit = L10n.tr("Constant", "app.acknowledgement.text.alertKit", fallback: "AlertKit") + /// Colorful + internal static let colorful = L10n.tr("Constant", "app.acknowledgement.text.colorful", fallback: "Colorful") + /// EhTagTranslation/Database + internal static let ehTagTranslationDatabase = L10n.tr("Constant", "app.acknowledgement.text.ehTagTranslationDatabase", fallback: "EhTagTranslation/Database") + /// FilePicker + internal static let filePicker = L10n.tr("Constant", "app.acknowledgement.text.filePicker", fallback: "FilePicker") + /// Kanna + internal static let kanna = L10n.tr("Constant", "app.acknowledgement.text.kanna", fallback: "Kanna") + /// Kingfisher + internal static let kingfisher = L10n.tr("Constant", "app.acknowledgement.text.kingfisher", fallback: "Kingfisher") + /// SFSafeSymbols + internal static let sfSafeSymbols = L10n.tr("Constant", "app.acknowledgement.text.sfSafeSymbols", fallback: "SFSafeSymbols") + /// SwiftCommonMark + internal static let swiftCommonMark = L10n.tr("Constant", "app.acknowledgement.text.swiftCommonMark", fallback: "SwiftCommonMark") + /// SwiftGen + internal static let swiftGen = L10n.tr("Constant", "app.acknowledgement.text.swiftGen", fallback: "SwiftGen") + /// SwiftUI Navigation + internal static let swiftUINavigation = L10n.tr("Constant", "app.acknowledgement.text.swiftUINavigation", fallback: "SwiftUI Navigation") + /// SwiftUIPager + internal static let swiftUIPager = L10n.tr("Constant", "app.acknowledgement.text.swiftUIPager", fallback: "SwiftUIPager") + /// SwiftyBeaver + internal static let swiftyBeaver = L10n.tr("Constant", "app.acknowledgement.text.swiftyBeaver", fallback: "SwiftyBeaver") + /// SwiftyOpenCC + internal static let swiftyOpenCC = L10n.tr("Constant", "app.acknowledgement.text.swiftyOpenCC", fallback: "SwiftyOpenCC") + /// The Composable Architecture + internal static let tca = L10n.tr("Constant", "app.acknowledgement.text.tca", fallback: "The Composable Architecture") + /// TTProgressHUD + internal static let ttProgressHUD = L10n.tr("Constant", "app.acknowledgement.text.ttProgressHUD", fallback: "TTProgressHUD") + /// UIImageColors + internal static let uiImageColors = L10n.tr("Constant", "app.acknowledgement.text.uiImageColors", fallback: "UIImageColors") + /// WaterfallGrid + internal static let waterfallGrid = L10n.tr("Constant", "app.acknowledgement.text.waterfallGrid", fallback: "WaterfallGrid") + } + } + internal enum CodeLevelContributor { + internal enum Link { + /// https://github.com/chihchy + internal static let chihchy = L10n.tr("Constant", "app.code_level_contributor.link.chihchy", fallback: "https://github.com/chihchy") + /// https://github.com/Jimmy-Prime + internal static let jimmyPrime = L10n.tr("Constant", "app.code_level_contributor.link.Jimmy-Prime", fallback: "https://github.com/Jimmy-Prime") + /// https://github.com/remlostime + internal static let remlostime = L10n.tr("Constant", "app.code_level_contributor.link.remlostime", fallback: "https://github.com/remlostime") + /// https://github.com/tatsuz0u + internal static let tatsuz0u = L10n.tr("Constant", "app.code_level_contributor.link.tatsuz0u", fallback: "https://github.com/tatsuz0u") + /// https://github.com/xioxin + internal static let xioxin = L10n.tr("Constant", "app.code_level_contributor.link.xioxin", fallback: "https://github.com/xioxin") + } + internal enum Text { + /// Chihchy + internal static let chihchy = L10n.tr("Constant", "app.code_level_contributor.text.chihchy", fallback: "Chihchy") + /// Jimmy Prime + internal static let jimmyPrime = L10n.tr("Constant", "app.code_level_contributor.text.Jimmy-Prime", fallback: "Jimmy Prime") + /// Kai Chen + internal static let remlostime = L10n.tr("Constant", "app.code_level_contributor.text.remlostime", fallback: "Kai Chen") + /// Tatsuzo Araki + internal static let tatsuz0u = L10n.tr("Constant", "app.code_level_contributor.text.tatsuz0u", fallback: "Tatsuzo Araki") + /// xioxin + internal static let xioxin = L10n.tr("Constant", "app.code_level_contributor.text.xioxin", fallback: "xioxin") + } + } + internal enum Contact { + internal enum Link { + /// altstore://source?url=https://github.com/EhPanda-Team/EhPanda/raw/main/AltStore.json + internal static let altStore = L10n.tr("Constant", "app.contact.link.altStore", fallback: "altstore://source?url=https://github.com/EhPanda-Team/EhPanda/raw/main/AltStore.json") + /// https://discord.gg/BSBE9FCBTq + internal static let discord = L10n.tr("Constant", "app.contact.link.discord", fallback: "https://discord.gg/BSBE9FCBTq") + /// https://github.com/EhPanda-Team/EhPanda + internal static let gitHub = L10n.tr("Constant", "app.contact.link.gitHub", fallback: "https://github.com/EhPanda-Team/EhPanda") + /// https://t.me/ehpanda + internal static let telegram = L10n.tr("Constant", "app.contact.link.telegram", fallback: "https://t.me/ehpanda") + /// https://ehpanda.app + internal static let website = L10n.tr("Constant", "app.contact.link.website", fallback: "https://ehpanda.app") + } + internal enum Text { + /// Discord + internal static let discord = L10n.tr("Constant", "app.contact.text.discord", fallback: "Discord") + /// GitHub + internal static let gitHub = L10n.tr("Constant", "app.contact.text.gitHub", fallback: "GitHub") + /// Telegram + internal static let telegram = L10n.tr("Constant", "app.contact.text.telegram", fallback: "Telegram") + } + } + internal enum SpecialThanks { + internal enum Link { + /// https://github.com/caxerx + internal static let caxerx = L10n.tr("Constant", "app.special_thanks.link.caxerx", fallback: "https://github.com/caxerx") + /// https://github.com/honjow + internal static let honjow = L10n.tr("Constant", "app.special_thanks.link.honjow", fallback: "https://github.com/honjow") + /// + internal static let luminescentYq = L10n.tr("Constant", "app.special_thanks.link.luminescent_yq", fallback: "") + /// https://github.com/taylorlannister + internal static let taylorlannister = L10n.tr("Constant", "app.special_thanks.link.taylorlannister", fallback: "https://github.com/taylorlannister") + } + internal enum Text { + /// caxerx + internal static let caxerx = L10n.tr("Constant", "app.special_thanks.text.caxerx", fallback: "caxerx") + /// honjow + internal static let honjow = L10n.tr("Constant", "app.special_thanks.text.honjow", fallback: "honjow") + /// Luminescent_yq + internal static let luminescentYq = L10n.tr("Constant", "app.special_thanks.text.luminescent_yq", fallback: "Luminescent_yq") + /// taylorlannister + internal static let taylorlannister = L10n.tr("Constant", "app.special_thanks.text.taylorlannister", fallback: "taylorlannister") + } + } + internal enum TranslationContributor { + internal enum Link { + /// https://github.com/caxerx + internal static let caxerx = L10n.tr("Constant", "app.translation_contributor.link.caxerx", fallback: "https://github.com/caxerx") + /// https://github.com/Nebulosa-Cat + internal static let nebulosaCat = L10n.tr("Constant", "app.translation_contributor.link.nebulosa-cat", fallback: "https://github.com/Nebulosa-Cat") + /// https://github.com/nyaanim + internal static let nyaanim = L10n.tr("Constant", "app.translation_contributor.link.nyaanim", fallback: "https://github.com/nyaanim") + /// https://github.com/PaulHaeussler + internal static let paulHaeussler = L10n.tr("Constant", "app.translation_contributor.link.paulHaeussler", fallback: "https://github.com/PaulHaeussler") + /// https://github.com/tatsuz0u + internal static let tatsuz0u = L10n.tr("Constant", "app.translation_contributor.link.tatsuz0u", fallback: "https://github.com/tatsuz0u") + } + internal enum Text { + /// caxerx + internal static let caxerx = L10n.tr("Constant", "app.translation_contributor.text.caxerx", fallback: "caxerx") + /// 雲豹 ΦωΦ + internal static let nebulosaCat = L10n.tr("Constant", "app.translation_contributor.text.nebulosa-cat", fallback: "雲豹 ΦωΦ") + /// nyaanim + internal static let nyaanim = L10n.tr("Constant", "app.translation_contributor.text.nyaanim", fallback: "nyaanim") + /// PaulHaeussler + internal static let paulHaeussler = L10n.tr("Constant", "app.translation_contributor.text.paulHaeussler", fallback: "PaulHaeussler") + /// Tatsuzo Araki + internal static let tatsuz0u = L10n.tr("Constant", "app.translation_contributor.text.tatsuz0u", fallback: "Tatsuzo Araki") + } + } + } + internal enum Website { + internal enum Response { + /// This gallery has been removed or is unavailable. + internal static let galleryUnavailable = L10n.tr("Constant", "website.response.gallery_unavailable", fallback: "This gallery has been removed or is unavailable.") + /// Constant.strings + /// EhPanda + /// + /// Created by 荒木辰造 on R 4/02/04. + internal static let hathClientNotFound = L10n.tr("Constant", "website.response.hath_client_not_found", fallback: "You must have a H@H client assigned to your account to use this feature.") + /// Your H@H client appears to be offline. Turn it on, then try again. + internal static let hathClientNotOnline = L10n.tr("Constant", "website.response.hath_client_not_online", fallback: "Your H@H client appears to be offline. Turn it on, then try again.") + /// The requested gallery cannot be downloaded with the selected resolution. + internal static let invalidResolution = L10n.tr("Constant", "website.response.invalid_resolution", fallback: "The requested gallery cannot be downloaded with the selected resolution.") + } + } + } + internal enum InfoPlist { + /// InfoPlist.strings + /// EhPanda + /// + /// Created by 荒木辰造 on R 3/02/09. + internal static let nsFaceIDUsageDescription = L10n.tr("InfoPlist", "NSFaceIDUsageDescription", fallback: "We need this permission to provide Face ID option while unlocking the App.") + /// We need this permission to save images to your photo library. + internal static let nsPhotoLibraryAddUsageDescription = L10n.tr("InfoPlist", "NSPhotoLibraryAddUsageDescription", fallback: "We need this permission to save images to your photo library.") + } + internal enum Localizable { + internal enum AboutView { + internal enum Button { + /// AltStore source + internal static let altStoreSource = L10n.tr("Localizable", "about_view.button.altStore_source", fallback: "AltStore source") + /// Website + internal static let website = L10n.tr("Localizable", "about_view.button.website", fallback: "Website") + } + internal enum Section { + internal enum Title { + /// Acknowledgements + internal static let acknowledgements = L10n.tr("Localizable", "about_view.section.title.acknowledgements", fallback: "Acknowledgements") + /// Code-level contributors + internal static let codeLevelContributors = L10n.tr("Localizable", "about_view.section.title.code_level_contributors", fallback: "Code-level contributors") + /// Special thanks + internal static let specialThanks = L10n.tr("Localizable", "about_view.section.title.special_thanks", fallback: "Special thanks") + /// Translation contributors + internal static let translationContributors = L10n.tr("Localizable", "about_view.section.title.translation_contributors", fallback: "Translation contributors") + } + } + internal enum Title { + /// EhPanda + internal static let ehPanda = L10n.tr("Localizable", "about_view.title.ehPanda", fallback: "EhPanda") + /// Version + internal static let version = L10n.tr("Localizable", "about_view.title.version", fallback: "Version") + } + } + internal enum AccountSettingView { + internal enum Button { + /// Account configuration + internal static let accountConfiguration = L10n.tr("Localizable", "account_setting_view.button.account_configuration", fallback: "Account configuration") + /// Copy cookies + internal static let copyCookies = L10n.tr("Localizable", "account_setting_view.button.copy_cookies", fallback: "Copy cookies") + /// Login + internal static let login = L10n.tr("Localizable", "account_setting_view.button.login", fallback: "Login") + /// Logout + internal static let logout = L10n.tr("Localizable", "account_setting_view.button.logout", fallback: "Logout") + /// Manage tags subscription + internal static let tagsManagement = L10n.tr("Localizable", "account_setting_view.button.tags_management", fallback: "Manage tags subscription") + } + internal enum Title { + /// Account + internal static let account = L10n.tr("Localizable", "account_setting_view.title.account", fallback: "Account") + /// Shows new dawn greeting + internal static let showsNewDawnGreeting = L10n.tr("Localizable", "account_setting_view.title.shows_new_dawn_greeting", fallback: "Shows new dawn greeting") + } + } + internal enum AppIconView { + internal enum Title { + /// App icon + internal static let appIcon = L10n.tr("Localizable", "app_icon_view.title.app_icon", fallback: "App icon") + } + } + internal enum AppearanceSettingView { + internal enum Button { + /// App icon + internal static let appIcon = L10n.tr("Localizable", "appearance_setting_view.button.app_icon", fallback: "App icon") + } + internal enum Menu { + internal enum Title { + /// Infite + internal static let infite = L10n.tr("Localizable", "appearance_setting_view.menu.title.infite", fallback: "Infite") + } + } + internal enum Section { + internal enum Title { + /// Gallery + internal static let gallery = L10n.tr("Localizable", "appearance_setting_view.section.title.gallery", fallback: "Gallery") + /// List + internal static let list = L10n.tr("Localizable", "appearance_setting_view.section.title.list", fallback: "List") + } + } + internal enum Title { + /// Appearance + internal static let appearance = L10n.tr("Localizable", "appearance_setting_view.title.appearance", fallback: "Appearance") + /// Display mode + internal static let displayMode = L10n.tr("Localizable", "appearance_setting_view.title.display_mode", fallback: "Display mode") + /// Displays Japanese title + internal static let displaysJapaneseTitle = L10n.tr("Localizable", "appearance_setting_view.title.displays_japanese_title", fallback: "Displays Japanese title") + /// Maximum number of tags + internal static let maximumNumberOfTags = L10n.tr("Localizable", "appearance_setting_view.title.maximum_number_of_tags", fallback: "Maximum number of tags") + /// Shows tags in list + internal static let showsTagsInList = L10n.tr("Localizable", "appearance_setting_view.title.shows_tags_in_list", fallback: "Shows tags in list") + /// Theme + internal static let theme = L10n.tr("Localizable", "appearance_setting_view.title.theme", fallback: "Theme") + /// Tint color + internal static let tintColor = L10n.tr("Localizable", "appearance_setting_view.title.tint_color", fallback: "Tint color") + } + } + internal enum ArchivesView { + internal enum Button { + /// Download To H@H Client + internal static let downloadToHathClient = L10n.tr("Localizable", "archives_view.button.download_to_hath_client", fallback: "Download To H@H Client") + } + internal enum Title { + /// Archives + internal static let archives = L10n.tr("Localizable", "archives_view.title.archives", fallback: "Archives") + } + } + internal enum CommentsView { + internal enum Title { + /// Comments + internal static let comments = L10n.tr("Localizable", "comments_view.title.comments", fallback: "Comments") + } + } + internal enum Common { + internal enum Value { + /// %@ day + internal static func day(_ p1: Any) -> String { + return L10n.tr("Localizable", "common.value.day", String(describing: p1), fallback: "%@ day") + } + /// %@ days + internal static func days(_ p1: Any) -> String { + return L10n.tr("Localizable", "common.value.days", String(describing: p1), fallback: "%@ days") + } + /// %@ hour + internal static func hour(_ p1: Any) -> String { + return L10n.tr("Localizable", "common.value.hour", String(describing: p1), fallback: "%@ hour") + } + /// %@ hours + internal static func hours(_ p1: Any) -> String { + return L10n.tr("Localizable", "common.value.hours", String(describing: p1), fallback: "%@ hours") + } + /// %@ minute + internal static func minute(_ p1: Any) -> String { + return L10n.tr("Localizable", "common.value.minute", String(describing: p1), fallback: "%@ minute") + } + /// %@ minutes + internal static func minutes(_ p1: Any) -> String { + return L10n.tr("Localizable", "common.value.minutes", String(describing: p1), fallback: "%@ minutes") + } + /// %@ pages + internal static func pages(_ p1: Any) -> String { + return L10n.tr("Localizable", "common.value.pages", String(describing: p1), fallback: "%@ pages") + } + /// %@ records + internal static func records(_ p1: Any) -> String { + return L10n.tr("Localizable", "common.value.records", String(describing: p1), fallback: "%@ records") + } + /// %@ second + internal static func second(_ p1: Any) -> String { + return L10n.tr("Localizable", "common.value.second", String(describing: p1), fallback: "%@ second") + } + /// %@ seconds + internal static func seconds(_ p1: Any) -> String { + return L10n.tr("Localizable", "common.value.seconds", String(describing: p1), fallback: "%@ seconds") + } + /// %@ stars + internal static func stars(_ p1: Any) -> String { + return L10n.tr("Localizable", "common.value.stars", String(describing: p1), fallback: "%@ stars") + } + /// %@ times + internal static func times(_ p1: Any) -> String { + return L10n.tr("Localizable", "common.value.times", String(describing: p1), fallback: "%@ times") + } + } + } + internal enum ConfirmationDialog { + internal enum Button { + /// Clear + internal static let clear = L10n.tr("Localizable", "confirmation_dialog.button.clear", fallback: "Clear") + /// Delete + internal static let delete = L10n.tr("Localizable", "confirmation_dialog.button.delete", fallback: "Delete") + /// Drop the database + internal static let dropDatabase = L10n.tr("Localizable", "confirmation_dialog.button.drop_database", fallback: "Drop the database") + /// Logout + internal static let logout = L10n.tr("Localizable", "confirmation_dialog.button.logout", fallback: "Logout") + /// Remove + internal static let remove = L10n.tr("Localizable", "confirmation_dialog.button.remove", fallback: "Remove") + /// Reset + internal static let reset = L10n.tr("Localizable", "confirmation_dialog.button.reset", fallback: "Reset") + } + internal enum Title { + /// Are you sure to clear? + internal static let clear = L10n.tr("Localizable", "confirmation_dialog.title.clear", fallback: "Are you sure to clear?") + /// Are you sure to delete this item? + internal static let delete = L10n.tr("Localizable", "confirmation_dialog.title.delete", fallback: "Are you sure to delete this item?") + /// You will lose all your data in this app. + /// Are you sure to drop the database? + internal static let dropDatabase = L10n.tr("Localizable", "confirmation_dialog.title.drop_database", fallback: "You will lose all your data in this app.\nAre you sure to drop the database?") + /// Are you sure to logout? + internal static let logout = L10n.tr("Localizable", "confirmation_dialog.title.logout", fallback: "Are you sure to logout?") + /// Are you sure to remove your custom translations? + internal static let removeCustomTranslations = L10n.tr("Localizable", "confirmation_dialog.title.remove_custom_translations", fallback: "Are you sure to remove your custom translations?") + /// Are you sure to reset? + internal static let reset = L10n.tr("Localizable", "confirmation_dialog.title.reset", fallback: "Are you sure to reset?") + } + } + internal enum DetailView { + internal enum ActionSection { + internal enum Button { + /// Give a Rating + internal static let giveARating = L10n.tr("Localizable", "detail_view.action_section.button.give_a_rating", fallback: "Give a Rating") + /// Similar Gallery + internal static let similarGallery = L10n.tr("Localizable", "detail_view.action_section.button.similar_gallery", fallback: "Similar Gallery") + } + } + internal enum Button { + /// Post comment + internal static let postComment = L10n.tr("Localizable", "detail_view.button.post_comment", fallback: "Post comment") + /// Read + internal static let read = L10n.tr("Localizable", "detail_view.button.read", fallback: "Read") + } + internal enum ContextMenu { + internal enum Button { + /// Detail + internal static let detail = L10n.tr("Localizable", "detail_view.context_menu.button.detail", fallback: "Detail") + /// Vote down + internal static let voteDown = L10n.tr("Localizable", "detail_view.context_menu.button.vote_down", fallback: "Vote down") + /// Vote up + internal static let voteUp = L10n.tr("Localizable", "detail_view.context_menu.button.vote_up", fallback: "Vote up") + /// Withdraw vote + internal static let withdrawVote = L10n.tr("Localizable", "detail_view.context_menu.button.withdraw_vote", fallback: "Withdraw vote") + } + } + internal enum DescriptionSection { + internal enum Description { + /// Times + internal static let favorited = L10n.tr("Localizable", "detail_view.description_section.description.favorited", fallback: "Times") + /// Pages + internal static let pageCount = L10n.tr("Localizable", "detail_view.description_section.description.page_count", fallback: "Pages") + } + internal enum Title { + /// Favorited + internal static let favorited = L10n.tr("Localizable", "detail_view.description_section.title.favorited", fallback: "Favorited") + /// File Size + internal static let fileSize = L10n.tr("Localizable", "detail_view.description_section.title.file_size", fallback: "File Size") + /// Language + internal static let language = L10n.tr("Localizable", "detail_view.description_section.title.language", fallback: "Language") + /// Page Count + internal static let pageCount = L10n.tr("Localizable", "detail_view.description_section.title.page_count", fallback: "Page Count") + /// %@ Ratings + internal static func ratings(_ p1: Any) -> String { + return L10n.tr("Localizable", "detail_view.description_section.title.ratings", String(describing: p1), fallback: "%@ Ratings") + } + } + } + internal enum Section { + internal enum Title { + /// Comments + internal static let comments = L10n.tr("Localizable", "detail_view.section.title.comments", fallback: "Comments") + /// Previews + internal static let previews = L10n.tr("Localizable", "detail_view.section.title.previews", fallback: "Previews") + } + } + internal enum ToolbarItem { + internal enum Button { + /// Archives + internal static let archives = L10n.tr("Localizable", "detail_view.toolbar_item.button.archives", fallback: "Archives") + /// Share + internal static let share = L10n.tr("Localizable", "detail_view.toolbar_item.button.share", fallback: "Share") + /// Torrents + internal static let torrents = L10n.tr("Localizable", "detail_view.toolbar_item.button.torrents", fallback: "Torrents") + } + } + } + internal enum EhSettingView { + internal enum Button { + /// Create new + internal static let createNew = L10n.tr("Localizable", "eh_setting_view.button.create_new", fallback: "Create new") + /// Delete profile + internal static let deleteProfile = L10n.tr("Localizable", "eh_setting_view.button.delete_profile", fallback: "Delete profile") + /// Rename + internal static let rename = L10n.tr("Localizable", "eh_setting_view.button.rename", fallback: "Rename") + /// Set as default + internal static let setAsDefault = L10n.tr("Localizable", "eh_setting_view.button.set_as_default", fallback: "Set as default") + } + internal enum Description { + /// 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. + internal static let archiverBehavior = L10n.tr("Localizable", "eh_setting_view.description.archiver_behavior", fallback: "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.") + /// You appear to be browsing the site from **%@** 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. + internal static func browsingCountry(_ p1: Any) -> String { + return L10n.tr("Localizable", "eh_setting_view.description.browsing_country", String(describing: p1), fallback: "You appear to be browsing the site from **%@** 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.") + } + /// Which display mode would you like to use on the front and search pages? + internal static let displayMode = L10n.tr("Localizable", "eh_setting_view.description.display_mode", fallback: "Which display mode would you like to use on the front and search pages?") + /// 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. + internal static let excludedLanguages = L10n.tr("Localizable", "eh_setting_view.description.excluded_languages", fallback: "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 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. + internal static let excludedUploaders = L10n.tr("Localizable", "eh_setting_view.description.excluded_uploaders", fallback: "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 **%@ / %@** exclusion slots. + internal static func excludedUploadersCount(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "eh_setting_view.description.excluded_uploaders_count", String(describing: p1), String(describing: p2), fallback: "You are currently using **%@ / %@** exclusion slots.") + } + /// Here you can choose and rename your favorite categories. + internal static let favoriteCategories = L10n.tr("Localizable", "eh_setting_view.description.favorite_categories", fallback: "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. + internal static let favoritesSortOrder = L10n.tr("Localizable", "eh_setting_view.description.favorites_sort_order", fallback: "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.") + /// Show the "Your default filters removed XX galleries from this page" readout? + internal static let filteredRemovalCount = L10n.tr("Localizable", "eh_setting_view.description.filtered_removal_count", fallback: "Show the \"Your default filters removed XX galleries from this page\" readout?") + /// What categories would you like to show by default on the front page and in searches? + internal static let galleryCategory = L10n.tr("Localizable", "eh_setting_view.description.gallery_category", fallback: "What categories would you like to show by default on the front page and in searches?") + /// Many galleries have both an English/Romanized title and a title in Japanese script. Which gallery name would you like as default? + internal static let galleryName = L10n.tr("Localizable", "eh_setting_view.description.gallery_name", fallback: "Many galleries have both an English/Romanized title and a title in Japanese script. Which gallery name would you like as default?") + /// 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. + internal static let imageResolution = L10n.tr("Localizable", "eh_setting_view.description.image_resolution", fallback: "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) + internal static let imageSize = L10n.tr("Localizable", "eh_setting_view.description.image_size", fallback: "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)") + /// This setting can be used if you have a H@H client running on your local network with the same public IP you browse the site with. Some routers are buggy and cannot route requests back to its own IP; this allows you to work around this problem. + /// If you are running the client on the same device you browse from, use the loopback address (127.0.0.1:port). If the client is running on another device on your network, use its local network IP. Some browser configurations prevent external web sites from accessing URLs with local network IPs, the site must then be whitelisted for this to work. + internal static let ipAddressPort = L10n.tr("Localizable", "eh_setting_view.description.ip_address_port", fallback: "This setting can be used if you have a H@H client running on your local network with the same public IP you browse the site with. Some routers are buggy and cannot route requests back to its own IP; this allows you to work around this problem.\nIf you are running the client on the same device you browse from, use the loopback address (127.0.0.1:port). If the client is running on another device on your network, use its local network IP. Some browser configurations prevent external web sites from accessing URLs with local network IPs, the site must then be whitelisted for this to work.") + /// 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. + internal static let ratingsColor = L10n.tr("Localizable", "eh_setting_view.description.ratings_color", fallback: "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.") + /// How many results would you like per page for the index/search page and torrent search pages? + /// (Hath Perk: Paging Enlargement Required) + internal static let resultCount = L10n.tr("Localizable", "eh_setting_view.description.result_count", fallback: "How many results would you like per page for the index/search page and torrent search pages?\n(Hath Perk: Paging Enlargement Required)") + /// Thumbnails on the thumbnail and extended gallery list views can be scaled to a custom value between 75%% and 150%%. + internal static let scaleFactor = L10n.tr("Localizable", "eh_setting_view.description.scale_factor", fallback: "Thumbnails on the thumbnail and extended gallery list views can be scaled to a custom value between 75%% and 150%%.") + /// 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. + internal static let tagFilteringThreshold = L10n.tr("Localizable", "eh_setting_view.description.tag_filtering_threshold", fallback: "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.") + /// 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. + internal static let tagWatchingThreshold = L10n.tr("Localizable", "eh_setting_view.description.tag_watching_threshold", fallback: "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.") + /// You can set a default thumbnail configuration for all galleries you visit. + internal static let thumbnailConfiguration = L10n.tr("Localizable", "eh_setting_view.description.thumbnail_configuration", fallback: "You can set a default thumbnail configuration for all galleries you visit.") + /// How would you like the mouse-over thumbnails on the front page to load when using List Mode? + internal static let thumbnailLoadTiming = L10n.tr("Localizable", "eh_setting_view.description.thumbnail_load_timing", fallback: "How would you like the mouse-over thumbnails on the front page to load when using List Mode?") + /// Allows 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. + internal static let virtualWidth = L10n.tr("Localizable", "eh_setting_view.description.virtual_width", fallback: "Allows 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.") + } + internal enum Promt { + /// RRGGB + internal static let ratingsColor = L10n.tr("Localizable", "eh_setting_view.promt.ratings_color", fallback: "RRGGB") + } + internal enum Section { + internal enum Title { + /// Archiver Settings + internal static let archiverSettings = L10n.tr("Localizable", "eh_setting_view.section.title.archiver_settings", fallback: "Archiver Settings") + /// Excluded Languages + internal static let excludedLanguages = L10n.tr("Localizable", "eh_setting_view.section.title.excluded_languages", fallback: "Excluded Languages") + /// Excluded Uploaders + internal static let excludedUploaders = L10n.tr("Localizable", "eh_setting_view.section.title.excluded_uploaders", fallback: "Excluded Uploaders") + /// Favorites + internal static let favorites = L10n.tr("Localizable", "eh_setting_view.section.title.favorites", fallback: "Favorites") + /// Show Filtered Removal Count + internal static let filteredRemovalCount = L10n.tr("Localizable", "eh_setting_view.section.title.filtered_removal_count", fallback: "Show Filtered Removal Count") + /// Front Page Settings + internal static let frontPageSettings = L10n.tr("Localizable", "eh_setting_view.section.title.front_page_settings", fallback: "Front Page Settings") + /// Gallery Comments + internal static let galleryComments = L10n.tr("Localizable", "eh_setting_view.section.title.gallery_comments", fallback: "Gallery Comments") + /// Gallery Name Display + internal static let galleryNameDisplay = L10n.tr("Localizable", "eh_setting_view.section.title.gallery_name_display", fallback: "Gallery Name Display") + /// Gallery Page Numbering + internal static let galleryPageNumbering = L10n.tr("Localizable", "eh_setting_view.section.title.gallery_page_numbering", fallback: "Gallery Page Numbering") + /// Gallery Tags + internal static let galleryTags = L10n.tr("Localizable", "eh_setting_view.section.title.gallery_tags", fallback: "Gallery Tags") + /// Hath Local Network Host + internal static let hathLocalNetworkHost = L10n.tr("Localizable", "eh_setting_view.section.title.hath_local_network_host", fallback: "Hath Local Network Host") + /// Image Load Settings + internal static let imageLoadSettings = L10n.tr("Localizable", "eh_setting_view.section.title.image_load_settings", fallback: "Image Load Settings") + /// Image Size Settings + internal static let imageSizeSettings = L10n.tr("Localizable", "eh_setting_view.section.title.image_size_settings", fallback: "Image Size Settings") + /// Multi-Page Viewer + internal static let multiPageViewer = L10n.tr("Localizable", "eh_setting_view.section.title.multi_page_viewer", fallback: "Multi-Page Viewer") + /// Original Images + internal static let originalImages = L10n.tr("Localizable", "eh_setting_view.section.title.original_images", fallback: "Original Images") + /// Profile Settings + internal static let profileSettings = L10n.tr("Localizable", "eh_setting_view.section.title.profile_settings", fallback: "Profile Settings") + /// Ratings + internal static let ratings = L10n.tr("Localizable", "eh_setting_view.section.title.ratings", fallback: "Ratings") + /// Search Result Count + internal static let searchResultCount = L10n.tr("Localizable", "eh_setting_view.section.title.search_result_count", fallback: "Search Result Count") + /// Search Range Indicator + internal static let showSearchRangeIndicator = L10n.tr("Localizable", "eh_setting_view.section.title.show_search_range_indicator", fallback: "Search Range Indicator") + /// Tag Filtering Threshold + internal static let tagFilteringThreshold = L10n.tr("Localizable", "eh_setting_view.section.title.tag_filtering_threshold", fallback: "Tag Filtering Threshold") + /// Tag Watching Threshold + internal static let tagWatchingThreshold = L10n.tr("Localizable", "eh_setting_view.section.title.tag_watching_threshold", fallback: "Tag Watching Threshold") + /// Thumbnail Scaling + internal static let thumbnailScaling = L10n.tr("Localizable", "eh_setting_view.section.title.thumbnail_scaling", fallback: "Thumbnail Scaling") + /// Thumbnail Settings + internal static let thumbnailSettings = L10n.tr("Localizable", "eh_setting_view.section.title.thumbnail_settings", fallback: "Thumbnail Settings") + /// Viewport Override + internal static let viewportOverride = L10n.tr("Localizable", "eh_setting_view.section.title.viewport_override", fallback: "Viewport Override") + } + } + internal enum Title { + /// Archiver behavior + internal static let archiverBehavior = L10n.tr("Localizable", "eh_setting_view.title.archiver_behavior", fallback: "Archiver behavior") + /// Browsing country + internal static let browsingCountry = L10n.tr("Localizable", "eh_setting_view.title.browsing_country", fallback: "Browsing country") + /// Comments sort order + internal static let commentsSortOrder = L10n.tr("Localizable", "eh_setting_view.title.comments_sort_order", fallback: "Comments sort order") + /// Comment votes show timing + internal static let commentsVotesShowTiming = L10n.tr("Localizable", "eh_setting_view.title.comments_votes_show_timing", fallback: "Comment votes show timing") + /// Display mode + internal static let displayMode = L10n.tr("Localizable", "eh_setting_view.title.display_mode", fallback: "Display mode") + /// Display style + internal static let displayStyle = L10n.tr("Localizable", "eh_setting_view.title.display_style", fallback: "Display style") + /// Favorites sort order + internal static let favoritesSortOrder = L10n.tr("Localizable", "eh_setting_view.title.favorites_sort_order", fallback: "Favorites sort order") + /// Gallery name + internal static let galleryName = L10n.tr("Localizable", "eh_setting_view.title.gallery_name", fallback: "Gallery name") + /// Horizontal + internal static let horizontal = L10n.tr("Localizable", "eh_setting_view.title.horizontal", fallback: "Horizontal") + /// %@ settings + internal static func hostSettings(_ p1: Any) -> String { + return L10n.tr("Localizable", "eh_setting_view.title.host_settings", String(describing: p1), fallback: "%@ settings") + } + /// Image resolution + internal static let imageResolution = L10n.tr("Localizable", "eh_setting_view.title.image_resolution", fallback: "Image resolution") + /// Image size + internal static let imageSize = L10n.tr("Localizable", "eh_setting_view.title.image_size", fallback: "Image size") + /// IP address:Port + internal static let ipAddressPort = L10n.tr("Localizable", "eh_setting_view.title.ip_address_port", fallback: "IP address:Port") + /// Load images through the Hath network + internal static let loadImagesThroughTheHathNetwork = L10n.tr("Localizable", "eh_setting_view.title.load_images_through_the_hath_network", fallback: "Load images through the Hath network") + /// Ratings color + internal static let ratingsColor = L10n.tr("Localizable", "eh_setting_view.title.ratings_color", fallback: "Ratings color") + /// Result count + internal static let resultCount = L10n.tr("Localizable", "eh_setting_view.title.result_count", fallback: "Result count") + /// Scale factor + internal static let scaleFactor = L10n.tr("Localizable", "eh_setting_view.title.scale_factor", fallback: "Scale factor") + /// Selected profile + internal static let selectedProfile = L10n.tr("Localizable", "eh_setting_view.title.selected_profile", fallback: "Selected profile") + /// Show filtered removal count + internal static let showFilteredRemovalCount = L10n.tr("Localizable", "eh_setting_view.title.show_filtered_removal_count", fallback: "Show filtered removal count") + /// Show gallery page numbers + internal static let showGalleryPageNumbers = L10n.tr("Localizable", "eh_setting_view.title.show_gallery_page_numbers", fallback: "Show gallery page numbers") + /// Show search range indicator + internal static let showSearchRangeIndicator = L10n.tr("Localizable", "eh_setting_view.title.show_search_range_indicator", fallback: "Show search range indicator") + /// Show thumbnail pane + internal static let showThumbnailPane = L10n.tr("Localizable", "eh_setting_view.title.show_thumbnail_pane", fallback: "Show thumbnail pane") + /// Tag Filtering Threshold + internal static let tagFilteringThreshold = L10n.tr("Localizable", "eh_setting_view.title.tag_filtering_threshold", fallback: "Tag Filtering Threshold") + /// Tag Watching Threshold + internal static let tagWatchingThreshold = L10n.tr("Localizable", "eh_setting_view.title.tag_watching_threshold", fallback: "Tag Watching Threshold") + /// Tags sort order + internal static let tagsSortOrder = L10n.tr("Localizable", "eh_setting_view.title.tags_sort_order", fallback: "Tags sort order") + /// Thumbnail load timing + internal static let thumbnailLoadTiming = L10n.tr("Localizable", "eh_setting_view.title.thumbnail_load_timing", fallback: "Thumbnail load timing") + /// Rows + internal static let thumbnailRowCount = L10n.tr("Localizable", "eh_setting_view.title.thumbnail_row_count", fallback: "Rows") + /// Size + internal static let thumbnailSize = L10n.tr("Localizable", "eh_setting_view.title.thumbnail_size", fallback: "Size") + /// Use Multi-Page Viewer + internal static let useMultiPageViewer = L10n.tr("Localizable", "eh_setting_view.title.use_multi_page_viewer", fallback: "Use Multi-Page Viewer") + /// Use original images + internal static let useOriginalImages = L10n.tr("Localizable", "eh_setting_view.title.use_original_images", fallback: "Use original images") + /// Vertical + internal static let vertical = L10n.tr("Localizable", "eh_setting_view.title.vertical", fallback: "Vertical") + /// Virtual width + internal static let virtualWidth = L10n.tr("Localizable", "eh_setting_view.title.virtual_width", fallback: "Virtual width") + } + internal enum ToolbarItem { + internal enum Button { + /// Done + internal static let done = L10n.tr("Localizable", "eh_setting_view.toolbar_item.button.done", fallback: "Done") + } + } + } + internal enum Enum { + internal enum AppIconType { + internal enum Value { + /// Default + internal static let `default` = L10n.tr("Localizable", "enum.app_icon_type.value.default", fallback: "Default") + /// Developer + internal static let developer = L10n.tr("Localizable", "enum.app_icon_type.value.developer", fallback: "Developer") + /// NOT MY PRESIDENT + internal static let notMyPresident = L10n.tr("Localizable", "enum.app_icon_type.value.not_my_president", fallback: "NOT MY PRESIDENT") + /// Stand With Ukraine (2022) + internal static let standWithUkraine2022 = L10n.tr("Localizable", "enum.app_icon_type.value.stand_with_ukraine_2022", fallback: "Stand With Ukraine (2022)") + /// Ukiyo-e + internal static let ukiyoe = L10n.tr("Localizable", "enum.app_icon_type.value.ukiyoe", fallback: "Ukiyo-e") + } + } + internal enum ArchiveResolution { + internal enum Value { + /// Original + internal static let original = L10n.tr("Localizable", "enum.archive_resolution.value.original", fallback: "Original") + } + } + internal enum AutoLockPolicy { + internal enum Value { + /// Instantly + internal static let instantly = L10n.tr("Localizable", "enum.auto_lock_policy.value.instantly", fallback: "Instantly") + /// Never + internal static let never = L10n.tr("Localizable", "enum.auto_lock_policy.value.never", fallback: "Never") + } + } + internal enum AutoPlayPolicy { + internal enum Value { + /// Off + internal static let off = L10n.tr("Localizable", "enum.auto_play_policy.value.off", fallback: "Off") + } + } + internal enum BanInterval { + internal enum Description { + /// Localizable.strings + /// EhPanda + /// + /// Created by 荒木辰造 on R 2/12/25. + internal static let and = L10n.tr("Localizable", "enum.ban_interval.description.and", fallback: "and") + } + } + internal enum BrowsingCountry { + internal enum Name { + /// Afghanistan + internal static let afghanistan = L10n.tr("Localizable", "enum.browsing_country.name.afghanistan", fallback: "Afghanistan") + /// Aland Islands + internal static let alandIslands = L10n.tr("Localizable", "enum.browsing_country.name.aland_islands", fallback: "Aland Islands") + /// Albania + internal static let albania = L10n.tr("Localizable", "enum.browsing_country.name.albania", fallback: "Albania") + /// Algeria + internal static let algeria = L10n.tr("Localizable", "enum.browsing_country.name.algeria", fallback: "Algeria") + /// American Samoa + internal static let americanSamoa = L10n.tr("Localizable", "enum.browsing_country.name.american_samoa", fallback: "American Samoa") + /// Andorra + internal static let andorra = L10n.tr("Localizable", "enum.browsing_country.name.andorra", fallback: "Andorra") + /// Angola + internal static let angola = L10n.tr("Localizable", "enum.browsing_country.name.angola", fallback: "Angola") + /// Anguilla + internal static let anguilla = L10n.tr("Localizable", "enum.browsing_country.name.anguilla", fallback: "Anguilla") + /// Antarctica + internal static let antarctica = L10n.tr("Localizable", "enum.browsing_country.name.antarctica", fallback: "Antarctica") + /// Antigua and Barbuda + internal static let antiguaAndBarbuda = L10n.tr("Localizable", "enum.browsing_country.name.antigua_and_barbuda", fallback: "Antigua and Barbuda") + /// Argentina + internal static let argentina = L10n.tr("Localizable", "enum.browsing_country.name.argentina", fallback: "Argentina") + /// Armenia + internal static let armenia = L10n.tr("Localizable", "enum.browsing_country.name.armenia", fallback: "Armenia") + /// Aruba + internal static let aruba = L10n.tr("Localizable", "enum.browsing_country.name.aruba", fallback: "Aruba") + /// Asia-Pacific Region + internal static let asiaPacificRegion = L10n.tr("Localizable", "enum.browsing_country.name.asia_pacific_region", fallback: "Asia-Pacific Region") + /// Australia + internal static let australia = L10n.tr("Localizable", "enum.browsing_country.name.australia", fallback: "Australia") + /// Austria + internal static let austria = L10n.tr("Localizable", "enum.browsing_country.name.austria", fallback: "Austria") + /// Auto-Detect + internal static let autoDetect = L10n.tr("Localizable", "enum.browsing_country.name.auto_detect", fallback: "Auto-Detect") + /// Azerbaijan + internal static let azerbaijan = L10n.tr("Localizable", "enum.browsing_country.name.azerbaijan", fallback: "Azerbaijan") + /// Bahamas + internal static let bahamas = L10n.tr("Localizable", "enum.browsing_country.name.bahamas", fallback: "Bahamas") + /// Bahrain + internal static let bahrain = L10n.tr("Localizable", "enum.browsing_country.name.bahrain", fallback: "Bahrain") + /// Bangladesh + internal static let bangladesh = L10n.tr("Localizable", "enum.browsing_country.name.bangladesh", fallback: "Bangladesh") + /// Barbados + internal static let barbados = L10n.tr("Localizable", "enum.browsing_country.name.barbados", fallback: "Barbados") + /// Belarus + internal static let belarus = L10n.tr("Localizable", "enum.browsing_country.name.belarus", fallback: "Belarus") + /// Belgium + internal static let belgium = L10n.tr("Localizable", "enum.browsing_country.name.belgium", fallback: "Belgium") + /// Belize + internal static let belize = L10n.tr("Localizable", "enum.browsing_country.name.belize", fallback: "Belize") + /// Benin + internal static let benin = L10n.tr("Localizable", "enum.browsing_country.name.benin", fallback: "Benin") + /// Bermuda + internal static let bermuda = L10n.tr("Localizable", "enum.browsing_country.name.bermuda", fallback: "Bermuda") + /// Bhutan + internal static let bhutan = L10n.tr("Localizable", "enum.browsing_country.name.bhutan", fallback: "Bhutan") + /// Bolivia + internal static let bolivia = L10n.tr("Localizable", "enum.browsing_country.name.bolivia", fallback: "Bolivia") + /// Bonaire Saint Eustatius and Saba + internal static let bonaireSaintEustatiusAndSaba = L10n.tr("Localizable", "enum.browsing_country.name.bonaire_saint_eustatius_and_saba", fallback: "Bonaire Saint Eustatius and Saba") + /// Bosnia and Herzegovina + internal static let bosniaAndHerzegovina = L10n.tr("Localizable", "enum.browsing_country.name.bosnia_and_herzegovina", fallback: "Bosnia and Herzegovina") + /// Botswana + internal static let botswana = L10n.tr("Localizable", "enum.browsing_country.name.botswana", fallback: "Botswana") + /// Bouvet Island + internal static let bouvetIsland = L10n.tr("Localizable", "enum.browsing_country.name.bouvet_island", fallback: "Bouvet Island") + /// Brazil + internal static let brazil = L10n.tr("Localizable", "enum.browsing_country.name.brazil", fallback: "Brazil") + /// British Indian Ocean Territory + internal static let britishIndianOceanTerritory = L10n.tr("Localizable", "enum.browsing_country.name.british_indian_ocean_territory", fallback: "British Indian Ocean Territory") + /// Brunei Darussalam + internal static let bruneiDarussalam = L10n.tr("Localizable", "enum.browsing_country.name.brunei_darussalam", fallback: "Brunei Darussalam") + /// Bulgaria + internal static let bulgaria = L10n.tr("Localizable", "enum.browsing_country.name.bulgaria", fallback: "Bulgaria") + /// Burkina Faso + internal static let burkinaFaso = L10n.tr("Localizable", "enum.browsing_country.name.burkina_faso", fallback: "Burkina Faso") + /// Burundi + internal static let burundi = L10n.tr("Localizable", "enum.browsing_country.name.burundi", fallback: "Burundi") + /// Cambodia + internal static let cambodia = L10n.tr("Localizable", "enum.browsing_country.name.cambodia", fallback: "Cambodia") + /// Cameroon + internal static let cameroon = L10n.tr("Localizable", "enum.browsing_country.name.cameroon", fallback: "Cameroon") + /// Canada + internal static let canada = L10n.tr("Localizable", "enum.browsing_country.name.canada", fallback: "Canada") + /// Cape Verde + internal static let capeVerde = L10n.tr("Localizable", "enum.browsing_country.name.cape_verde", fallback: "Cape Verde") + /// Cayman Islands + internal static let caymanIslands = L10n.tr("Localizable", "enum.browsing_country.name.cayman_islands", fallback: "Cayman Islands") + /// Central African Republic + internal static let centralAfricanRepublic = L10n.tr("Localizable", "enum.browsing_country.name.central_african_republic", fallback: "Central African Republic") + /// Chad + internal static let chad = L10n.tr("Localizable", "enum.browsing_country.name.chad", fallback: "Chad") + /// Chile + internal static let chile = L10n.tr("Localizable", "enum.browsing_country.name.chile", fallback: "Chile") + /// China + internal static let china = L10n.tr("Localizable", "enum.browsing_country.name.china", fallback: "China") + /// Christmas Island + internal static let christmasIsland = L10n.tr("Localizable", "enum.browsing_country.name.christmas_island", fallback: "Christmas Island") + /// Cocos Islands + internal static let cocosIslands = L10n.tr("Localizable", "enum.browsing_country.name.cocos_islands", fallback: "Cocos Islands") + /// Colombia + internal static let colombia = L10n.tr("Localizable", "enum.browsing_country.name.colombia", fallback: "Colombia") + /// Comoros + internal static let comoros = L10n.tr("Localizable", "enum.browsing_country.name.comoros", fallback: "Comoros") + /// Congo + internal static let congo = L10n.tr("Localizable", "enum.browsing_country.name.congo", fallback: "Congo") + /// Cook Islands + internal static let cookIslands = L10n.tr("Localizable", "enum.browsing_country.name.cook_islands", fallback: "Cook Islands") + /// Costa Rica + internal static let costaRica = L10n.tr("Localizable", "enum.browsing_country.name.costa_rica", fallback: "Costa Rica") + /// Cote D'Ivoire + internal static let coteDIvoire = L10n.tr("Localizable", "enum.browsing_country.name.cote_d_ivoire", fallback: "Cote D'Ivoire") + /// Croatia + internal static let croatia = L10n.tr("Localizable", "enum.browsing_country.name.croatia", fallback: "Croatia") + /// Cuba + internal static let cuba = L10n.tr("Localizable", "enum.browsing_country.name.cuba", fallback: "Cuba") + /// Curacao + internal static let curacao = L10n.tr("Localizable", "enum.browsing_country.name.curacao", fallback: "Curacao") + /// Cyprus + internal static let cyprus = L10n.tr("Localizable", "enum.browsing_country.name.cyprus", fallback: "Cyprus") + /// Czech Republic + internal static let czechRepublic = L10n.tr("Localizable", "enum.browsing_country.name.czech_republic", fallback: "Czech Republic") + /// Denmark + internal static let denmark = L10n.tr("Localizable", "enum.browsing_country.name.denmark", fallback: "Denmark") + /// Djibouti + internal static let djibouti = L10n.tr("Localizable", "enum.browsing_country.name.djibouti", fallback: "Djibouti") + /// Dominica + internal static let dominica = L10n.tr("Localizable", "enum.browsing_country.name.dominica", fallback: "Dominica") + /// Dominican Republic + internal static let dominicanRepublic = L10n.tr("Localizable", "enum.browsing_country.name.dominican_republic", fallback: "Dominican Republic") + /// Ecuador + internal static let ecuador = L10n.tr("Localizable", "enum.browsing_country.name.ecuador", fallback: "Ecuador") + /// Egypt + internal static let egypt = L10n.tr("Localizable", "enum.browsing_country.name.egypt", fallback: "Egypt") + /// El Salvador + internal static let elSalvador = L10n.tr("Localizable", "enum.browsing_country.name.el_salvador", fallback: "El Salvador") + /// Equatorial Guinea + internal static let equatorialGuinea = L10n.tr("Localizable", "enum.browsing_country.name.equatorial_guinea", fallback: "Equatorial Guinea") + /// Eritrea + internal static let eritrea = L10n.tr("Localizable", "enum.browsing_country.name.eritrea", fallback: "Eritrea") + /// Estonia + internal static let estonia = L10n.tr("Localizable", "enum.browsing_country.name.estonia", fallback: "Estonia") + /// Ethiopia + internal static let ethiopia = L10n.tr("Localizable", "enum.browsing_country.name.ethiopia", fallback: "Ethiopia") + /// Europe + internal static let europe = L10n.tr("Localizable", "enum.browsing_country.name.europe", fallback: "Europe") + /// Falkland Islands + internal static let falklandIslands = L10n.tr("Localizable", "enum.browsing_country.name.falkland_islands", fallback: "Falkland Islands") + /// Faroe Islands + internal static let faroeIslands = L10n.tr("Localizable", "enum.browsing_country.name.faroe_islands", fallback: "Faroe Islands") + /// Fiji + internal static let fiji = L10n.tr("Localizable", "enum.browsing_country.name.fiji", fallback: "Fiji") + /// Finland + internal static let finland = L10n.tr("Localizable", "enum.browsing_country.name.finland", fallback: "Finland") + /// France + internal static let france = L10n.tr("Localizable", "enum.browsing_country.name.france", fallback: "France") + /// French Guiana + internal static let frenchGuiana = L10n.tr("Localizable", "enum.browsing_country.name.french_guiana", fallback: "French Guiana") + /// French Polynesia + internal static let frenchPolynesia = L10n.tr("Localizable", "enum.browsing_country.name.french_polynesia", fallback: "French Polynesia") + /// French Southern Territories + internal static let frenchSouthernTerritories = L10n.tr("Localizable", "enum.browsing_country.name.french_southern_territories", fallback: "French Southern Territories") + /// Gabon + internal static let gabon = L10n.tr("Localizable", "enum.browsing_country.name.gabon", fallback: "Gabon") + /// Gambia + internal static let gambia = L10n.tr("Localizable", "enum.browsing_country.name.gambia", fallback: "Gambia") + /// Georgia + internal static let georgia = L10n.tr("Localizable", "enum.browsing_country.name.georgia", fallback: "Georgia") + /// Germany + internal static let germany = L10n.tr("Localizable", "enum.browsing_country.name.germany", fallback: "Germany") + /// Ghana + internal static let ghana = L10n.tr("Localizable", "enum.browsing_country.name.ghana", fallback: "Ghana") + /// Gibraltar + internal static let gibraltar = L10n.tr("Localizable", "enum.browsing_country.name.gibraltar", fallback: "Gibraltar") + /// Greece + internal static let greece = L10n.tr("Localizable", "enum.browsing_country.name.greece", fallback: "Greece") + /// Greenland + internal static let greenland = L10n.tr("Localizable", "enum.browsing_country.name.greenland", fallback: "Greenland") + /// Grenada + internal static let grenada = L10n.tr("Localizable", "enum.browsing_country.name.grenada", fallback: "Grenada") + /// Guadeloupe + internal static let guadeloupe = L10n.tr("Localizable", "enum.browsing_country.name.guadeloupe", fallback: "Guadeloupe") + /// Guam + internal static let guam = L10n.tr("Localizable", "enum.browsing_country.name.guam", fallback: "Guam") + /// Guatemala + internal static let guatemala = L10n.tr("Localizable", "enum.browsing_country.name.guatemala", fallback: "Guatemala") + /// Guernsey + internal static let guernsey = L10n.tr("Localizable", "enum.browsing_country.name.guernsey", fallback: "Guernsey") + /// Guinea + internal static let guinea = L10n.tr("Localizable", "enum.browsing_country.name.guinea", fallback: "Guinea") + /// Guinea-Bissau + internal static let guineaBissau = L10n.tr("Localizable", "enum.browsing_country.name.guinea_bissau", fallback: "Guinea-Bissau") + /// Guyana + internal static let guyana = L10n.tr("Localizable", "enum.browsing_country.name.guyana", fallback: "Guyana") + /// Haiti + internal static let haiti = L10n.tr("Localizable", "enum.browsing_country.name.haiti", fallback: "Haiti") + /// Heard Island and McDonald Islands + internal static let heardIslandAndMcDonaldIslands = L10n.tr("Localizable", "enum.browsing_country.name.heard_island_and_mc_donald_islands", fallback: "Heard Island and McDonald Islands") + /// Honduras + internal static let honduras = L10n.tr("Localizable", "enum.browsing_country.name.honduras", fallback: "Honduras") + /// Hong Kong + internal static let hongKong = L10n.tr("Localizable", "enum.browsing_country.name.hong_kong", fallback: "Hong Kong") + /// Hungary + internal static let hungary = L10n.tr("Localizable", "enum.browsing_country.name.hungary", fallback: "Hungary") + /// Iceland + internal static let iceland = L10n.tr("Localizable", "enum.browsing_country.name.iceland", fallback: "Iceland") + /// India + internal static let india = L10n.tr("Localizable", "enum.browsing_country.name.india", fallback: "India") + /// Indonesia + internal static let indonesia = L10n.tr("Localizable", "enum.browsing_country.name.indonesia", fallback: "Indonesia") + /// Iran + internal static let iran = L10n.tr("Localizable", "enum.browsing_country.name.iran", fallback: "Iran") + /// Iraq + internal static let iraq = L10n.tr("Localizable", "enum.browsing_country.name.iraq", fallback: "Iraq") + /// Ireland + internal static let ireland = L10n.tr("Localizable", "enum.browsing_country.name.ireland", fallback: "Ireland") + /// Isle of Man + internal static let isleOfMan = L10n.tr("Localizable", "enum.browsing_country.name.isle_of_man", fallback: "Isle of Man") + /// Israel + internal static let israel = L10n.tr("Localizable", "enum.browsing_country.name.israel", fallback: "Israel") + /// Italy + internal static let italy = L10n.tr("Localizable", "enum.browsing_country.name.italy", fallback: "Italy") + /// Jamaica + internal static let jamaica = L10n.tr("Localizable", "enum.browsing_country.name.jamaica", fallback: "Jamaica") + /// Japan + internal static let japan = L10n.tr("Localizable", "enum.browsing_country.name.japan", fallback: "Japan") + /// Jersey + internal static let jersey = L10n.tr("Localizable", "enum.browsing_country.name.jersey", fallback: "Jersey") + /// Jordan + internal static let jordan = L10n.tr("Localizable", "enum.browsing_country.name.jordan", fallback: "Jordan") + /// Kazakhstan + internal static let kazakhstan = L10n.tr("Localizable", "enum.browsing_country.name.kazakhstan", fallback: "Kazakhstan") + /// Kenya + internal static let kenya = L10n.tr("Localizable", "enum.browsing_country.name.kenya", fallback: "Kenya") + /// Kiribati + internal static let kiribati = L10n.tr("Localizable", "enum.browsing_country.name.kiribati", fallback: "Kiribati") + /// Kuwait + internal static let kuwait = L10n.tr("Localizable", "enum.browsing_country.name.kuwait", fallback: "Kuwait") + /// Kyrgyzstan + internal static let kyrgyzstan = L10n.tr("Localizable", "enum.browsing_country.name.kyrgyzstan", fallback: "Kyrgyzstan") + /// Lao People's Democratic Republic + internal static let laoPeoplesDemocraticRepublic = L10n.tr("Localizable", "enum.browsing_country.name.lao_peoples_democratic_republic", fallback: "Lao People's Democratic Republic") + /// Latvia + internal static let latvia = L10n.tr("Localizable", "enum.browsing_country.name.latvia", fallback: "Latvia") + /// Lebanon + internal static let lebanon = L10n.tr("Localizable", "enum.browsing_country.name.lebanon", fallback: "Lebanon") + /// Lesotho + internal static let lesotho = L10n.tr("Localizable", "enum.browsing_country.name.lesotho", fallback: "Lesotho") + /// Liberia + internal static let liberia = L10n.tr("Localizable", "enum.browsing_country.name.liberia", fallback: "Liberia") + /// Libya + internal static let libya = L10n.tr("Localizable", "enum.browsing_country.name.libya", fallback: "Libya") + /// Liechtenstein + internal static let liechtenstein = L10n.tr("Localizable", "enum.browsing_country.name.liechtenstein", fallback: "Liechtenstein") + /// Lithuania + internal static let lithuania = L10n.tr("Localizable", "enum.browsing_country.name.lithuania", fallback: "Lithuania") + /// Luxembourg + internal static let luxembourg = L10n.tr("Localizable", "enum.browsing_country.name.luxembourg", fallback: "Luxembourg") + /// Macau + internal static let macau = L10n.tr("Localizable", "enum.browsing_country.name.macau", fallback: "Macau") + /// Macedonia + internal static let macedonia = L10n.tr("Localizable", "enum.browsing_country.name.macedonia", fallback: "Macedonia") + /// Madagascar + internal static let madagascar = L10n.tr("Localizable", "enum.browsing_country.name.madagascar", fallback: "Madagascar") + /// Malawi + internal static let malawi = L10n.tr("Localizable", "enum.browsing_country.name.malawi", fallback: "Malawi") + /// Malaysia + internal static let malaysia = L10n.tr("Localizable", "enum.browsing_country.name.malaysia", fallback: "Malaysia") + /// Maldives + internal static let maldives = L10n.tr("Localizable", "enum.browsing_country.name.maldives", fallback: "Maldives") + /// Mali + internal static let mali = L10n.tr("Localizable", "enum.browsing_country.name.mali", fallback: "Mali") + /// Malta + internal static let malta = L10n.tr("Localizable", "enum.browsing_country.name.malta", fallback: "Malta") + /// Marshall Islands + internal static let marshallIslands = L10n.tr("Localizable", "enum.browsing_country.name.marshall_islands", fallback: "Marshall Islands") + /// Martinique + internal static let martinique = L10n.tr("Localizable", "enum.browsing_country.name.martinique", fallback: "Martinique") + /// Mauritania + internal static let mauritania = L10n.tr("Localizable", "enum.browsing_country.name.mauritania", fallback: "Mauritania") + /// Mauritius + internal static let mauritius = L10n.tr("Localizable", "enum.browsing_country.name.mauritius", fallback: "Mauritius") + /// Mayotte + internal static let mayotte = L10n.tr("Localizable", "enum.browsing_country.name.mayotte", fallback: "Mayotte") + /// Mexico + internal static let mexico = L10n.tr("Localizable", "enum.browsing_country.name.mexico", fallback: "Mexico") + /// Micronesia + internal static let micronesia = L10n.tr("Localizable", "enum.browsing_country.name.micronesia", fallback: "Micronesia") + /// Moldova + internal static let moldova = L10n.tr("Localizable", "enum.browsing_country.name.moldova", fallback: "Moldova") + /// Monaco + internal static let monaco = L10n.tr("Localizable", "enum.browsing_country.name.monaco", fallback: "Monaco") + /// Mongolia + internal static let mongolia = L10n.tr("Localizable", "enum.browsing_country.name.mongolia", fallback: "Mongolia") + /// Montenegro + internal static let montenegro = L10n.tr("Localizable", "enum.browsing_country.name.montenegro", fallback: "Montenegro") + /// Montserrat + internal static let montserrat = L10n.tr("Localizable", "enum.browsing_country.name.montserrat", fallback: "Montserrat") + /// Morocco + internal static let morocco = L10n.tr("Localizable", "enum.browsing_country.name.morocco", fallback: "Morocco") + /// Mozambique + internal static let mozambique = L10n.tr("Localizable", "enum.browsing_country.name.mozambique", fallback: "Mozambique") + /// Myanmar + internal static let myanmar = L10n.tr("Localizable", "enum.browsing_country.name.myanmar", fallback: "Myanmar") + /// Namibia + internal static let namibia = L10n.tr("Localizable", "enum.browsing_country.name.namibia", fallback: "Namibia") + /// Nauru + internal static let nauru = L10n.tr("Localizable", "enum.browsing_country.name.nauru", fallback: "Nauru") + /// Nepal + internal static let nepal = L10n.tr("Localizable", "enum.browsing_country.name.nepal", fallback: "Nepal") + /// Netherlands + internal static let netherlands = L10n.tr("Localizable", "enum.browsing_country.name.netherlands", fallback: "Netherlands") + /// New Caledonia + internal static let newCaledonia = L10n.tr("Localizable", "enum.browsing_country.name.new_caledonia", fallback: "New Caledonia") + /// New Zealand + internal static let newZealand = L10n.tr("Localizable", "enum.browsing_country.name.new_zealand", fallback: "New Zealand") + /// Nicaragua + internal static let nicaragua = L10n.tr("Localizable", "enum.browsing_country.name.nicaragua", fallback: "Nicaragua") + /// Niger + internal static let niger = L10n.tr("Localizable", "enum.browsing_country.name.niger", fallback: "Niger") + /// Nigeria + internal static let nigeria = L10n.tr("Localizable", "enum.browsing_country.name.nigeria", fallback: "Nigeria") + /// Niue + internal static let niue = L10n.tr("Localizable", "enum.browsing_country.name.niue", fallback: "Niue") + /// Norfolk Island + internal static let norfolkIsland = L10n.tr("Localizable", "enum.browsing_country.name.norfolk_island", fallback: "Norfolk Island") + /// North Korea + internal static let northKorea = L10n.tr("Localizable", "enum.browsing_country.name.north_korea", fallback: "North Korea") + /// Northern Mariana Islands + internal static let northernMarianaIslands = L10n.tr("Localizable", "enum.browsing_country.name.northern_mariana_islands", fallback: "Northern Mariana Islands") + /// Norway + internal static let norway = L10n.tr("Localizable", "enum.browsing_country.name.norway", fallback: "Norway") + /// Oman + internal static let oman = L10n.tr("Localizable", "enum.browsing_country.name.oman", fallback: "Oman") + /// Pakistan + internal static let pakistan = L10n.tr("Localizable", "enum.browsing_country.name.pakistan", fallback: "Pakistan") + /// Palau + internal static let palau = L10n.tr("Localizable", "enum.browsing_country.name.palau", fallback: "Palau") + /// Palestinian Territory + internal static let palestinianTerritory = L10n.tr("Localizable", "enum.browsing_country.name.palestinian_territory", fallback: "Palestinian Territory") + /// Panama + internal static let panama = L10n.tr("Localizable", "enum.browsing_country.name.panama", fallback: "Panama") + /// Papua New Guinea + internal static let papuaNewGuinea = L10n.tr("Localizable", "enum.browsing_country.name.papua_new_guinea", fallback: "Papua New Guinea") + /// Paraguay + internal static let paraguay = L10n.tr("Localizable", "enum.browsing_country.name.paraguay", fallback: "Paraguay") + /// Peru + internal static let peru = L10n.tr("Localizable", "enum.browsing_country.name.peru", fallback: "Peru") + /// Philippines + internal static let philippines = L10n.tr("Localizable", "enum.browsing_country.name.philippines", fallback: "Philippines") + /// Pitcairn Islands + internal static let pitcairnIslands = L10n.tr("Localizable", "enum.browsing_country.name.pitcairn_islands", fallback: "Pitcairn Islands") + /// Poland + internal static let poland = L10n.tr("Localizable", "enum.browsing_country.name.poland", fallback: "Poland") + /// Portugal + internal static let portugal = L10n.tr("Localizable", "enum.browsing_country.name.portugal", fallback: "Portugal") + /// Puerto Rico + internal static let puertoRico = L10n.tr("Localizable", "enum.browsing_country.name.puerto_rico", fallback: "Puerto Rico") + /// Qatar + internal static let qatar = L10n.tr("Localizable", "enum.browsing_country.name.qatar", fallback: "Qatar") + /// Reunion + internal static let reunion = L10n.tr("Localizable", "enum.browsing_country.name.reunion", fallback: "Reunion") + /// Romania + internal static let romania = L10n.tr("Localizable", "enum.browsing_country.name.romania", fallback: "Romania") + /// Russian Federation + internal static let russianFederation = L10n.tr("Localizable", "enum.browsing_country.name.russian_federation", fallback: "Russian Federation") + /// Rwanda + internal static let rwanda = L10n.tr("Localizable", "enum.browsing_country.name.rwanda", fallback: "Rwanda") + /// Saint Barthelemy + internal static let saintBarthelemy = L10n.tr("Localizable", "enum.browsing_country.name.saint_barthelemy", fallback: "Saint Barthelemy") + /// Saint Helena + internal static let saintHelena = L10n.tr("Localizable", "enum.browsing_country.name.saint_helena", fallback: "Saint Helena") + /// Saint Kitts and Nevis + internal static let saintKittsAndNevis = L10n.tr("Localizable", "enum.browsing_country.name.saint_kitts_and_nevis", fallback: "Saint Kitts and Nevis") + /// Saint Lucia + internal static let saintLucia = L10n.tr("Localizable", "enum.browsing_country.name.saint_lucia", fallback: "Saint Lucia") + /// Saint Martin + internal static let saintMartin = L10n.tr("Localizable", "enum.browsing_country.name.saint_martin", fallback: "Saint Martin") + /// Saint Pierre and Miquelon + internal static let saintPierreAndMiquelon = L10n.tr("Localizable", "enum.browsing_country.name.saint_pierre_and_miquelon", fallback: "Saint Pierre and Miquelon") + /// Saint Vincent and the Grenadines + internal static let saintVincentAndTheGrenadines = L10n.tr("Localizable", "enum.browsing_country.name.saint_vincent_and_the_grenadines", fallback: "Saint Vincent and the Grenadines") + /// Samoa + internal static let samoa = L10n.tr("Localizable", "enum.browsing_country.name.samoa", fallback: "Samoa") + /// San Marino + internal static let sanMarino = L10n.tr("Localizable", "enum.browsing_country.name.san_marino", fallback: "San Marino") + /// Sao Tome and Principe + internal static let saoTomeAndPrincipe = L10n.tr("Localizable", "enum.browsing_country.name.sao_tome_and_principe", fallback: "Sao Tome and Principe") + /// Saudi Arabia + internal static let saudiArabia = L10n.tr("Localizable", "enum.browsing_country.name.saudi_arabia", fallback: "Saudi Arabia") + /// Senegal + internal static let senegal = L10n.tr("Localizable", "enum.browsing_country.name.senegal", fallback: "Senegal") + /// Serbia + internal static let serbia = L10n.tr("Localizable", "enum.browsing_country.name.serbia", fallback: "Serbia") + /// Seychelles + internal static let seychelles = L10n.tr("Localizable", "enum.browsing_country.name.seychelles", fallback: "Seychelles") + /// Sierra Leone + internal static let sierraLeone = L10n.tr("Localizable", "enum.browsing_country.name.sierra_leone", fallback: "Sierra Leone") + /// Singapore + internal static let singapore = L10n.tr("Localizable", "enum.browsing_country.name.singapore", fallback: "Singapore") + /// Sint Maarten + internal static let sintMaarten = L10n.tr("Localizable", "enum.browsing_country.name.sint_maarten", fallback: "Sint Maarten") + /// Slovakia + internal static let slovakia = L10n.tr("Localizable", "enum.browsing_country.name.slovakia", fallback: "Slovakia") + /// Slovenia + internal static let slovenia = L10n.tr("Localizable", "enum.browsing_country.name.slovenia", fallback: "Slovenia") + /// Solomon Islands + internal static let solomonIslands = L10n.tr("Localizable", "enum.browsing_country.name.solomon_islands", fallback: "Solomon Islands") + /// Somalia + internal static let somalia = L10n.tr("Localizable", "enum.browsing_country.name.somalia", fallback: "Somalia") + /// South Africa + internal static let southAfrica = L10n.tr("Localizable", "enum.browsing_country.name.south_africa", fallback: "South Africa") + /// South Georgia and the South Sandwich Islands + internal static let southGeorgiaAndTheSouthSandwichIslands = L10n.tr("Localizable", "enum.browsing_country.name.south_georgia_and_the_south_sandwich_islands", fallback: "South Georgia and the South Sandwich Islands") + /// South Korea + internal static let southKorea = L10n.tr("Localizable", "enum.browsing_country.name.south_korea", fallback: "South Korea") + /// South Sudan + internal static let southSudan = L10n.tr("Localizable", "enum.browsing_country.name.south_sudan", fallback: "South Sudan") + /// Spain + internal static let spain = L10n.tr("Localizable", "enum.browsing_country.name.spain", fallback: "Spain") + /// Sri Lanka + internal static let sriLanka = L10n.tr("Localizable", "enum.browsing_country.name.sri_lanka", fallback: "Sri Lanka") + /// Sudan + internal static let sudan = L10n.tr("Localizable", "enum.browsing_country.name.sudan", fallback: "Sudan") + /// Suriname + internal static let suriname = L10n.tr("Localizable", "enum.browsing_country.name.suriname", fallback: "Suriname") + /// Svalbard and Jan Mayen + internal static let svalbardAndJanMayen = L10n.tr("Localizable", "enum.browsing_country.name.svalbard_and_jan_mayen", fallback: "Svalbard and Jan Mayen") + /// Swaziland + internal static let swaziland = L10n.tr("Localizable", "enum.browsing_country.name.swaziland", fallback: "Swaziland") + /// Sweden + internal static let sweden = L10n.tr("Localizable", "enum.browsing_country.name.sweden", fallback: "Sweden") + /// Switzerland + internal static let switzerland = L10n.tr("Localizable", "enum.browsing_country.name.switzerland", fallback: "Switzerland") + /// Syrian Arab Republic + internal static let syrianArabRepublic = L10n.tr("Localizable", "enum.browsing_country.name.syrian_arab_republic", fallback: "Syrian Arab Republic") + /// Taiwan + internal static let taiwan = L10n.tr("Localizable", "enum.browsing_country.name.taiwan", fallback: "Taiwan") + /// Tajikistan + internal static let tajikistan = L10n.tr("Localizable", "enum.browsing_country.name.tajikistan", fallback: "Tajikistan") + /// Tanzania + internal static let tanzania = L10n.tr("Localizable", "enum.browsing_country.name.tanzania", fallback: "Tanzania") + /// Thailand + internal static let thailand = L10n.tr("Localizable", "enum.browsing_country.name.thailand", fallback: "Thailand") + /// The Democratic Republic of the Congo + internal static let theDemocraticRepublicOfTheCongo = L10n.tr("Localizable", "enum.browsing_country.name.the_democratic_republic_of_the_congo", fallback: "The Democratic Republic of the Congo") + /// Timor-Leste + internal static let timorLeste = L10n.tr("Localizable", "enum.browsing_country.name.timor_leste", fallback: "Timor-Leste") + /// Togo + internal static let togo = L10n.tr("Localizable", "enum.browsing_country.name.togo", fallback: "Togo") + /// Tokelau + internal static let tokelau = L10n.tr("Localizable", "enum.browsing_country.name.tokelau", fallback: "Tokelau") + /// Tonga + internal static let tonga = L10n.tr("Localizable", "enum.browsing_country.name.tonga", fallback: "Tonga") + /// Trinidad and Tobago + internal static let trinidadAndTobago = L10n.tr("Localizable", "enum.browsing_country.name.trinidad_and_tobago", fallback: "Trinidad and Tobago") + /// Tunisia + internal static let tunisia = L10n.tr("Localizable", "enum.browsing_country.name.tunisia", fallback: "Tunisia") + /// Turkey + internal static let turkey = L10n.tr("Localizable", "enum.browsing_country.name.turkey", fallback: "Turkey") + /// Turkmenistan + internal static let turkmenistan = L10n.tr("Localizable", "enum.browsing_country.name.turkmenistan", fallback: "Turkmenistan") + /// Turks and Caicos Islands + internal static let turksAndCaicosIslands = L10n.tr("Localizable", "enum.browsing_country.name.turks_and_caicos_islands", fallback: "Turks and Caicos Islands") + /// Tuvalu + internal static let tuvalu = L10n.tr("Localizable", "enum.browsing_country.name.tuvalu", fallback: "Tuvalu") + /// Uganda + internal static let uganda = L10n.tr("Localizable", "enum.browsing_country.name.uganda", fallback: "Uganda") + /// Ukraine + internal static let ukraine = L10n.tr("Localizable", "enum.browsing_country.name.ukraine", fallback: "Ukraine") + /// United Arab Emirates + internal static let unitedArabEmirates = L10n.tr("Localizable", "enum.browsing_country.name.united_arab_emirates", fallback: "United Arab Emirates") + /// United Kingdom + internal static let unitedKingdom = L10n.tr("Localizable", "enum.browsing_country.name.united_kingdom", fallback: "United Kingdom") + /// United States + internal static let unitedStates = L10n.tr("Localizable", "enum.browsing_country.name.united_states", fallback: "United States") + /// United States Minor Outlying Islands + internal static let unitedStatesMinorOutlyingIslands = L10n.tr("Localizable", "enum.browsing_country.name.united_states_minor_outlying_islands", fallback: "United States Minor Outlying Islands") + /// Uruguay + internal static let uruguay = L10n.tr("Localizable", "enum.browsing_country.name.uruguay", fallback: "Uruguay") + /// Uzbekistan + internal static let uzbekistan = L10n.tr("Localizable", "enum.browsing_country.name.uzbekistan", fallback: "Uzbekistan") + /// Vanuatu + internal static let vanuatu = L10n.tr("Localizable", "enum.browsing_country.name.vanuatu", fallback: "Vanuatu") + /// Vatican City State + internal static let vaticanCityState = L10n.tr("Localizable", "enum.browsing_country.name.vatican_city_state", fallback: "Vatican City State") + /// Venezuela + internal static let venezuela = L10n.tr("Localizable", "enum.browsing_country.name.venezuela", fallback: "Venezuela") + /// Vietnam + internal static let vietnam = L10n.tr("Localizable", "enum.browsing_country.name.vietnam", fallback: "Vietnam") + /// British Virgin Islands + internal static let virginIslandsBritish = L10n.tr("Localizable", "enum.browsing_country.name.virgin_islands_british", fallback: "British Virgin Islands") + /// U.S. Virgin Islands + internal static let virginIslandsUS = L10n.tr("Localizable", "enum.browsing_country.name.virgin_islands_US", fallback: "U.S. Virgin Islands") + /// Wallis and Futuna + internal static let wallisAndFutuna = L10n.tr("Localizable", "enum.browsing_country.name.wallis_and_futuna", fallback: "Wallis and Futuna") + /// Western Sahara + internal static let westernSahara = L10n.tr("Localizable", "enum.browsing_country.name.western_sahara", fallback: "Western Sahara") + /// Yemen + internal static let yemen = L10n.tr("Localizable", "enum.browsing_country.name.yemen", fallback: "Yemen") + /// Zambia + internal static let zambia = L10n.tr("Localizable", "enum.browsing_country.name.zambia", fallback: "Zambia") + /// Zimbabwe + internal static let zimbabwe = L10n.tr("Localizable", "enum.browsing_country.name.zimbabwe", fallback: "Zimbabwe") + } + } + internal enum Category { + internal enum Value { + /// Artist CG + internal static let artistCG = L10n.tr("Localizable", "enum.category.value.artist_CG", fallback: "Artist CG") + /// Asian Porn + internal static let asianPorn = L10n.tr("Localizable", "enum.category.value.asian_porn", fallback: "Asian Porn") + /// Cosplay + internal static let cosplay = L10n.tr("Localizable", "enum.category.value.cosplay", fallback: "Cosplay") + /// Doujinshi + internal static let doujinshi = L10n.tr("Localizable", "enum.category.value.doujinshi", fallback: "Doujinshi") + /// Game CG + internal static let gameCG = L10n.tr("Localizable", "enum.category.value.game_CG", fallback: "Game CG") + /// Image Set + internal static let imageSet = L10n.tr("Localizable", "enum.category.value.image_set", fallback: "Image Set") + /// Manga + internal static let manga = L10n.tr("Localizable", "enum.category.value.manga", fallback: "Manga") + /// Misc + internal static let misc = L10n.tr("Localizable", "enum.category.value.misc", fallback: "Misc") + /// Non-H + internal static let nonH = L10n.tr("Localizable", "enum.category.value.non_h", fallback: "Non-H") + /// Private + internal static let `private` = L10n.tr("Localizable", "enum.category.value.private", fallback: "Private") + /// Western + internal static let western = L10n.tr("Localizable", "enum.category.value.western", fallback: "Western") + } + } + internal enum EhSetting { + internal enum ArchiverBehavior { + internal enum Value { + /// Auto Select Original, Auto Start + internal static let autoSelectOriginalAutoStart = L10n.tr("Localizable", "enum.eh_setting.archiver_behavior.value.auto_select_original_auto_start", fallback: "Auto Select Original, Auto Start") + /// Auto Select Original, Manual Start + internal static let autoSelectOriginalManualStart = L10n.tr("Localizable", "enum.eh_setting.archiver_behavior.value.auto_select_original_manual_start", fallback: "Auto Select Original, Manual Start") + /// Auto Select Resample, Auto Start + internal static let autoSelectResampleAutoStart = L10n.tr("Localizable", "enum.eh_setting.archiver_behavior.value.auto_select_resample_auto_start", fallback: "Auto Select Resample, Auto Start") + /// Auto Select Resample, Manual Start + internal static let autoSelectResampleManualStart = L10n.tr("Localizable", "enum.eh_setting.archiver_behavior.value.auto_select_resample_manual_start", fallback: "Auto Select Resample, Manual Start") + /// Manual Select, Auto Start + internal static let manualSelectAutoStart = L10n.tr("Localizable", "enum.eh_setting.archiver_behavior.value.manual_select_auto_start", fallback: "Manual Select, Auto Start") + /// Manual Select, Manual Start (Default) + internal static let manualSelectManualStart = L10n.tr("Localizable", "enum.eh_setting.archiver_behavior.value.manual_select_manual_start", fallback: "Manual Select, Manual Start (Default)") + } + } + internal enum CommentsSortOrder { + internal enum Value { + /// By highest score + internal static let highestScore = L10n.tr("Localizable", "enum.eh_setting.comments_sort_order.value.highest_score", fallback: "By highest score") + /// Oldest comments first + internal static let oldest = L10n.tr("Localizable", "enum.eh_setting.comments_sort_order.value.oldest", fallback: "Oldest comments first") + /// Recent comments first + internal static let recent = L10n.tr("Localizable", "enum.eh_setting.comments_sort_order.value.recent", fallback: "Recent comments first") + } + } + internal enum CommentsVotesShowTiming { + internal enum Value { + /// Always + internal static let always = L10n.tr("Localizable", "enum.eh_setting.comments_votes_show_timing.value.always", fallback: "Always") + /// On score hover or click + internal static let onHoverOrClick = L10n.tr("Localizable", "enum.eh_setting.comments_votes_show_timing.value.on_hover_or_click", fallback: "On score hover or click") + } + } + internal enum DisplayMode { + internal enum Value { + /// Compact + internal static let compact = L10n.tr("Localizable", "enum.eh_setting.display_mode.value.compact", fallback: "Compact") + /// Extended + internal static let extended = L10n.tr("Localizable", "enum.eh_setting.display_mode.value.extended", fallback: "Extended") + /// Minimal + internal static let minimal = L10n.tr("Localizable", "enum.eh_setting.display_mode.value.minimal", fallback: "Minimal") + /// Minimal+ + internal static let minimalPlus = L10n.tr("Localizable", "enum.eh_setting.display_mode.value.minimalPlus", fallback: "Minimal+") + /// Thumbnail + internal static let thumbnail = L10n.tr("Localizable", "enum.eh_setting.display_mode.value.thumbnail", fallback: "Thumbnail") + } + } + internal enum ExcludedLanguagesCategory { + internal enum Value { + /// Original + internal static let original = L10n.tr("Localizable", "enum.eh_setting.excluded_languages_category.value.original", fallback: "Original") + /// Rewrite + internal static let rewrite = L10n.tr("Localizable", "enum.eh_setting.excluded_languages_category.value.rewrite", fallback: "Rewrite") + /// Translated + internal static let translated = L10n.tr("Localizable", "enum.eh_setting.excluded_languages_category.value.translated", fallback: "Translated") + } + } + internal enum FavoritesSortOrder { + internal enum Value { + /// By favorited time + internal static let favoritedTime = L10n.tr("Localizable", "enum.eh_setting.favorites_sort_order.value.favorited_time", fallback: "By favorited time") + /// By last gallery update time + internal static let lastUpdateTime = L10n.tr("Localizable", "enum.eh_setting.favorites_sort_order.value.last_update_time", fallback: "By last gallery update time") + } + } + internal enum GalleryName { + internal enum Value { + /// Default Title + internal static let `default` = L10n.tr("Localizable", "enum.eh_setting.gallery_name.value.default", fallback: "Default Title") + /// Japanese Title (if available) + internal static let japanese = L10n.tr("Localizable", "enum.eh_setting.gallery_name.value.japanese", fallback: "Japanese Title (if available)") + } + } + internal enum ImageResolution { + internal enum Value { + /// Auto + internal static let auto = L10n.tr("Localizable", "enum.eh_setting.image_resolution.value.auto", fallback: "Auto") + } + } + internal enum LoadThroughHathSetting { + internal enum Description { + /// Recommended. + internal static let anyClient = L10n.tr("Localizable", "enum.eh_setting.load_through_hath_setting.description.any_client", fallback: "Recommended.") + /// Can be slower. Enable if behind firewall/proxy that blocks outgoing non-standard ports. + internal static let defaultPortOnly = L10n.tr("Localizable", "enum.eh_setting.load_through_hath_setting.description.default_port_only", fallback: "Can be slower. Enable if behind firewall/proxy that blocks outgoing non-standard ports.") + /// Donator only. May not work by default in modern browsers. Recommended for legacy/outdated browsers only. + internal static let legacyNo = L10n.tr("Localizable", "enum.eh_setting.load_through_hath_setting.description.legacy_no", fallback: "Donator only. May not work by default in modern browsers. Recommended for legacy/outdated browsers only.") + /// Donator only. You will not be able to browse as many pages. Recommended only if having severe problems. + internal static let modernNo = L10n.tr("Localizable", "enum.eh_setting.load_through_hath_setting.description.modern_no", fallback: "Donator only. You will not be able to browse as many pages. Recommended only if having severe problems.") + } + internal enum Value { + /// Any client + internal static let anyClient = L10n.tr("Localizable", "enum.eh_setting.load_through_hath_setting.value.any_client", fallback: "Any client") + /// Default port clients only + internal static let defaultPortOnly = L10n.tr("Localizable", "enum.eh_setting.load_through_hath_setting.value.default_port_only", fallback: "Default port clients only") + /// No [Legacy/HTTP] + internal static let legacyNo = L10n.tr("Localizable", "enum.eh_setting.load_through_hath_setting.value.legacy_no", fallback: "No [Legacy/HTTP]") + /// No [Modern/HTTPS] + internal static let modernNo = L10n.tr("Localizable", "enum.eh_setting.load_through_hath_setting.value.modern_no", fallback: "No [Modern/HTTPS]") + } + } + internal enum MultiplePageViewerStyle { + internal enum Value { + /// Align center, always scale + internal static let alignCenterAlwaysScale = L10n.tr("Localizable", "enum.eh_setting.multiple_page_viewer_style.value.align_center_always_scale", fallback: "Align center, always scale") + /// Align center, scale if overwidth + internal static let alignCenterScaleIfOverWidth = L10n.tr("Localizable", "enum.eh_setting.multiple_page_viewer_style.value.align_center_scale_if_over_width", fallback: "Align center, scale if overwidth") + /// Align left, scale if overwidth + internal static let alignLeftScaleIfOverWidth = L10n.tr("Localizable", "enum.eh_setting.multiple_page_viewer_style.value.align_left_scale_if_over_width", fallback: "Align left, scale if overwidth") + } + } + internal enum TagsSortOrder { + internal enum Value { + /// Alphabetical + internal static let alphabetical = L10n.tr("Localizable", "enum.eh_setting.tags_sort_order.value.alphabetical", fallback: "Alphabetical") + /// By tag power + internal static let tagPower = L10n.tr("Localizable", "enum.eh_setting.tags_sort_order.value.tag_power", fallback: "By tag power") + } + } + internal enum ThumbnailLoadTiming { + internal enum Description { + /// Pages load faster, but there may be a slight delay before a thumb appears. + internal static let onMouseOver = L10n.tr("Localizable", "enum.eh_setting.thumbnail_load_timing.description.on_mouse_over", fallback: "Pages load faster, but there may be a slight delay before a thumb appears.") + /// Pages take longer to load, but there is no delay for loading a thumb after the page has loaded. + internal static let onPageLoad = L10n.tr("Localizable", "enum.eh_setting.thumbnail_load_timing.description.on_page_load", fallback: "Pages take longer to load, but there is no delay for loading a thumb after the page has loaded.") + } + internal enum Value { + /// On mouse-over + internal static let onMouseOver = L10n.tr("Localizable", "enum.eh_setting.thumbnail_load_timing.value.on_mouse_over", fallback: "On mouse-over") + /// On page load + internal static let onPageLoad = L10n.tr("Localizable", "enum.eh_setting.thumbnail_load_timing.value.on_page_load", fallback: "On page load") + } + } + internal enum ThumbnailSize { + internal enum Value { + /// Large + internal static let large = L10n.tr("Localizable", "enum.eh_setting.thumbnail_size.value.large", fallback: "Large") + /// Normal + internal static let normal = L10n.tr("Localizable", "enum.eh_setting.thumbnail_size.value.normal", fallback: "Normal") + } + } + } + internal enum FilterRange { + internal enum Value { + /// Global + internal static let global = L10n.tr("Localizable", "enum.filter_range.value.global", fallback: "Global") + /// Search + internal static let search = L10n.tr("Localizable", "enum.filter_range.value.search", fallback: "Search") + /// Watched + internal static let watched = L10n.tr("Localizable", "enum.filter_range.value.watched", fallback: "Watched") + } + } + internal enum GalleryVisibility { + internal enum Value { + /// No (%@) + internal static func no(_ p1: Any) -> String { + return L10n.tr("Localizable", "enum.gallery_visibility.value.no", String(describing: p1), fallback: "No (%@)") + } + /// Yes + internal static let yes = L10n.tr("Localizable", "enum.gallery_visibility.value.yes", fallback: "Yes") + internal enum No { + internal enum Reason { + /// Expunged + internal static let expunged = L10n.tr("Localizable", "enum.gallery_visibility.value.no.reason.expunged", fallback: "Expunged") + } + } + } + } + internal enum HomeMiscGridType { + internal enum Title { + /// History + internal static let history = L10n.tr("Localizable", "enum.home_misc_grid_type.title.history", fallback: "History") + /// Popular + internal static let popular = L10n.tr("Localizable", "enum.home_misc_grid_type.title.popular", fallback: "Popular") + /// Watched + internal static let watched = L10n.tr("Localizable", "enum.home_misc_grid_type.title.watched", fallback: "Watched") + } + } + internal enum Language { + internal enum Value { + /// Afrikaans + internal static let afrikaans = L10n.tr("Localizable", "enum.language.value.afrikaans", fallback: "Afrikaans") + /// Albanian + internal static let albanian = L10n.tr("Localizable", "enum.language.value.albanian", fallback: "Albanian") + /// Arabic + internal static let arabic = L10n.tr("Localizable", "enum.language.value.arabic", fallback: "Arabic") + /// Bengali + internal static let bengali = L10n.tr("Localizable", "enum.language.value.bengali", fallback: "Bengali") + /// Bosnian + internal static let bosnian = L10n.tr("Localizable", "enum.language.value.bosnian", fallback: "Bosnian") + /// Bulgarian + internal static let bulgarian = L10n.tr("Localizable", "enum.language.value.bulgarian", fallback: "Bulgarian") + /// Burmese + internal static let burmese = L10n.tr("Localizable", "enum.language.value.burmese", fallback: "Burmese") + /// Catalan + internal static let catalan = L10n.tr("Localizable", "enum.language.value.catalan", fallback: "Catalan") + /// Cebuano + internal static let cebuano = L10n.tr("Localizable", "enum.language.value.cebuano", fallback: "Cebuano") + /// Chinese + internal static let chinese = L10n.tr("Localizable", "enum.language.value.chinese", fallback: "Chinese") + /// Croatian + internal static let croatian = L10n.tr("Localizable", "enum.language.value.croatian", fallback: "Croatian") + /// Czech + internal static let czech = L10n.tr("Localizable", "enum.language.value.czech", fallback: "Czech") + /// Danish + internal static let danish = L10n.tr("Localizable", "enum.language.value.danish", fallback: "Danish") + /// Dutch + internal static let dutch = L10n.tr("Localizable", "enum.language.value.dutch", fallback: "Dutch") + /// English + internal static let english = L10n.tr("Localizable", "enum.language.value.english", fallback: "English") + /// Esperanto + internal static let esperanto = L10n.tr("Localizable", "enum.language.value.esperanto", fallback: "Esperanto") + /// Estonian + internal static let estonian = L10n.tr("Localizable", "enum.language.value.estonian", fallback: "Estonian") + /// Finnish + internal static let finnish = L10n.tr("Localizable", "enum.language.value.finnish", fallback: "Finnish") + /// French + internal static let french = L10n.tr("Localizable", "enum.language.value.french", fallback: "French") + /// Georgian + internal static let georgian = L10n.tr("Localizable", "enum.language.value.georgian", fallback: "Georgian") + /// German + internal static let german = L10n.tr("Localizable", "enum.language.value.german", fallback: "German") + /// Greek + internal static let greek = L10n.tr("Localizable", "enum.language.value.greek", fallback: "Greek") + /// Hebrew + internal static let hebrew = L10n.tr("Localizable", "enum.language.value.hebrew", fallback: "Hebrew") + /// Hindi + internal static let hindi = L10n.tr("Localizable", "enum.language.value.hindi", fallback: "Hindi") + /// Hmong + internal static let hmong = L10n.tr("Localizable", "enum.language.value.hmong", fallback: "Hmong") + /// Hungarian + internal static let hungarian = L10n.tr("Localizable", "enum.language.value.hungarian", fallback: "Hungarian") + /// Indonesian + internal static let indonesian = L10n.tr("Localizable", "enum.language.value.indonesian", fallback: "Indonesian") + /// N/A + internal static let invalid = L10n.tr("Localizable", "enum.language.value.invalid", fallback: "N/A") + /// Italian + internal static let italian = L10n.tr("Localizable", "enum.language.value.italian", fallback: "Italian") + /// Japanese + internal static let japanese = L10n.tr("Localizable", "enum.language.value.japanese", fallback: "Japanese") + /// Kazakh + internal static let kazakh = L10n.tr("Localizable", "enum.language.value.kazakh", fallback: "Kazakh") + /// Khmer + internal static let khmer = L10n.tr("Localizable", "enum.language.value.khmer", fallback: "Khmer") + /// Korean + internal static let korean = L10n.tr("Localizable", "enum.language.value.korean", fallback: "Korean") + /// Kurdish + internal static let kurdish = L10n.tr("Localizable", "enum.language.value.kurdish", fallback: "Kurdish") + /// Lao + internal static let lao = L10n.tr("Localizable", "enum.language.value.lao", fallback: "Lao") + /// Latin + internal static let latin = L10n.tr("Localizable", "enum.language.value.latin", fallback: "Latin") + /// Mongolian + internal static let mongolian = L10n.tr("Localizable", "enum.language.value.mongolian", fallback: "Mongolian") + /// Ndebele + internal static let ndebele = L10n.tr("Localizable", "enum.language.value.ndebele", fallback: "Ndebele") + /// Nepali + internal static let nepali = L10n.tr("Localizable", "enum.language.value.nepali", fallback: "Nepali") + /// Norwegian + internal static let norwegian = L10n.tr("Localizable", "enum.language.value.norwegian", fallback: "Norwegian") + /// Oromo + internal static let oromo = L10n.tr("Localizable", "enum.language.value.oromo", fallback: "Oromo") + /// Other + internal static let other = L10n.tr("Localizable", "enum.language.value.other", fallback: "Other") + /// Pashto + internal static let pashto = L10n.tr("Localizable", "enum.language.value.pashto", fallback: "Pashto") + /// Persian + internal static let persian = L10n.tr("Localizable", "enum.language.value.persian", fallback: "Persian") + /// Polish + internal static let polish = L10n.tr("Localizable", "enum.language.value.polish", fallback: "Polish") + /// Portuguese + internal static let portuguese = L10n.tr("Localizable", "enum.language.value.portuguese", fallback: "Portuguese") + /// Punjabi + internal static let punjabi = L10n.tr("Localizable", "enum.language.value.punjabi", fallback: "Punjabi") + /// Romanian + internal static let romanian = L10n.tr("Localizable", "enum.language.value.romanian", fallback: "Romanian") + /// Russian + internal static let russian = L10n.tr("Localizable", "enum.language.value.russian", fallback: "Russian") + /// Sango + internal static let sango = L10n.tr("Localizable", "enum.language.value.sango", fallback: "Sango") + /// Serbian + internal static let serbian = L10n.tr("Localizable", "enum.language.value.serbian", fallback: "Serbian") + /// Shona + internal static let shona = L10n.tr("Localizable", "enum.language.value.shona", fallback: "Shona") + /// Slovak + internal static let slovak = L10n.tr("Localizable", "enum.language.value.slovak", fallback: "Slovak") + /// Slovenian + internal static let slovenian = L10n.tr("Localizable", "enum.language.value.slovenian", fallback: "Slovenian") + /// Somali + internal static let somali = L10n.tr("Localizable", "enum.language.value.somali", fallback: "Somali") + /// Spanish + internal static let spanish = L10n.tr("Localizable", "enum.language.value.spanish", fallback: "Spanish") + /// Swahili + internal static let swahili = L10n.tr("Localizable", "enum.language.value.swahili", fallback: "Swahili") + /// Swedish + internal static let swedish = L10n.tr("Localizable", "enum.language.value.swedish", fallback: "Swedish") + /// Tagalog + internal static let tagalog = L10n.tr("Localizable", "enum.language.value.tagalog", fallback: "Tagalog") + /// Thai + internal static let thai = L10n.tr("Localizable", "enum.language.value.thai", fallback: "Thai") + /// Tigrinya + internal static let tigrinya = L10n.tr("Localizable", "enum.language.value.tigrinya", fallback: "Tigrinya") + /// Turkish + internal static let turkish = L10n.tr("Localizable", "enum.language.value.turkish", fallback: "Turkish") + /// Ukrainian + internal static let ukrainian = L10n.tr("Localizable", "enum.language.value.ukrainian", fallback: "Ukrainian") + /// Urdu + internal static let urdu = L10n.tr("Localizable", "enum.language.value.urdu", fallback: "Urdu") + /// Vietnamese + internal static let vietnamese = L10n.tr("Localizable", "enum.language.value.vietnamese", fallback: "Vietnamese") + /// Zulu + internal static let zulu = L10n.tr("Localizable", "enum.language.value.zulu", fallback: "Zulu") + } + } + internal enum ListDisplayMode { + internal enum Value { + /// Detail + internal static let detail = L10n.tr("Localizable", "enum.list_display_mode.value.detail", fallback: "Detail") + /// Thumbnail + internal static let thumbnail = L10n.tr("Localizable", "enum.list_display_mode.value.thumbnail", fallback: "Thumbnail") + } + } + internal enum PreferredColorScheme { + internal enum Value { + /// Automatic + internal static let automatic = L10n.tr("Localizable", "enum.preferred_color_scheme.value.automatic", fallback: "Automatic") + /// Dark + internal static let dark = L10n.tr("Localizable", "enum.preferred_color_scheme.value.dark", fallback: "Dark") + /// Light + internal static let light = L10n.tr("Localizable", "enum.preferred_color_scheme.value.light", fallback: "Light") + } + } + internal enum ReadingDirection { + internal enum Value { + /// Left-to-right + internal static let leftToRight = L10n.tr("Localizable", "enum.reading_direction.value.left_to_right", fallback: "Left-to-right") + /// Right-to-left + internal static let rightToLeft = L10n.tr("Localizable", "enum.reading_direction.value.right_to_left", fallback: "Right-to-left") + /// Vertical + internal static let vertical = L10n.tr("Localizable", "enum.reading_direction.value.vertical", fallback: "Vertical") + } + } + internal enum SettingStateRoute { + internal enum Value { + /// About EhPanda + internal static let about = L10n.tr("Localizable", "enum.setting_state_route.value.about", fallback: "About EhPanda") + /// Account + internal static let account = L10n.tr("Localizable", "enum.setting_state_route.value.account", fallback: "Account") + /// Appearance + internal static let appearance = L10n.tr("Localizable", "enum.setting_state_route.value.appearance", fallback: "Appearance") + /// General + internal static let general = L10n.tr("Localizable", "enum.setting_state_route.value.general", fallback: "General") + /// Laboratory + internal static let laboratory = L10n.tr("Localizable", "enum.setting_state_route.value.laboratory", fallback: "Laboratory") + /// Reading + internal static let reading = L10n.tr("Localizable", "enum.setting_state_route.value.reading", fallback: "Reading") + } + } + internal enum TagNamespace { + internal enum Value { + /// Artist + internal static let artist = L10n.tr("Localizable", "enum.tag_namespace.value.artist", fallback: "Artist") + /// Character + internal static let character = L10n.tr("Localizable", "enum.tag_namespace.value.character", fallback: "Character") + /// Cosplayer + internal static let cosplayer = L10n.tr("Localizable", "enum.tag_namespace.value.cosplayer", fallback: "Cosplayer") + /// Female + internal static let female = L10n.tr("Localizable", "enum.tag_namespace.value.female", fallback: "Female") + /// Group + internal static let group = L10n.tr("Localizable", "enum.tag_namespace.value.group", fallback: "Group") + /// Language + internal static let language = L10n.tr("Localizable", "enum.tag_namespace.value.language", fallback: "Language") + /// Male + internal static let male = L10n.tr("Localizable", "enum.tag_namespace.value.male", fallback: "Male") + /// Mixed + internal static let mixed = L10n.tr("Localizable", "enum.tag_namespace.value.mixed", fallback: "Mixed") + /// Other + internal static let other = L10n.tr("Localizable", "enum.tag_namespace.value.other", fallback: "Other") + /// Parody + internal static let parody = L10n.tr("Localizable", "enum.tag_namespace.value.parody", fallback: "Parody") + /// Reclass + internal static let reclass = L10n.tr("Localizable", "enum.tag_namespace.value.reclass", fallback: "Reclass") + /// Temp + internal static let temp = L10n.tr("Localizable", "enum.tag_namespace.value.temp", fallback: "Temp") + } + } + internal enum ToplistsType { + internal enum Value { + /// All time + internal static let allTime = L10n.tr("Localizable", "enum.toplists_type.value.all_time", fallback: "All time") + /// Past month + internal static let pastMonth = L10n.tr("Localizable", "enum.toplists_type.value.past_month", fallback: "Past month") + /// Past year + internal static let pastYear = L10n.tr("Localizable", "enum.toplists_type.value.past_year", fallback: "Past year") + /// Yesterday + internal static let yesterday = L10n.tr("Localizable", "enum.toplists_type.value.yesterday", fallback: "Yesterday") + } + } + } + internal enum ErrorView { + internal enum Button { + /// Drop the database + internal static let dropDatabase = L10n.tr("Localizable", "error_view.button.drop_database", fallback: "Drop the database") + /// Retry + internal static let retry = L10n.tr("Localizable", "error_view.button.retry", fallback: "Retry") + } + internal enum Title { + /// This gallery is unavailable due to a copyright claim by %@. Sorry about that. + internal static func copyrightClaim(_ p1: Any) -> String { + return L10n.tr("Localizable", "error_view.title.copyright_claim", String(describing: p1), fallback: "This gallery is unavailable due to a copyright claim by %@. Sorry about that.") + } + /// The database is corrupted. + /// Please submit an issue on GitHub. + internal static let databaseCorrupted = L10n.tr("Localizable", "error_view.title.database_corrupted", fallback: "The database is corrupted.\nPlease submit an issue on GitHub.") + /// This gallery has been removed or is unavailable. + internal static let galleryUnavailable = L10n.tr("Localizable", "error_view.title.gallery_unavailable", fallback: "This gallery has been removed or is unavailable.") + /// Your IP address has been temporarily banned for excessive pageloads which indicates that you are using automated mirroring / harvesting software. The ban expires in %@. + internal static func ipBanned(_ p1: Any) -> String { + return L10n.tr("Localizable", "error_view.title.ip_banned", String(describing: p1), fallback: "Your IP address has been temporarily banned for excessive pageloads which indicates that you are using automated mirroring / harvesting software. The ban expires in %@.") + } + /// A network error occurred. + internal static let network = L10n.tr("Localizable", "error_view.title.network", fallback: "A network error occurred.") + /// There seems to be nothing here. + internal static let notFound = L10n.tr("Localizable", "error_view.title.not_found", fallback: "There seems to be nothing here.") + /// A parsing error occurred. + internal static let parsing = L10n.tr("Localizable", "error_view.title.parsing", fallback: "A parsing error occurred.") + /// Please try again later. + internal static let tryLater = L10n.tr("Localizable", "error_view.title.try_later", fallback: "Please try again later.") + /// An unknown error occurred. + internal static let unknown = L10n.tr("Localizable", "error_view.title.unknown", fallback: "An unknown error occurred.") + } + } + internal enum FavoritesView { + internal enum Title { + /// Favorites + internal static let favorites = L10n.tr("Localizable", "favorites_view.title.favorites", fallback: "Favorites") + } + } + internal enum FiltersView { + internal enum Button { + /// Reset filters + internal static let resetFilters = L10n.tr("Localizable", "filters_view.button.reset_filters", fallback: "Reset filters") + } + internal enum Section { + internal enum Title { + /// Advanced + internal static let advanced = L10n.tr("Localizable", "filters_view.section.title.advanced", fallback: "Advanced") + /// Default filter + internal static let defaultFilter = L10n.tr("Localizable", "filters_view.section.title.default_filter", fallback: "Default filter") + } + } + internal enum Title { + /// Advanced settings + internal static let advancedSettings = L10n.tr("Localizable", "filters_view.title.advanced_settings", fallback: "Advanced settings") + /// Disable language filter + internal static let disableLanguageFilter = L10n.tr("Localizable", "filters_view.title.disable_language_filter", fallback: "Disable language filter") + /// Disable tags filter + internal static let disableTagsFilter = L10n.tr("Localizable", "filters_view.title.disable_tags_filter", fallback: "Disable tags filter") + /// Disable uploader filter + internal static let disableUploaderFilter = L10n.tr("Localizable", "filters_view.title.disable_uploader_filter", fallback: "Disable uploader filter") + /// Filters + internal static let filters = L10n.tr("Localizable", "filters_view.title.filters", fallback: "Filters") + /// Minimum rating + internal static let minimumRating = L10n.tr("Localizable", "filters_view.title.minimum_rating", fallback: "Minimum rating") + /// Only show galleries with torrents + internal static let onlyShowGalleriesWithTorrents = L10n.tr("Localizable", "filters_view.title.only_show_galleries_with_torrents", fallback: "Only show galleries with torrents") + /// Pages range + internal static let pagesRange = L10n.tr("Localizable", "filters_view.title.pages_range", fallback: "Pages range") + /// Search downvoted tags + internal static let searchDownvotedTags = L10n.tr("Localizable", "filters_view.title.search_downvoted_tags", fallback: "Search downvoted tags") + /// Search expunged galleries + internal static let searchExpungedGalleries = L10n.tr("Localizable", "filters_view.title.search_expunged_galleries", fallback: "Search expunged galleries") + /// Search gallery description + internal static let searchGalleryDescription = L10n.tr("Localizable", "filters_view.title.search_gallery_description", fallback: "Search gallery description") + /// Search gallery name + internal static let searchGalleryName = L10n.tr("Localizable", "filters_view.title.search_gallery_name", fallback: "Search gallery name") + /// Search gallery tags + internal static let searchGalleryTags = L10n.tr("Localizable", "filters_view.title.search_gallery_tags", fallback: "Search gallery tags") + /// Search Low-Power tags + internal static let searchLowPowerTags = L10n.tr("Localizable", "filters_view.title.search_low_power_tags", fallback: "Search Low-Power tags") + /// Search torrent filenames + internal static let searchTorrentFilenames = L10n.tr("Localizable", "filters_view.title.search_torrent_filenames", fallback: "Search torrent filenames") + /// Set minimum rating + internal static let setMinimumRating = L10n.tr("Localizable", "filters_view.title.set_minimum_rating", fallback: "Set minimum rating") + /// Set pages range + internal static let setPagesRange = L10n.tr("Localizable", "filters_view.title.set_pages_range", fallback: "Set pages range") + } + } + internal enum FrontpageView { + internal enum Title { + /// Frontpage + internal static let frontpage = L10n.tr("Localizable", "frontpage_view.title.frontpage", fallback: "Frontpage") + } + } + internal enum GalleryInfosView { + internal enum Title { + /// Archive URL + internal static let archiveURL = L10n.tr("Localizable", "gallery_infos_view.title.archive_URL", fallback: "Archive URL") + /// Average rating + internal static let averageRating = L10n.tr("Localizable", "gallery_infos_view.title.average_rating", fallback: "Average rating") + /// Category + internal static let category = L10n.tr("Localizable", "gallery_infos_view.title.category", fallback: "Category") + /// Cover URL + internal static let coverURL = L10n.tr("Localizable", "gallery_infos_view.title.cover_URL", fallback: "Cover URL") + /// Favorited + internal static let favorited = L10n.tr("Localizable", "gallery_infos_view.title.favorited", fallback: "Favorited") + /// Favorited times + internal static let favoritedTimes = L10n.tr("Localizable", "gallery_infos_view.title.favorited_times", fallback: "Favorited times") + /// File size + internal static let fileSize = L10n.tr("Localizable", "gallery_infos_view.title.file_size", fallback: "File size") + /// Gallery infos + internal static let galleryInfos = L10n.tr("Localizable", "gallery_infos_view.title.gallery_infos", fallback: "Gallery infos") + /// Gallery URL + internal static let galleryURL = L10n.tr("Localizable", "gallery_infos_view.title.gallery_URL", fallback: "Gallery URL") + /// ID + internal static let id = L10n.tr("Localizable", "gallery_infos_view.title.id", fallback: "ID") + /// Japanese title + internal static let japaneseTitle = L10n.tr("Localizable", "gallery_infos_view.title.japanese_title", fallback: "Japanese title") + /// Language + internal static let language = L10n.tr("Localizable", "gallery_infos_view.title.language", fallback: "Language") + /// My rating + internal static let myRating = L10n.tr("Localizable", "gallery_infos_view.title.my_rating", fallback: "My rating") + /// Page count + internal static let pageCount = L10n.tr("Localizable", "gallery_infos_view.title.page_count", fallback: "Page count") + /// Parent URL + internal static let parentURL = L10n.tr("Localizable", "gallery_infos_view.title.parent_URL", fallback: "Parent URL") + /// Posted date + internal static let postedDate = L10n.tr("Localizable", "gallery_infos_view.title.posted_date", fallback: "Posted date") + /// Rating count + internal static let ratingCount = L10n.tr("Localizable", "gallery_infos_view.title.rating_count", fallback: "Rating count") + /// Title + internal static let title = L10n.tr("Localizable", "gallery_infos_view.title.title", fallback: "Title") + /// Token + internal static let token = L10n.tr("Localizable", "gallery_infos_view.title.token", fallback: "Token") + /// Torrent count + internal static let torrentCount = L10n.tr("Localizable", "gallery_infos_view.title.torrent_count", fallback: "Torrent count") + /// Torrent URL + internal static let torrentURL = L10n.tr("Localizable", "gallery_infos_view.title.torrent_URL", fallback: "Torrent URL") + /// Uploader + internal static let uploader = L10n.tr("Localizable", "gallery_infos_view.title.uploader", fallback: "Uploader") + /// Visibility + internal static let visibility = L10n.tr("Localizable", "gallery_infos_view.title.visibility", fallback: "Visibility") + } + internal enum Value { + /// No + internal static let no = L10n.tr("Localizable", "gallery_infos_view.value.no", fallback: "No") + /// None + internal static let `none` = L10n.tr("Localizable", "gallery_infos_view.value.none", fallback: "None") + /// Yes + internal static let yes = L10n.tr("Localizable", "gallery_infos_view.value.yes", fallback: "Yes") + } + } + internal enum GeneralSettingView { + internal enum Button { + /// Clear image caches + internal static let clearImageCaches = L10n.tr("Localizable", "general_setting_view.button.clear_image_caches", fallback: "Clear image caches") + /// Import custom translations + internal static let importCustomTranslations = L10n.tr("Localizable", "general_setting_view.button.import_custom_translations", fallback: "Import custom translations") + /// Logs + internal static let logs = L10n.tr("Localizable", "general_setting_view.button.logs", fallback: "Logs") + /// Remove custom translations + internal static let removeCustomTranslations = L10n.tr("Localizable", "general_setting_view.button.remove_custom_translations", fallback: "Remove custom translations") + } + internal enum Section { + internal enum Title { + /// Caches + internal static let caches = L10n.tr("Localizable", "general_setting_view.section.title.caches", fallback: "Caches") + /// Navigation + internal static let navigation = L10n.tr("Localizable", "general_setting_view.section.title.navigation", fallback: "Navigation") + /// Security + internal static let security = L10n.tr("Localizable", "general_setting_view.section.title.security", fallback: "Security") + /// Tags + internal static let tags = L10n.tr("Localizable", "general_setting_view.section.title.tags", fallback: "Tags") + } + } + internal enum Title { + /// Auto-Lock + internal static let autoLock = L10n.tr("Localizable", "general_setting_view.title.auto_lock", fallback: "Auto-Lock") + /// Background blur radius + internal static let backgroundBlurRadius = L10n.tr("Localizable", "general_setting_view.title.background_blur_radius", fallback: "Background blur radius") + /// Detects links from the clipboard + internal static let detectsLinksFromClipboard = L10n.tr("Localizable", "general_setting_view.title.detects_links_from_clipboard", fallback: "Detects links from the clipboard") + /// Enables tags extension + internal static let enablesTagsExtension = L10n.tr("Localizable", "general_setting_view.title.enables_tags_extension", fallback: "Enables tags extension") + /// General + internal static let general = L10n.tr("Localizable", "general_setting_view.title.general", fallback: "General") + /// Language + internal static let language = L10n.tr("Localizable", "general_setting_view.title.language", fallback: "Language") + /// Redirects links to the selected host + internal static let redirectsLinksToTheSelectedHost = L10n.tr("Localizable", "general_setting_view.title.redirects_links_to_the_selected_host", fallback: "Redirects links to the selected host") + /// Shows images in tags + internal static let showsImagesInTags = L10n.tr("Localizable", "general_setting_view.title.shows_images_in_tags", fallback: "Shows images in tags") + /// Shows tags search suggestion + internal static let showsTagsSearchSuggestion = L10n.tr("Localizable", "general_setting_view.title.shows_tags_search_suggestion", fallback: "Shows tags search suggestion") + /// Translates tags + internal static let translatesTags = L10n.tr("Localizable", "general_setting_view.title.translates_tags", fallback: "Translates tags") + } + internal enum Value { + /// N/A + internal static let defaultLanguageDescription = L10n.tr("Localizable", "general_setting_view.value.default_language_description", fallback: "N/A") + } + } + internal enum HistoryView { + internal enum Title { + /// History + internal static let history = L10n.tr("Localizable", "history_view.title.history", fallback: "History") + } + } + internal enum HomeView { + internal enum Section { + internal enum Title { + /// Frontpage + internal static let frontpage = L10n.tr("Localizable", "home_view.section.title.frontpage", fallback: "Frontpage") + /// Other + internal static let other = L10n.tr("Localizable", "home_view.section.title.other", fallback: "Other") + /// Toplists + internal static let toplists = L10n.tr("Localizable", "home_view.section.title.toplists", fallback: "Toplists") + } + } + internal enum Title { + /// Home + internal static let home = L10n.tr("Localizable", "home_view.title.home", fallback: "Home") + } + } + internal enum Hud { + internal enum Caption { + /// Copied to clipboard + internal static let copiedToClipboard = L10n.tr("Localizable", "hud.caption.copied_to_clipboard", fallback: "Copied to clipboard") + /// Saved to photo library + internal static let savedToPhotoLibrary = L10n.tr("Localizable", "hud.caption.saved_to_photo_library", fallback: "Saved to photo library") + } + internal enum Title { + /// Communicating... + internal static let communicating = L10n.tr("Localizable", "hud.title.communicating", fallback: "Communicating...") + /// Error + internal static let error = L10n.tr("Localizable", "hud.title.error", fallback: "Error") + /// Loading... + internal static let loading = L10n.tr("Localizable", "hud.title.loading", fallback: "Loading...") + /// Success + internal static let success = L10n.tr("Localizable", "hud.title.success", fallback: "Success") + } + } + internal enum JumpPageView { + internal enum Button { + /// Confirm + internal static let confirm = L10n.tr("Localizable", "jump_page_view.button.confirm", fallback: "Confirm") + } + internal enum Title { + /// Jump page + internal static let jumpPage = L10n.tr("Localizable", "jump_page_view.title.jump_page", fallback: "Jump page") + } + } + internal enum LaboratorySettingView { + internal enum Title { + /// Bypasses SNI Filtering + internal static let bypassesSNIFiltering = L10n.tr("Localizable", "laboratory_setting_view.title.bypasses_SNI_filtering", fallback: "Bypasses SNI Filtering") + /// Laboratory + internal static let laboratory = L10n.tr("Localizable", "laboratory_setting_view.title.laboratory", fallback: "Laboratory") + } + } + internal enum LoadingView { + internal enum Title { + /// Loading... + internal static let loading = L10n.tr("Localizable", "loading_view.title.loading", fallback: "Loading...") + /// Preparing the database... + internal static let preparingDatabase = L10n.tr("Localizable", "loading_view.title.preparing_database", fallback: "Preparing the database...") + } + } + internal enum LocalAuthorization { + /// The App has been locked due to the Auto-Lock expiration. + internal static let reason = L10n.tr("Localizable", "local_authorization.reason", fallback: "The App has been locked due to the Auto-Lock expiration.") + } + internal enum LoginView { + internal enum Title { + /// Login + internal static let login = L10n.tr("Localizable", "login_view.title.login", fallback: "Login") + /// Password + internal static let password = L10n.tr("Localizable", "login_view.title.password", fallback: "Password") + /// Username + internal static let username = L10n.tr("Localizable", "login_view.title.username", fallback: "Username") + } + } + internal enum LogsView { + internal enum Title { + /// Latest + internal static let latest = L10n.tr("Localizable", "logs_view.title.latest", fallback: "Latest") + /// Logs + internal static let logs = L10n.tr("Localizable", "logs_view.title.logs", fallback: "Logs") + } + } + internal enum NewDawnView { + internal enum Title { + /// It is the dawn of a new day! + internal static let first = L10n.tr("Localizable", "new_dawn_view.title.first", fallback: "It is the dawn of a new day!") + /// Reflecting on your journey so far, you find that you are a little wiser. + internal static let second = L10n.tr("Localizable", "new_dawn_view.title.second", fallback: "Reflecting on your journey so far, you find that you are a little wiser.") + } + } + internal enum NotLoginView { + internal enum Button { + /// Login + internal static let login = L10n.tr("Localizable", "not_login_view.button.login", fallback: "Login") + } + internal enum Title { + /// You need to login to access this feature. + internal static let needLogin = L10n.tr("Localizable", "not_login_view.title.need_login", fallback: "You need to login to access this feature.") + } + } + internal enum PopularView { + internal enum Title { + /// Popular + internal static let popular = L10n.tr("Localizable", "popular_view.title.popular", fallback: "Popular") + } + } + internal enum PostCommentView { + internal enum Button { + /// Cancel + internal static let cancel = L10n.tr("Localizable", "post_comment_view.button.cancel", fallback: "Cancel") + /// Post + internal static let post = L10n.tr("Localizable", "post_comment_view.button.post", fallback: "Post") + } + internal enum Title { + /// Edit comment + internal static let editComment = L10n.tr("Localizable", "post_comment_view.title.edit_comment", fallback: "Edit comment") + /// Post comment + internal static let postComment = L10n.tr("Localizable", "post_comment_view.title.post_comment", fallback: "Post comment") + } + } + internal enum PreviewsView { + internal enum Title { + /// Previews + internal static let previews = L10n.tr("Localizable", "previews_view.title.previews", fallback: "Previews") + } + } + internal enum QuickSearchView { + internal enum Placeholder { + /// Optional + internal static let `optional` = L10n.tr("Localizable", "quick_search_view.placeholder.optional", fallback: "Optional") + } + internal enum Title { + /// Content + internal static let content = L10n.tr("Localizable", "quick_search_view.title.content", fallback: "Content") + /// Edit word + internal static let editWord = L10n.tr("Localizable", "quick_search_view.title.edit_word", fallback: "Edit word") + /// Name + internal static let name = L10n.tr("Localizable", "quick_search_view.title.name", fallback: "Name") + /// New word + internal static let newWord = L10n.tr("Localizable", "quick_search_view.title.new_word", fallback: "New word") + /// Quick search + internal static let quickSearch = L10n.tr("Localizable", "quick_search_view.title.quick_search", fallback: "Quick search") + } + internal enum ToolbarItem { + internal enum Button { + /// Confirm + internal static let confirm = L10n.tr("Localizable", "quick_search_view.toolbar_item.button.confirm", fallback: "Confirm") + } + } + } + internal enum ReadingSettingView { + internal enum Section { + internal enum Title { + /// Appearance + internal static let appearance = L10n.tr("Localizable", "reading_setting_view.section.title.appearance", fallback: "Appearance") + } + } + internal enum Title { + /// Direction + internal static let direction = L10n.tr("Localizable", "reading_setting_view.title.direction", fallback: "Direction") + /// Double tap scale factor + internal static let doubleTapScaleFactor = L10n.tr("Localizable", "reading_setting_view.title.double_tap_scale_factor", fallback: "Double tap scale factor") + /// Enables landscape + internal static let enablesLandscape = L10n.tr("Localizable", "reading_setting_view.title.enables_landscape", fallback: "Enables landscape") + /// Maximum scale factor + internal static let maximumScaleFactor = L10n.tr("Localizable", "reading_setting_view.title.maximum_scale_factor", fallback: "Maximum scale factor") + /// Preload limit + internal static let preloadLimit = L10n.tr("Localizable", "reading_setting_view.title.preload_limit", fallback: "Preload limit") + /// Reading + internal static let reading = L10n.tr("Localizable", "reading_setting_view.title.reading", fallback: "Reading") + /// Separator height + internal static let separatorHeight = L10n.tr("Localizable", "reading_setting_view.title.separator_height", fallback: "Separator height") + } + } + internal enum ReadingView { + internal enum ContextMenu { + internal enum Button { + /// Copy + internal static let copy = L10n.tr("Localizable", "reading_view.context_menu.button.copy", fallback: "Copy") + /// Reload + internal static let reload = L10n.tr("Localizable", "reading_view.context_menu.button.reload", fallback: "Reload") + /// Save + internal static let save = L10n.tr("Localizable", "reading_view.context_menu.button.save", fallback: "Save") + /// Save original + internal static let saveOriginal = L10n.tr("Localizable", "reading_view.context_menu.button.save_original", fallback: "Save original") + /// Share + internal static let share = L10n.tr("Localizable", "reading_view.context_menu.button.share", fallback: "Share") + } + } + internal enum ToolbarItem { + internal enum Button { + /// Reading setting + internal static let readingSetting = L10n.tr("Localizable", "reading_view.toolbar_item.button.reading_setting", fallback: "Reading setting") + /// Reload all images + internal static let reloadAllImages = L10n.tr("Localizable", "reading_view.toolbar_item.button.reload_all_images", fallback: "Reload all images") + /// Retry all failed images + internal static let retryAllFailedImages = L10n.tr("Localizable", "reading_view.toolbar_item.button.retry_all_failed_images", fallback: "Retry all failed images") + } + internal enum Title { + /// Auto-Play + internal static let autoPlay = L10n.tr("Localizable", "reading_view.toolbar_item.title.auto_play", fallback: "Auto-Play") + /// Dual-Page mode + internal static let dualPageMode = L10n.tr("Localizable", "reading_view.toolbar_item.title.dual_page_mode", fallback: "Dual-Page mode") + /// Except the cover + internal static let exceptTheCover = L10n.tr("Localizable", "reading_view.toolbar_item.title.except_the_cover", fallback: "Except the cover") + } + } + } + internal enum SearchView { + internal enum Section { + internal enum Title { + /// Quick search + internal static let quickSearch = L10n.tr("Localizable", "search_view.section.title.quick_search", fallback: "Quick search") + /// Recently searched + internal static let recentlySearched = L10n.tr("Localizable", "search_view.section.title.recently_searched", fallback: "Recently searched") + /// Recently seen + internal static let recentlySeen = L10n.tr("Localizable", "search_view.section.title.recently_seen", fallback: "Recently seen") + } + } + internal enum Title { + /// Search + internal static let search = L10n.tr("Localizable", "search_view.title.search", fallback: "Search") + } + } + internal enum Searchable { + internal enum Prompt { + /// Filter + internal static let filter = L10n.tr("Localizable", "searchable.prompt.filter", fallback: "Filter") + } + internal enum Title { + /// Found %d matches. + internal static func matchesCount(_ p1: Int) -> String { + return L10n.tr("Localizable", "searchable.title.matches_count", p1, fallback: "Found %d matches.") + } + } + } + internal enum SettingView { + internal enum Title { + /// Setting + internal static let setting = L10n.tr("Localizable", "setting_view.title.setting", fallback: "Setting") + } + } + internal enum Struct { + internal enum CookieValue { + internal enum LocalizedString { + /// Expired + internal static let expired = L10n.tr("Localizable", "struct.cookie_value.localized_string.expired", fallback: "Expired") + /// Rejected + internal static let mystery = L10n.tr("Localizable", "struct.cookie_value.localized_string.mystery", fallback: "Rejected") + /// None + internal static let `none` = L10n.tr("Localizable", "struct.cookie_value.localized_string.none", fallback: "None") + } + } + internal enum Greeting { + internal enum Mark { + /// and + internal static let and = L10n.tr("Localizable", "struct.greeting.mark.and", fallback: " and ") + /// ! + internal static let end = L10n.tr("Localizable", "struct.greeting.mark.end", fallback: "!") + /// , + internal static let separator = L10n.tr("Localizable", "struct.greeting.mark.separator", fallback: ", ") + /// You gain + internal static let start = L10n.tr("Localizable", "struct.greeting.mark.start", fallback: "You gain ") + } + } + internal enum HathArchive { + internal enum Price { + /// Free + internal static let free = L10n.tr("Localizable", "struct.hath_archive.price.free", fallback: "Free") + /// N/A + internal static let notAvailable = L10n.tr("Localizable", "struct.hath_archive.price.not_available", fallback: "N/A") + } + } + internal enum User { + internal enum FavoriteCategory { + /// All + internal static let all = L10n.tr("Localizable", "struct.user.favorite_category.all", fallback: "All") + /// Favorites %@ + internal static func `default`(_ p1: Any) -> String { + return L10n.tr("Localizable", "struct.user.favorite_category.default", String(describing: p1), fallback: "Favorites %@") + } + } + } + } + internal enum SubSection { + internal enum Button { + /// Show all + internal static let showAll = L10n.tr("Localizable", "sub_section.button.show_all", fallback: "Show all") + } + } + internal enum TabItem { + internal enum Title { + /// Favorites + internal static let favorites = L10n.tr("Localizable", "tab_item.title.favorites", fallback: "Favorites") + /// Home + internal static let home = L10n.tr("Localizable", "tab_item.title.home", fallback: "Home") + /// Search + internal static let search = L10n.tr("Localizable", "tab_item.title.search", fallback: "Search") + /// Setting + internal static let setting = L10n.tr("Localizable", "tab_item.title.setting", fallback: "Setting") + } + } + internal enum TagDetailView { + internal enum Section { + internal enum Title { + /// Images + internal static let images = L10n.tr("Localizable", "tag_detail_view.section.title.images", fallback: "Images") + /// Links + internal static let links = L10n.tr("Localizable", "tag_detail_view.section.title.links", fallback: "Links") + } + } + } + internal enum ToolbarItem { + internal enum Button { + /// Filters + internal static let filters = L10n.tr("Localizable", "toolbar_item.button.filters", fallback: "Filters") + /// Jump page + internal static let jumpPage = L10n.tr("Localizable", "toolbar_item.button.jump_page", fallback: "Jump page") + /// Quick search + internal static let quickSearch = L10n.tr("Localizable", "toolbar_item.button.quick_search", fallback: "Quick search") + } + } + internal enum ToplistsView { + internal enum Title { + /// Toplists + internal static let toplists = L10n.tr("Localizable", "toplists_view.title.toplists", fallback: "Toplists") + } + } + internal enum TorrentsView { + internal enum Title { + /// Torrents + internal static let torrents = L10n.tr("Localizable", "torrents_view.title.torrents", fallback: "Torrents") + } + } + internal enum WatchedView { + internal enum Title { + /// Watched + internal static let watched = L10n.tr("Localizable", "watched_view.title.watched", fallback: "Watched") + } + } + internal enum Website { + internal enum Response { + /// You must have a H@H client assigned to your account to use this feature. + internal static let hathClientNotFound = L10n.tr("Localizable", "website.response.hath_client_not_found", fallback: "You must have a H@H client assigned to your account to use this feature.") + /// Your H@H client appears to be offline. Turn it on, then try again. + internal static let hathClientNotOnline = L10n.tr("Localizable", "website.response.hath_client_not_online", fallback: "Your H@H client appears to be offline. Turn it on, then try again.") + /// The requested gallery cannot be downloaded with the selected resolution. + internal static let invalidResolution = L10n.tr("Localizable", "website.response.invalid_resolution", fallback: "The requested gallery cannot be downloaded with the selected resolution.") + } + } + } +} +// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces + +// MARK: - Implementation Details + +extension L10n { + private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String { + let format = BundleToken.bundle.localizedString(forKey: key, value: value, table: table) + return String(format: format, locale: Locale.current, arguments: args) + } +} + +// swiftlint:disable convenience_type +private final class BundleToken { + static let bundle: Bundle = { + #if SWIFT_PACKAGE + return Bundle.module + #else + return Bundle(for: BundleToken.self) + #endif + }() +} +// swiftlint:enable convenience_type diff --git a/EhPanda/App/Tools/Clients/AppDelegateClient.swift b/EhPanda/App/Tools/Clients/AppDelegateClient.swift index a904cca7..41626c76 100644 --- a/EhPanda/App/Tools/Clients/AppDelegateClient.swift +++ b/EhPanda/App/Tools/Clients/AppDelegateClient.swift @@ -9,31 +9,28 @@ import SwiftUI import ComposableArchitecture struct AppDelegateClient { - let setOrientation: (UIInterfaceOrientationMask) -> EffectTask - let setOrientationMask: (UIInterfaceOrientationMask) -> EffectTask + let setOrientation: @MainActor (UIInterfaceOrientationMask) -> Void + let setOrientationMask: (UIInterfaceOrientationMask) -> Void } extension AppDelegateClient { static let live: Self = .init( setOrientation: { mask in - .fireAndForget { - DeviceUtil.keyWindow?.windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: mask)) - } + DeviceUtil.keyWindow?.windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: mask)) }, setOrientationMask: { mask in - .fireAndForget { - AppDelegate.orientationMask = mask - } + AppDelegate.orientationMask = mask } ) - func setPortraitOrientation() -> EffectTask { + @MainActor + func setPortraitOrientation() { setOrientation(.portrait) } - func setAllOrientationMask() -> EffectTask { + func setAllOrientationMask() { setOrientationMask([.all]) } - func setPortraitOrientationMask() -> EffectTask { + func setPortraitOrientationMask() { setOrientationMask([.portrait, .portraitUpsideDown]) } } @@ -55,8 +52,8 @@ extension DependencyValues { // MARK: Test extension AppDelegateClient { static let noop: Self = .init( - setOrientation: { _ in .none }, - setOrientationMask: { _ in .none } + setOrientation: { _ in }, + setOrientationMask: { _ in } ) static let unimplemented: Self = .init( diff --git a/EhPanda/App/Tools/Clients/AuthorizationClient.swift b/EhPanda/App/Tools/Clients/AuthorizationClient.swift index 442811fe..62599839 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) -> EffectTask + let localAuthroize: (String) async -> Bool } extension AuthorizationClient { @@ -21,21 +21,18 @@ extension AuthorizationClient { return !LAContext().canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) }, localAuthroize: { reason in - Future { promise in + await withCheckedContinuation { continuation in let context = LAContext() var error: NSError? if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) { context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { isSuccess, _ in - promise(.success(isSuccess)) + continuation.resume(returning: isSuccess) } } else { - promise(.success(false)) + continuation.resume(returning: false) } } - .eraseToAnyPublisher() - .receive(on: DispatchQueue.main) - .eraseToEffect() } ) } @@ -58,7 +55,7 @@ extension DependencyValues { extension AuthorizationClient { static let noop: Self = .init( passcodeNotSet: { false }, - localAuthroize: { _ in .none } + localAuthroize: { _ in false } ) static let unimplemented: Self = .init( diff --git a/EhPanda/App/Tools/Clients/ClipboardClient.swift b/EhPanda/App/Tools/Clients/ClipboardClient.swift index 4c72c63f..0b4a312d 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) -> EffectTask - let saveImage: (UIImage, Bool) -> EffectTask + let saveText: (String) -> Void + let saveImage: (UIImage, Bool) -> Void } extension ClipboardClient { @@ -29,21 +29,17 @@ extension ClipboardClient { UIPasteboard.general.changeCount }, saveText: { text in - .fireAndForget { - UIPasteboard.general.string = text - } + UIPasteboard.general.string = text }, saveImage: { (image, isAnimated) in - .fireAndForget { - if isAnimated { - DispatchQueue.global(qos: .utility).async { - if let data = image.kf.data(format: .GIF) { - UIPasteboard.general.setData(data, forPasteboardType: UTType.gif.identifier) - } + if isAnimated { + DispatchQueue.global(qos: .utility).async { + if let data = image.kf.data(format: .GIF) { + UIPasteboard.general.setData(data, forPasteboardType: UTType.gif.identifier) } - } else { - UIPasteboard.general.image = image } + } else { + UIPasteboard.general.image = image } } ) @@ -68,8 +64,8 @@ extension ClipboardClient { static let noop: Self = .init( url: { nil }, changeCount: { 0 }, - saveText: { _ in .none }, - saveImage: { _, _ in .none } + saveText: { _ in }, + saveImage: { _, _ in } ) static let unimplemented: Self = .init( diff --git a/EhPanda/App/Tools/Clients/CookieClient.swift b/EhPanda/App/Tools/Clients/CookieClient.swift index 86143d11..5f091819 100644 --- a/EhPanda/App/Tools/Clients/CookieClient.swift +++ b/EhPanda/App/Tools/Clients/CookieClient.swift @@ -9,7 +9,7 @@ import Foundation import ComposableArchitecture struct CookieClient { - let clearAll: () -> EffectTask + let clearAll: () -> Void let getCookie: (URL, String) -> CookieValue private let removeCookie: (URL, String) -> Void private let checkExistence: (URL, String) -> Bool @@ -19,11 +19,9 @@ struct CookieClient { extension CookieClient { static let live: Self = .init( clearAll: { - .fireAndForget { - if let historyCookies = HTTPCookieStorage.shared.cookies { - historyCookies.forEach { - HTTPCookieStorage.shared.deleteCookie($0) - } + if let historyCookies = HTTPCookieStorage.shared.cookies { + historyCookies.forEach { + HTTPCookieStorage.shared.deleteCookie($0) } } }, @@ -108,13 +106,11 @@ extension CookieClient { guard let cookie = newCookie else { return } HTTPCookieStorage.shared.setCookie(cookie) } - func setOrEditCookie(for url: URL, key: String, value: String) -> EffectTask { - .fireAndForget { - if checkExistence(url, key) { - editCookie(for: url, key: key, value: value) - } else { - setCookie(for: url, key: key, value: value) - } + func setOrEditCookie(for url: URL, key: String, value: String) { + if checkExistence(url, key) { + editCookie(for: url, key: key, value: value) + } else { + setCookie(for: url, key: key, value: value) } } } @@ -138,35 +134,29 @@ extension CookieClient { && !getCookie(url, Defaults.Cookie.ipbPassHash).rawValue.isEmpty && getCookie(url, Defaults.Cookie.igneous).rawValue.isEmpty } - func removeYay() -> EffectTask { - .fireAndForget { - removeCookie(Defaults.URL.exhentai, Defaults.Cookie.yay) - removeCookie(Defaults.URL.sexhentai, Defaults.Cookie.yay) - } + func removeYay() { + 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 syncExCookies() { + let cookies = [ + Defaults.Cookie.ipbMemberId, + Defaults.Cookie.ipbPassHash, + Defaults.Cookie.igneous + ] + for cookie in cookies { + setOrEditCookie( + for: Defaults.URL.sexhentai, + key: cookie, + value: getCookie(Defaults.URL.exhentai, cookie).rawValue + ) + } } - 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 ignoreOffensive() { + setOrEditCookie(for: Defaults.URL.ehentai, key: Defaults.Cookie.ignoreOffensive, value: "1") + setOrEditCookie(for: Defaults.URL.exhentai, key: Defaults.Cookie.ignoreOffensive, value: "1") } - func fulfillAnotherHostField() -> EffectTask { + func fulfillAnotherHostField() { let ehURL = Defaults.URL.ehentai let exURL = Defaults.URL.exhentai let memberIdKey = Defaults.Cookie.ipbMemberId @@ -177,17 +167,11 @@ extension CookieClient { let exPassHash = getCookie(exURL, passHashKey).rawValue if !ehMemberId.isEmpty && !ehPassHash.isEmpty && (exMemberId.isEmpty || exPassHash.isEmpty) { - return .merge( - setOrEditCookie(for: exURL, key: memberIdKey, value: ehMemberId), - setOrEditCookie(for: exURL, key: passHashKey, value: ehPassHash) - ) + setOrEditCookie(for: exURL, key: memberIdKey, value: ehMemberId) + setOrEditCookie(for: exURL, key: passHashKey, value: ehPassHash) } else if !exMemberId.isEmpty && !exPassHash.isEmpty && (ehMemberId.isEmpty || ehPassHash.isEmpty) { - return .merge( - setOrEditCookie(for: ehURL, key: memberIdKey, value: exMemberId), - setOrEditCookie(for: ehURL, key: passHashKey, value: exPassHash) - ) - } else { - return .none + setOrEditCookie(for: ehURL, key: memberIdKey, value: exMemberId) + setOrEditCookie(for: ehURL, key: passHashKey, value: exPassHash) } } func loadCookiesState(host: GalleryHost) -> CookiesState { @@ -218,55 +202,49 @@ extension CookieClient { // MARK: SetCookies extension CookieClient { - 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: trimsSpaces - ? subState.editingText .trimmingCharacters(in: .whitespaces) : subState.editingText - ) - } + func setCookies(state: CookiesState, trimsSpaces: Bool = true) { + for subState in state.allCases { + for cookie in state.host.cookieURLs { + setOrEditCookie( + for: cookie, + key: subState.key, + value: trimsSpaces + ? subState.editingText .trimmingCharacters(in: .whitespaces) : subState.editingText + ) } - return effects.isEmpty ? .none : .merge(effects) + } + } - func setCredentials(response: HTTPURLResponse) -> EffectTask { - .fireAndForget { - guard let setString = response.allHeaderFields["Set-Cookie"] as? String else { return } - setString.components(separatedBy: ", ") - .flatMap { $0.components(separatedBy: "; ") }.forEach { value in - [Defaults.URL.ehentai, Defaults.URL.exhentai].forEach { url in - [ - Defaults.Cookie.ipbMemberId, - Defaults.Cookie.ipbPassHash, - Defaults.Cookie.igneous - ].forEach { key in - guard !(url == Defaults.URL.ehentai && key == Defaults.Cookie.igneous), - let range = value.range(of: "\(key)=") else { return } - setCookie(for: url, key: key, value: String(value[range.upperBound...])) - } + func setCredentials(response: HTTPURLResponse) { + guard let setString = response.allHeaderFields["Set-Cookie"] as? String else { return } + setString.components(separatedBy: ", ") + .flatMap { $0.components(separatedBy: "; ") }.forEach { value in + [Defaults.URL.ehentai, Defaults.URL.exhentai].forEach { url in + [ + Defaults.Cookie.ipbMemberId, + Defaults.Cookie.ipbPassHash, + Defaults.Cookie.igneous + ].forEach { key in + guard !(url == Defaults.URL.ehentai && key == Defaults.Cookie.igneous), + let range = value.range(of: "\(key)=") else { return } + setCookie(for: url, key: key, value: String(value[range.upperBound...])) } } - } + } } - func setSkipServer(response: HTTPURLResponse) -> EffectTask { - .fireAndForget { - guard let setString = response.allHeaderFields["Set-Cookie"] as? String else { return } - setString.components(separatedBy: ", ") - .flatMap { $0.components(separatedBy: "; ") } - .forEach { value in - let key = Defaults.Cookie.skipServer - if let range = value.range(of: "\(key)=") { - setCookie( - for: Defaults.URL.host, key: key, - value: String(value[range.upperBound...]), path: "/s/" - ) - } + func setSkipServer(response: HTTPURLResponse) { + guard let setString = response.allHeaderFields["Set-Cookie"] as? String else { return } + setString.components(separatedBy: ", ") + .flatMap { $0.components(separatedBy: "; ") } + .forEach { value in + let key = Defaults.Cookie.skipServer + if let range = value.range(of: "\(key)=") { + setCookie( + for: Defaults.URL.host, key: key, + value: String(value[range.upperBound...]), path: "/s/" + ) } - } + } } } @@ -287,7 +265,7 @@ extension DependencyValues { // MARK: Test extension CookieClient { static let noop: Self = .init( - clearAll: { .none }, + clearAll: {}, getCookie: { _, _ in .empty }, removeCookie: { _, _ in }, checkExistence: { _, _ in false }, diff --git a/EhPanda/App/Tools/Clients/DFClient.swift b/EhPanda/App/Tools/Clients/DFClient.swift index 54a900c5..449b9aae 100644 --- a/EhPanda/App/Tools/Clients/DFClient.swift +++ b/EhPanda/App/Tools/Clients/DFClient.swift @@ -10,23 +10,21 @@ import Kingfisher import ComposableArchitecture struct DFClient { - let setActive: (Bool) -> EffectTask + let setActive: (Bool) -> Void } extension DFClient { static let live: Self = .init( setActive: { newValue in - .fireAndForget { - if newValue { - URLProtocol.registerClass(DFURLProtocol.self) - } else { - URLProtocol.unregisterClass(DFURLProtocol.self) - } - // Kingfisher - let config = KingfisherManager.shared.downloader.sessionConfiguration - config.protocolClasses = newValue ? [DFURLProtocol.self] : nil - KingfisherManager.shared.downloader.sessionConfiguration = config + if newValue { + URLProtocol.registerClass(DFURLProtocol.self) + } else { + URLProtocol.unregisterClass(DFURLProtocol.self) } + // Kingfisher + let config = KingfisherManager.shared.downloader.sessionConfiguration + config.protocolClasses = newValue ? [DFURLProtocol.self] : nil + KingfisherManager.shared.downloader.sessionConfiguration = config } ) } @@ -48,7 +46,7 @@ extension DependencyValues { // MARK: Test extension DFClient { static let noop: Self = .init( - setActive: { _ in .none } + setActive: { _ in } ) static let unimplemented: Self = .init( diff --git a/EhPanda/App/Tools/Clients/DatabaseClient.swift b/EhPanda/App/Tools/Clients/DatabaseClient.swift index 574f0fb0..80f9b6a8 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: () async -> Result + let dropDatabase: () async -> Result private let saveContext: () -> Void private let materializedObjects: (NSManagedObjectContext, NSPredicate) -> [NSManagedObject] } @@ -20,36 +20,18 @@ struct DatabaseClient { extension DatabaseClient { static let live: Self = .init( prepareDatabase: { - Future { promise in - PersistenceController.shared.prepare { - switch $0 { - case .success: - promise(.success(nil)) - - case .failure(let appError): - promise(.success(appError)) - } + await withCheckedContinuation { continuation in + PersistenceController.shared.prepare { result in + continuation.resume(returning: result) } } - .eraseToAnyPublisher() - .receive(on: DispatchQueue.main) - .eraseToEffect() }, dropDatabase: { - Future { promise in - PersistenceController.shared.rebuild { - switch $0 { - case .success: - promise(.success(nil)) - - case .failure(let appError): - promise(.success(appError)) - } + await withCheckedContinuation { continuation in + PersistenceController.shared.rebuild { result in + continuation.resume(returning: result) } } - .eraseToAnyPublisher() - .receive(on: DispatchQueue.main) - .eraseToEffect() }, saveContext: { let context = PersistenceController.shared.container.viewContext @@ -216,50 +198,27 @@ extension DatabaseClient { } return entity } - func fetchAppEnv() -> EffectTask { - Future { promise in - DispatchQueue.main.async { - promise(.success(fetchOrCreate(entityType: AppEnvMO.self).toEntity())) - } - } - .eraseToAnyPublisher() - .receive(on: DispatchQueue.main) - .eraseToEffect() + @MainActor func fetchAppEnv() -> AppEnv { + fetchOrCreate(entityType: AppEnvMO.self).toEntity() } func fetchAppEnvSynchronously() -> AppEnv { fetchOrCreate(entityType: AppEnvMO.self).toEntity() } - func fetchGalleryState(gid: String) -> EffectTask { - guard gid.isValidGID else { return .none } - return Future { promise in - DispatchQueue.main.async { - promise(.success( - fetchOrCreate(entityType: GalleryStateMO.self, gid: gid).toEntity() - )) - } - } - .eraseToAnyPublisher() - .receive(on: DispatchQueue.main) - .eraseToEffect() - } - func fetchHistoryGalleries(fetchLimit: Int = 0) -> EffectTask<[Gallery]> { - Future { promise in - DispatchQueue.main.async { - let predicate = NSPredicate(format: "lastOpenDate != nil") - let sortDescriptor = NSSortDescriptor( - keyPath: \GalleryMO.lastOpenDate, ascending: false - ) - let galleries = batchFetch( - entityType: GalleryMO.self, fetchLimit: fetchLimit, predicate: predicate, - findBeforeFetch: false, sortDescriptors: [sortDescriptor] - ) - .map { $0.toEntity() } - promise(.success(galleries)) - } - } - .eraseToAnyPublisher() - .receive(on: DispatchQueue.main) - .eraseToEffect() + @MainActor func fetchGalleryState(gid: String) async -> GalleryState? { + guard gid.isValidGID else { return nil } + return fetchOrCreate(entityType: GalleryStateMO.self, gid: gid).toEntity() + } + @MainActor func fetchHistoryGalleries(fetchLimit: Int = 0) -> [Gallery] { + let predicate = NSPredicate(format: "lastOpenDate != nil") + let sortDescriptor = NSSortDescriptor( + keyPath: \GalleryMO.lastOpenDate, ascending: false + ) + let galleries = batchFetch( + entityType: GalleryMO.self, fetchLimit: fetchLimit, predicate: predicate, + findBeforeFetch: false, sortDescriptors: [sortDescriptor] + ) + .map { $0.toEntity() } + return galleries } } // MARK: FetchAccessor @@ -274,200 +233,172 @@ extension DatabaseClient { return fetchAppEnvSynchronously().watchedFilter } } - func fetchHistoryKeywords() -> EffectTask<[String]> { - fetchAppEnv().map(\.historyKeywords) + @MainActor func fetchHistoryKeywords() -> [String] { + fetchAppEnv().historyKeywords } - func fetchQuickSearchWords() -> EffectTask<[QuickSearchWord]> { - fetchAppEnv().map(\.quickSearchWords) + @MainActor func fetchQuickSearchWords() -> [QuickSearchWord] { + fetchAppEnv().quickSearchWords } - func fetchGalleryPreviewURLs(gid: String) -> EffectTask<[Int: URL]> { - guard gid.isValidGID else { return .none } - return fetchGalleryState(gid: gid).map(\.previewURLs) + @MainActor func fetchGalleryPreviewURLs(gid: String) async -> [Int: URL]? { + guard gid.isValidGID else { return nil } + return await fetchGalleryState(gid: gid).map(\.previewURLs) } } // MARK: UpdateGallery extension DatabaseClient { - func updateGallery(gid: String, key: String, value: Any?) -> EffectTask { - guard gid.isValidGID else { return .none } - return .fireAndForget { - DispatchQueue.main.async { - update( - entityType: GalleryMO.self, gid: gid, createIfNil: true, - commitChanges: { $0.setValue(value, forKeyPath: key) } - ) - } - } + @MainActor func updateGallery(gid: String, key: String, value: Any?) { + guard gid.isValidGID else { return } + update( + entityType: GalleryMO.self, gid: gid, createIfNil: true, + commitChanges: { $0.setValue(value, forKeyPath: key) } + ) } - func updateLastOpenDate(gid: String, date: Date = .now) -> EffectTask { - guard gid.isValidGID else { return .none } - return updateGallery(gid: gid, key: "lastOpenDate", value: date) - } - func clearHistoryGalleries() -> EffectTask { - .fireAndForget { - DispatchQueue.main.async { - let predicate = NSPredicate(format: "lastOpenDate != nil") - batchUpdate(entityType: GalleryMO.self, predicate: predicate) { galleryMOs in - galleryMOs.forEach { galleryMO in - galleryMO.lastOpenDate = nil - } - } + @MainActor func updateLastOpenDate(gid: String, date: Date = .now) { + guard gid.isValidGID else { return } + updateGallery(gid: gid, key: "lastOpenDate", value: date) + } + @MainActor func clearHistoryGalleries() { + let predicate = NSPredicate(format: "lastOpenDate != nil") + batchUpdate(entityType: GalleryMO.self, predicate: predicate) { galleryMOs in + galleryMOs.forEach { galleryMO in + galleryMO.lastOpenDate = nil } } } - func cacheGalleries(_ galleries: [Gallery]) -> EffectTask { - .fireAndForget { - DispatchQueue.main.async { - for gallery in galleries.filter({ $0.id.isValidGID }) { - let storedMO = fetch( - entityType: GalleryMO.self, gid: gallery.gid - ) { managedObject in - managedObject?.category = gallery.category.rawValue - managedObject?.coverURL = gallery.coverURL - managedObject?.galleryURL = gallery.galleryURL - // managedObject?.lastOpenDate = gallery.lastOpenDate - managedObject?.pageCount = Int64(gallery.pageCount) - managedObject?.postedDate = gallery.postedDate - managedObject?.rating = gallery.rating - managedObject?.tags = gallery.tags.toData() - managedObject?.title = gallery.title - managedObject?.token = gallery.token - if let uploader = gallery.uploader { - managedObject?.uploader = uploader - } - } - if storedMO == nil { - gallery.toManagedObject(in: PersistenceController.shared.container.viewContext) - } + @MainActor func cacheGalleries(_ galleries: [Gallery]) { + for gallery in galleries.filter({ $0.id.isValidGID }) { + let storedMO = fetch( + entityType: GalleryMO.self, gid: gallery.gid + ) { managedObject in + managedObject?.category = gallery.category.rawValue + managedObject?.coverURL = gallery.coverURL + managedObject?.galleryURL = gallery.galleryURL + // managedObject?.lastOpenDate = gallery.lastOpenDate + managedObject?.pageCount = Int64(gallery.pageCount) + managedObject?.postedDate = gallery.postedDate + managedObject?.rating = gallery.rating + managedObject?.tags = gallery.tags.toData() + managedObject?.title = gallery.title + managedObject?.token = gallery.token + if let uploader = gallery.uploader { + managedObject?.uploader = uploader } - saveContext() + } + if storedMO == nil { + gallery.toManagedObject(in: PersistenceController.shared.container.viewContext) } } + saveContext() } } // MARK: UpdateGalleryDetail extension DatabaseClient { - func cacheGalleryDetail(_ detail: GalleryDetail) -> EffectTask { - guard detail.gid.isValidGID else { return .none } - return .fireAndForget { - DispatchQueue.main.async { - let storedMO = fetch( - entityType: GalleryDetailMO.self, gid: detail.gid - ) { managedObject in - managedObject?.archiveURL = detail.archiveURL - managedObject?.category = detail.category.rawValue - managedObject?.coverURL = detail.coverURL - managedObject?.isFavorited = detail.isFavorited - managedObject?.visibility = detail.visibility.toData() - managedObject?.jpnTitle = detail.jpnTitle - managedObject?.language = detail.language.rawValue - managedObject?.favoritedCount = Int64(detail.favoritedCount) - managedObject?.pageCount = Int64(detail.pageCount) - managedObject?.parentURL = detail.parentURL - managedObject?.postedDate = detail.postedDate - managedObject?.rating = detail.rating - managedObject?.userRating = detail.userRating - managedObject?.ratingCount = Int64(detail.ratingCount) - managedObject?.sizeCount = detail.sizeCount - managedObject?.sizeType = detail.sizeType - managedObject?.title = detail.title - managedObject?.torrentCount = Int64(detail.torrentCount) - managedObject?.uploader = detail.uploader - } - if storedMO == nil { - detail.toManagedObject(in: PersistenceController.shared.container.viewContext) - } - saveContext() - } + @MainActor func cacheGalleryDetail(_ detail: GalleryDetail) { + guard detail.gid.isValidGID else { return } + let storedMO = fetch( + entityType: GalleryDetailMO.self, gid: detail.gid + ) { managedObject in + managedObject?.archiveURL = detail.archiveURL + managedObject?.category = detail.category.rawValue + managedObject?.coverURL = detail.coverURL + managedObject?.isFavorited = detail.isFavorited + managedObject?.visibility = detail.visibility.toData() + managedObject?.jpnTitle = detail.jpnTitle + managedObject?.language = detail.language.rawValue + managedObject?.favoritedCount = Int64(detail.favoritedCount) + managedObject?.pageCount = Int64(detail.pageCount) + managedObject?.parentURL = detail.parentURL + managedObject?.postedDate = detail.postedDate + managedObject?.rating = detail.rating + managedObject?.userRating = detail.userRating + managedObject?.ratingCount = Int64(detail.ratingCount) + managedObject?.sizeCount = detail.sizeCount + managedObject?.sizeType = detail.sizeType + managedObject?.title = detail.title + managedObject?.torrentCount = Int64(detail.torrentCount) + managedObject?.uploader = detail.uploader } + if storedMO == nil { + detail.toManagedObject(in: PersistenceController.shared.container.viewContext) + } + saveContext() } } // MARK: UpdateGalleryState extension DatabaseClient { - func updateGalleryState(gid: String, commitChanges: @escaping (GalleryStateMO) -> Void) -> EffectTask { - guard gid.isValidGID else { return .none } - return .fireAndForget { - DispatchQueue.main.async { - update( - entityType: GalleryStateMO.self, gid: gid, createIfNil: true, - commitChanges: commitChanges - ) - } - } + @MainActor func updateGalleryState(gid: String, commitChanges: @escaping (GalleryStateMO) -> Void) { + guard gid.isValidGID else { return } + update( + entityType: GalleryStateMO.self, gid: gid, createIfNil: true, + commitChanges: commitChanges + ) } - func updateGalleryState(gid: String, key: String, value: Any?) -> EffectTask { - guard gid.isValidGID else { return .none } - return updateGalleryState(gid: gid) { stateMO in + @MainActor func updateGalleryState(gid: String, key: String, value: Any?) { + guard gid.isValidGID else { return } + updateGalleryState(gid: gid) { stateMO in stateMO.setValue(value, forKeyPath: key) } } - func updateGalleryTags(gid: String, tags: [GalleryTag]) -> EffectTask { - guard gid.isValidGID else { return .none } - return updateGalleryState(gid: gid, key: "tags", value: tags.toData()) + @MainActor func updateGalleryTags(gid: String, tags: [GalleryTag]) { + guard gid.isValidGID else { return } + updateGalleryState(gid: gid, key: "tags", value: tags.toData()) } - func updatePreviewConfig(gid: String, config: PreviewConfig) -> EffectTask { - guard gid.isValidGID else { return .none } - return updateGalleryState(gid: gid, key: "previewConfig", value: config.toData()) + @MainActor func updatePreviewConfig(gid: String, config: PreviewConfig) { + guard gid.isValidGID else { return } + updateGalleryState(gid: gid, key: "previewConfig", value: config.toData()) } - func updateReadingProgress(gid: String, progress: Int) -> EffectTask { - guard gid.isValidGID else { return .none } - return updateGalleryState(gid: gid, key: "readingProgress", value: Int64(progress)) + @MainActor func updateReadingProgress(gid: String, progress: Int) { + guard gid.isValidGID else { return } + updateGalleryState(gid: gid, key: "readingProgress", value: Int64(progress)) } - func updateComments(gid: String, comments: [GalleryComment]) -> EffectTask { - guard gid.isValidGID else { return .none } - return updateGalleryState(gid: gid, key: "comments", value: comments.toData()) + @MainActor func updateComments(gid: String, comments: [GalleryComment]) { + guard gid.isValidGID else { return } + updateGalleryState(gid: gid, key: "comments", value: comments.toData()) } - func removeImageURLs(gid: String) -> EffectTask { - guard gid.isValidGID else { return .none } - return updateGalleryState(gid: gid) { galleryStateMO in + @MainActor func removeImageURLs(gid: String) { + guard gid.isValidGID else { return } + updateGalleryState(gid: gid) { galleryStateMO in galleryStateMO.imageURLs = nil galleryStateMO.previewURLs = nil galleryStateMO.thumbnailURLs = nil galleryStateMO.originalImageURLs = nil } } - func removeImageURLs() -> EffectTask { - .fireAndForget { - DispatchQueue.main.async { - batchUpdate(entityType: GalleryStateMO.self) { galleryStateMOs in - galleryStateMOs.forEach { galleryStateMO in - galleryStateMO.imageURLs = nil - galleryStateMO.previewURLs = nil - galleryStateMO.thumbnailURLs = nil - galleryStateMO.originalImageURLs = nil - } - } + @MainActor func removeImageURLs() { + batchUpdate(entityType: GalleryStateMO.self) { galleryStateMOs in + galleryStateMOs.forEach { galleryStateMO in + galleryStateMO.imageURLs = nil + galleryStateMO.previewURLs = nil + galleryStateMO.thumbnailURLs = nil + galleryStateMO.originalImageURLs = nil } } } - func removeExpiredImageURLs() -> EffectTask { + @MainActor func removeExpiredImageURLs() { fetchHistoryGalleries() - .map { $0.filter { Date().timeIntervalSince($0.lastOpenDate ?? .distantPast) > .oneWeek } } - .map { $0.map { removeImageURLs(gid: $0.id) } } - .map(EffectTask.merge) - .fireAndForget() - } - func updateThumbnailURLs(gid: String, thumbnailURLs: [Int: URL]) -> EffectTask { - guard gid.isValidGID else { return .none } - return updateGalleryState(gid: gid) { galleryStateMO in + .filter { Date().timeIntervalSince($0.lastOpenDate ?? .distantPast) > .oneWeek } + .forEach { removeImageURLs(gid: $0.id) } + } + @MainActor func updateThumbnailURLs(gid: String, thumbnailURLs: [Int: URL]) { + guard gid.isValidGID else { return } + updateGalleryState(gid: gid) { galleryStateMO in update(gid: gid, storedData: &galleryStateMO.thumbnailURLs, new: thumbnailURLs) } } - func updateImageURLs( - gid: String, imageURLs: [Int: URL], originalImageURLs: [Int: URL] - ) -> EffectTask { - guard gid.isValidGID else { return .none } - return updateGalleryState(gid: gid) { galleryStateMO in + @MainActor func updateImageURLs(gid: String, imageURLs: [Int: URL], originalImageURLs: [Int: URL]) { + guard gid.isValidGID else { 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]) -> EffectTask { - guard gid.isValidGID else { return .none } - return updateGalleryState(gid: gid) { galleryStateMO in + @MainActor func updatePreviewURLs(gid: String, previewURLs: [Int: URL]) { + guard gid.isValidGID else { return } + updateGalleryState(gid: gid) { galleryStateMO in update(gid: gid, storedData: &galleryStateMO.previewURLs, new: previewURLs) } } @@ -489,20 +420,16 @@ extension DatabaseClient { // MARK: UpdateAppEnv extension DatabaseClient { - func updateAppEnv(key: String, value: Any?) -> EffectTask { - .fireAndForget { - DispatchQueue.main.async { - update( - entityType: AppEnvMO.self, createIfNil: true, - commitChanges: { $0.setValue(value, forKeyPath: key) } - ) - } - } + @MainActor func updateAppEnv(key: String, value: Any?) { + update( + entityType: AppEnvMO.self, createIfNil: true, + commitChanges: { $0.setValue(value, forKeyPath: key) } + ) } - func updateSetting(_ setting: Setting) -> EffectTask { + @MainActor func updateSetting(_ setting: Setting) { updateAppEnv(key: "setting", value: setting.toData()) } - func updateFilter(_ filter: Filter, range: FilterRange) -> EffectTask { + @MainActor func updateFilter(_ filter: Filter, range: FilterRange) { let key: String switch range { case .search: @@ -512,38 +439,33 @@ extension DatabaseClient { case .watched: key = "watchedFilter" } - return updateAppEnv(key: key, value: filter.toData()) + updateAppEnv(key: key, value: filter.toData()) } - func updateTagTranslator(_ tagTranslator: TagTranslator) -> EffectTask { + @MainActor func updateTagTranslator(_ tagTranslator: TagTranslator) { updateAppEnv(key: "tagTranslator", value: tagTranslator.toData()) } - func updateUser(_ user: User) -> EffectTask { + @MainActor func updateUser(_ user: User) { updateAppEnv(key: "user", value: user.toData()) } - func updateHistoryKeywords(_ keywords: [String]) -> EffectTask { + @MainActor func updateHistoryKeywords(_ keywords: [String]) { updateAppEnv(key: "historyKeywords", value: keywords.toData()) } - func updateQuickSearchWords(_ words: [QuickSearchWord]) -> EffectTask { + @MainActor func updateQuickSearchWords(_ words: [QuickSearchWord]) { updateAppEnv(key: "quickSearchWords", value: words.toData()) } // Update User - func updateUserProperty(_ commitChanges: @escaping (inout User) -> Void) -> EffectTask { - fetchAppEnv().map(\.user) - .map { (user: User) -> User in - var user = user - commitChanges(&user) - return user - } - .flatMap(updateUser) - .eraseToEffect() + @MainActor func updateUserProperty(_ commitChanges: @escaping (inout User) -> Void) { + var user = fetchAppEnv().user + commitChanges(&user) + updateUser(user) } - func updateGreeting(_ greeting: Greeting) -> EffectTask { + @MainActor func updateGreeting(_ greeting: Greeting) { updateUserProperty { user in user.greeting = greeting } } - func updateGalleryFunds(galleryPoints: String, credits: String) -> EffectTask { + @MainActor func updateGalleryFunds(galleryPoints: String, credits: String) { updateUserProperty { user in user.credits = credits user.galleryPoints = galleryPoints @@ -568,8 +490,8 @@ extension DependencyValues { // MARK: Test extension DatabaseClient { static let noop: Self = .init( - prepareDatabase: { .none }, - dropDatabase: { .none }, + prepareDatabase: { .success(()) }, + dropDatabase: { .success(()) }, saveContext: {}, materializedObjects: { _, _ in .init() } ) diff --git a/EhPanda/App/Tools/Clients/FileClient.swift b/EhPanda/App/Tools/Clients/FileClient.swift index 15461dd9..5e15dad1 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: () -> EffectTask> - let deleteLog: (String) -> EffectTask> - let importTagTranslator: (URL) -> EffectTask> + let fetchLogs: () async -> Result<[Log], AppError> + let deleteLog: (String) async -> Result + let importTagTranslator: (URL) async -> Result } extension FileClient { @@ -22,76 +22,63 @@ extension FileClient { FileManager.default.createFile(atPath: path, contents: data, attributes: nil) }, fetchLogs: { - Future { promise in - DispatchQueue.global(qos: .userInitiated).async { - guard let path = FileUtil.logsDirectoryURL?.path, - let enumerator = FileManager.default.enumerator(atPath: path), - let fileNames = (enumerator.allObjects as? [String])? - .filter({ $0.contains(Defaults.FilePath.ehpandaLog) }) - else { - promise(.failure(.notFound)) - return - } + await withCheckedContinuation { continuation in + guard let path = FileUtil.logsDirectoryURL?.path, + let enumerator = FileManager.default.enumerator(atPath: path), + let fileNames = (enumerator.allObjects as? [String])? + .filter({ $0.contains(Defaults.FilePath.ehpandaLog) }) + else { + continuation.resume(returning: .failure(.notFound)) + return + } - let logs: [Log] = fileNames.compactMap { name in - guard let fileURL = FileUtil.logsDirectoryURL?.appendingPathComponent(name), - let content = try? String(contentsOf: fileURL) - else { return nil } + let logs: [Log] = fileNames.compactMap { name in + guard let fileURL = FileUtil.logsDirectoryURL?.appendingPathComponent(name), + let content = try? String(contentsOf: fileURL) + else { return nil } - return Log( - fileName: name, contents: content - .components(separatedBy: "\n") - .filter({ !$0.isEmpty }) - ) - } - .sorted() - promise(.success(logs)) + return Log( + fileName: name, contents: content + .components(separatedBy: "\n") + .filter({ !$0.isEmpty }) + ) } + .sorted() + continuation.resume(returning: .success(logs)) } - .eraseToAnyPublisher() - .receive(on: DispatchQueue.main) - .catchToEffect() }, deleteLog: { fileName in - Future { promise in + await withCheckedContinuation { continuation in guard let fileURL = FileUtil.logsDirectoryURL?.appendingPathComponent(fileName) else { - promise(.failure(.notFound)) + continuation.resume(returning: .failure(.notFound)) return } try? FileManager.default.removeItem(at: fileURL) if FileManager.default.fileExists(atPath: fileURL.path) { - promise(.failure(.unknown)) + continuation.resume(returning: .failure(.unknown)) } - promise(.success(fileName)) + continuation.resume(returning: .success(fileName)) } - .eraseToAnyPublisher() - .receive(on: DispatchQueue.main) - .catchToEffect() }, importTagTranslator: { url in - Future { promise in - DispatchQueue.global(qos: .userInitiated).async { - guard let data = try? Data(contentsOf: url), - let translations = try? JSONDecoder().decode( - EhTagTranslationDatabaseResponse.self, from: data - ).tagTranslations - else { - promise(.failure(.parseFailed)) - return - } - guard !translations.isEmpty else { - promise(.failure(.parseFailed)) - return - } - promise(.success(.init(hasCustomTranslations: true, translations: translations))) - } + await withCheckedContinuation { continuation in + guard let data = try? Data(contentsOf: url), + let translations = try? JSONDecoder().decode( + EhTagTranslationDatabaseResponse.self, from: data + ).tagTranslations + else { + continuation.resume(returning: .failure(.parseFailed)) + return + } + guard !translations.isEmpty else { + continuation.resume(returning: .failure(.parseFailed)) + return + } + continuation.resume(returning: .success(.init(hasCustomTranslations: true, translations: translations))) } - .eraseToAnyPublisher() - .receive(on: DispatchQueue.main) - .catchToEffect() } ) @@ -123,9 +110,9 @@ extension DependencyValues { extension FileClient { static let noop: Self = .init( createFile: { _, _ in false }, - fetchLogs: { .none }, - deleteLog: { _ in .none }, - importTagTranslator: { _ in .none } + fetchLogs: { .success([]) }, + deleteLog: { _ in .success("") }, + importTagTranslator: { _ in .success(.init()) } ) static let unimplemented: Self = .init( diff --git a/EhPanda/App/Tools/Clients/ImageClient.swift b/EhPanda/App/Tools/Clients/ImageClient.swift index f31a8638..4ebd0766 100644 --- a/EhPanda/App/Tools/Clients/ImageClient.swift +++ b/EhPanda/App/Tools/Clients/ImageClient.swift @@ -12,77 +12,66 @@ import Kingfisher import ComposableArchitecture struct ImageClient { - let prefetchImages: ([URL]) -> EffectTask - let saveImageToPhotoLibrary: (UIImage, Bool) -> EffectTask - let downloadImage: (URL) -> EffectTask> - let retrieveImage: (String) -> EffectTask> + let prefetchImages: ([URL]) -> Void + let saveImageToPhotoLibrary: (UIImage, Bool) async -> Bool + let downloadImage: (URL) async -> Result + let retrieveImage: (String) async -> Result } extension ImageClient { static let live: Self = .init( prefetchImages: { urls in - .fireAndForget { - ImagePrefetcher(urls: urls).start() - } + ImagePrefetcher(urls: urls).start() }, saveImageToPhotoLibrary: { (image, isAnimated) in - Future { promise in - DispatchQueue.global(qos: .utility).async { - if let data = image.kf.data(format: isAnimated ? .GIF : .unknown) { - PHPhotoLibrary.shared().performChanges { - let request = PHAssetCreationRequest.forAsset() - request.addResource(with: .photo, data: data, options: nil) - } completionHandler: { (isSuccess, _) in - promise(.success(isSuccess)) - } - } else { - promise(.success(false)) + await withCheckedContinuation { continuation in + if let data = image.kf.data(format: isAnimated ? .GIF : .unknown) { + PHPhotoLibrary.shared().performChanges { + let request = PHAssetCreationRequest.forAsset() + request.addResource(with: .photo, data: data, options: nil) + } completionHandler: { (isSuccess, _) in + continuation.resume(returning: isSuccess) } + } else { + continuation.resume(returning: false) } } - .eraseToAnyPublisher() - .receive(on: DispatchQueue.main) - .eraseToEffect() }, downloadImage: { url in - Future { promise in + await withCheckedContinuation { continuation in KingfisherManager.shared.downloader.downloadImage(with: url, options: nil) { result in switch result { case .success(let result): - promise(.success(result.image)) + continuation.resume(returning: .success(result.image)) case .failure(let error): - promise(.failure(error)) + continuation.resume(returning: .failure(error)) } } } - .eraseToAnyPublisher() - .catchToEffect() }, retrieveImage: { key in - Future { promise in + await withCheckedContinuation { continuation in KingfisherManager.shared.cache.retrieveImage(forKey: key) { result in switch result { case .success(let result): if let image = result.image { - promise(.success(image)) + continuation.resume(returning: .success(image)) } else { - promise(.failure(AppError.notFound)) + continuation.resume(returning: .failure(AppError.notFound)) } case .failure(let error): - promise(.failure(error)) + continuation.resume(returning: .failure(error)) } } } - .eraseToAnyPublisher() - .catchToEffect() } ) - func fetchImage(url: URL) -> EffectTask> { + func fetchImage(url: URL) async -> Result { if KingfisherManager.shared.cache.isCached(forKey: url.absoluteString) { - return retrieveImage(url.absoluteString) + return await retrieveImage(url.absoluteString) } else { - return downloadImage(url) + return await downloadImage(url) } } } @@ -121,10 +110,10 @@ extension DependencyValues { // MARK: Test extension ImageClient { static let noop: Self = .init( - prefetchImages: { _ in .none }, - saveImageToPhotoLibrary: { _, _ in .none }, - downloadImage: { _ in .none }, - retrieveImage: { _ in .none } + prefetchImages: { _ in }, + saveImageToPhotoLibrary: { _, _ in false }, + downloadImage: { _ in .success(UIImage()) }, + retrieveImage: { _ in .success(UIImage()) } ) static let unimplemented: Self = .init( diff --git a/EhPanda/App/Tools/Clients/LibraryClient.swift b/EhPanda/App/Tools/Clients/LibraryClient.swift index 5f566e54..ca18d4cb 100644 --- a/EhPanda/App/Tools/Clients/LibraryClient.swift +++ b/EhPanda/App/Tools/Clients/LibraryClient.swift @@ -14,76 +14,65 @@ import UIImageColors import ComposableArchitecture struct LibraryClient { - let initializeLogger: () -> EffectTask - let initializeWebImage: () -> EffectTask - let clearWebImageDiskCache: () -> EffectTask - let analyzeImageColors: (UIImage) -> EffectTask - let calculateWebImageDiskCacheSize: () -> EffectTask + let initializeLogger: () -> Void + let initializeWebImage: () -> Void + let clearWebImageDiskCache: () -> Void + let analyzeImageColors: (UIImage) async -> UIImageColors? + let calculateWebImageDiskCacheSize: () async -> UInt? } extension LibraryClient { static let live: Self = .init( initializeLogger: { - .fireAndForget { - // MARK: SwiftyBeaver - let file = FileDestination() - let console = ConsoleDestination() - let format = [ - "$Dyyyy-MM-dd HH:mm:ss.SSS$d", - "$C$L$c $N.$F:$l - $M $X" - ].joined(separator: " ") + // MARK: SwiftyBeaver + let file = FileDestination() + let console = ConsoleDestination() + let format = [ + "$Dyyyy-MM-dd HH:mm:ss.SSS$d", + "$C$L$c $N.$F:$l - $M $X" + ].joined(separator: " ") - file.format = format - file.logFileAmount = 10 - file.calendar = Calendar(identifier: .gregorian) - file.logFileURL = FileUtil.logsDirectoryURL? - .appendingPathComponent(Defaults.FilePath.ehpandaLog) + file.format = format + file.logFileAmount = 10 + file.calendar = Calendar(identifier: .gregorian) + file.logFileURL = FileUtil.logsDirectoryURL? + .appendingPathComponent(Defaults.FilePath.ehpandaLog) - console.format = format - console.calendar = Calendar(identifier: .gregorian) - console.asynchronously = false - console.levelColor.verbose = "😪" - console.levelColor.warning = "⚠️" - console.levelColor.error = "‼️" - console.levelColor.debug = "🐛" - console.levelColor.info = "📖" + console.format = format + console.calendar = Calendar(identifier: .gregorian) + console.asynchronously = false + console.levelColor.verbose = "😪" + console.levelColor.warning = "⚠️" + console.levelColor.error = "‼️" + console.levelColor.debug = "🐛" + console.levelColor.info = "📖" - SwiftyBeaver.addDestination(file) - #if DEBUG - SwiftyBeaver.addDestination(console) - #endif - } + SwiftyBeaver.addDestination(file) + #if DEBUG + SwiftyBeaver.addDestination(console) + #endif }, initializeWebImage: { - .fireAndForget { - let config = KingfisherManager.shared.downloader.sessionConfiguration - config.httpCookieStorage = HTTPCookieStorage.shared - KingfisherManager.shared.downloader.sessionConfiguration = config - } + let config = KingfisherManager.shared.downloader.sessionConfiguration + config.httpCookieStorage = HTTPCookieStorage.shared + KingfisherManager.shared.downloader.sessionConfiguration = config }, clearWebImageDiskCache: { - .fireAndForget { - KingfisherManager.shared.cache.clearDiskCache() - } + KingfisherManager.shared.cache.clearDiskCache() }, analyzeImageColors: { image in - Future { promise in + await withCheckedContinuation { continuation in image.getColors(quality: .lowest) { colors in - promise(.success(colors)) + continuation.resume(returning: colors) } } - .eraseToAnyPublisher() - .eraseToEffect() }, calculateWebImageDiskCacheSize: { - Future { promise in + await withCheckedContinuation { continuation in KingfisherManager.shared.cache.calculateDiskStorageSize { - promise(.success(try? $0.get())) + continuation.resume(returning: try? $0.get()) } } - .eraseToAnyPublisher() - .receive(on: DispatchQueue.main) - .eraseToEffect() } ) } @@ -105,9 +94,9 @@ extension DependencyValues { // MARK: Test extension LibraryClient { static let noop: Self = .init( - initializeLogger: { .none }, - initializeWebImage: { .none }, - clearWebImageDiskCache: { .none }, + initializeLogger: {}, + initializeWebImage: {}, + clearWebImageDiskCache: {}, analyzeImageColors: { _ in .none }, calculateWebImageDiskCacheSize: { .none } ) diff --git a/EhPanda/App/Tools/Clients/LoggerClient.swift b/EhPanda/App/Tools/Clients/LoggerClient.swift index ebeefb83..b1bfc6ad 100644 --- a/EhPanda/App/Tools/Clients/LoggerClient.swift +++ b/EhPanda/App/Tools/Clients/LoggerClient.swift @@ -8,21 +8,17 @@ import ComposableArchitecture struct LoggerClient { - let info: (Any, Any?) -> EffectTask - let error: (Any, Any?) -> EffectTask + let info: (Any, Any?) -> Void + let error: (Any, Any?) -> Void } extension LoggerClient { static let live: Self = .init( info: { message, context in - .fireAndForget { - Logger.info(message, context: context) - } + Logger.info(message, context: context) }, error: { message, context in - .fireAndForget { - Logger.error(message, context: context) - } + Logger.error(message, context: context) } ) } @@ -44,8 +40,8 @@ extension DependencyValues { // MARK: Test extension LoggerClient { static let noop: Self = .init( - info: { _, _ in .none }, - error: { _, _ in .none } + info: { _, _ in }, + error: { _, _ in } ) static let unimplemented: Self = .init( diff --git a/EhPanda/App/Tools/Clients/UIApplicationClient.swift b/EhPanda/App/Tools/Clients/UIApplicationClient.swift index 97aef088..9683bb5c 100644 --- a/EhPanda/App/Tools/Clients/UIApplicationClient.swift +++ b/EhPanda/App/Tools/Clients/UIApplicationClient.swift @@ -10,60 +10,52 @@ import Combine import ComposableArchitecture struct UIApplicationClient { - let openURL: (URL) -> EffectTask - let hideKeyboard: () -> EffectTask + let openURL: @MainActor (URL) -> Void + let hideKeyboard: () -> Void let alternateIconName: () -> String? - let setAlternateIconName: (String?) -> EffectTask> - let setUserInterfaceStyle: (UIUserInterfaceStyle) -> EffectTask + let setAlternateIconName: @MainActor (String?) async -> Bool + let setUserInterfaceStyle: @MainActor (UIUserInterfaceStyle) -> Void } extension UIApplicationClient { static let live: Self = .init( openURL: { url in - .fireAndForget { - UIApplication.shared.open(url, options: [:]) - } + UIApplication.shared.open(url, options: [:]) }, hideKeyboard: { - .fireAndForget { - UIApplication.shared.endEditing() - } + UIApplication.shared.endEditing() }, alternateIconName: { UIApplication.shared.alternateIconName }, setAlternateIconName: { iconName in - Future { promise in + await withCheckedContinuation { continuation in UIApplication.shared.setAlternateIconName(iconName) { error in if let error = error { - promise(.success(false)) + continuation.resume(returning: false) } else { - promise(.success(true)) + continuation.resume(returning: true) } } } - .eraseToAnyPublisher() - .catchToEffect() }, setUserInterfaceStyle: { userInterfaceStyle in - .fireAndForget { - (DeviceUtil.keyWindow ?? DeviceUtil.anyWindow)?.overrideUserInterfaceStyle = userInterfaceStyle - } + (DeviceUtil.keyWindow ?? DeviceUtil.anyWindow)?.overrideUserInterfaceStyle = userInterfaceStyle } ) - func openSettings() -> EffectTask { + @MainActor + func openSettings() { if let url = URL(string: UIApplication.openSettingsURLString) { return openURL(url) } - return .none } - func openFileApp() -> EffectTask { + @MainActor + func openFileApp() { if let dirPath = FileUtil.logsDirectoryURL?.path, let dirURL = URL(string: "shareddocuments://" + dirPath) { return openURL(dirURL) } - return .none } } @@ -84,11 +76,11 @@ extension DependencyValues { // MARK: Test extension UIApplicationClient { static let noop: Self = .init( - openURL: { _ in .none}, - hideKeyboard: { .none }, + openURL: { _ in}, + hideKeyboard: {}, alternateIconName: { nil }, - setAlternateIconName: { _ in .none }, - setUserInterfaceStyle: { _ in .none } + setAlternateIconName: { _ in false }, + setUserInterfaceStyle: { _ in } ) static let unimplemented: Self = .init( diff --git a/EhPanda/App/Tools/Clients/UserDefaultsClient.swift b/EhPanda/App/Tools/Clients/UserDefaultsClient.swift index c403f278..4ae104be 100644 --- a/EhPanda/App/Tools/Clients/UserDefaultsClient.swift +++ b/EhPanda/App/Tools/Clients/UserDefaultsClient.swift @@ -9,15 +9,13 @@ import Foundation import ComposableArchitecture struct UserDefaultsClient { - let setValue: (Any, AppUserDefaults) -> EffectTask + let setValue: (Any, AppUserDefaults) -> Void } extension UserDefaultsClient { static let live: Self = .init( setValue: { value, key in - .fireAndForget { - UserDefaults.standard.set(value, forKey: key.rawValue) - } + UserDefaults.standard.set(value, forKey: key.rawValue) } ) @@ -43,7 +41,7 @@ extension DependencyValues { // MARK: Test extension UserDefaultsClient { static let noop: Self = .init( - setValue: { _, _ in .none } + setValue: { _, _ in } ) static let unimplemented: Self = .init( diff --git a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift index 1fe1c4ae..fb6e445c 100644 --- a/EhPanda/App/Tools/Extensions/Reducer_Extension.swift +++ b/EhPanda/App/Tools/Extensions/Reducer_Extension.swift @@ -8,23 +8,23 @@ import SwiftUI import ComposableArchitecture -extension ReducerProtocol { +extension Reducer { func haptics( unwrapping enum: @escaping (State) -> Enum?, - case casePath: CasePath, + case casePath: AnyCasePath, hapticsClient: HapticsClient, style: UIImpactFeedbackGenerator.FeedbackStyle = .light - ) -> some ReducerProtocol { + ) -> some Reducer { onBecomeNonNil(unwrapping: `enum`, case: casePath) { _, _ in - .fireAndForget({ hapticsClient.generateFeedback(style) }) + .run(operation: { _ in hapticsClient.generateFeedback(style) }) } } private func onBecomeNonNil( unwrapping enum: @escaping (State) -> Enum?, - case casePath: CasePath, - perform additionalEffects: @escaping (inout State, Action) -> EffectTask - ) -> some ReducerProtocol { + case casePath: AnyCasePath, + perform additionalEffects: @escaping (inout State, Action) -> Effect + ) -> some Reducer { Reduce { state, action in let previousCase = Binding.constant(`enum`(state)).case(casePath).wrappedValue let effects = reduce(into: &state, action: action) @@ -38,7 +38,7 @@ extension ReducerProtocol { } // MARK: Recurse -struct RecurseReducer: ReducerProtocol +struct RecurseReducer: Reducer where State == Base.State, Action == Base.Action { let base: (Reduce) -> Base @@ -46,7 +46,7 @@ where State == Base.State, Action == Base.Action { self.base = base } - public var body: some ReducerProtocol { + public var body: some Reducer { var `self`: Reduce! self = Reduce { state, action in base(self).reduce(into: &state, action: action) @@ -56,7 +56,7 @@ where State == Base.State, Action == Base.Action { } // MARK: Logging -struct LoggingReducer: ReducerProtocol +struct LoggingReducer: Reducer where State == Base.State, Action == Base.Action { let base: Base @@ -65,7 +65,7 @@ where State == Base.State, Action == Base.Action { } @ReducerBuilder - var body: some ReducerProtocol { + var body: some Reducer { Reduce { state, action in if case .setting(.binding(let bindingAction)) = action as? AppReducer.Action { Logger.info("setting(EhPanda.SettingReducer.Action.\(bindingAction.customDumpDescription)") diff --git a/EhPanda/App/Tools/Extensions/SwiftUINavigation_Extension.swift b/EhPanda/App/Tools/Extensions/SwiftUINavigation_Extension.swift index 45c35763..1319fe5e 100644 --- a/EhPanda/App/Tools/Extensions/SwiftUINavigation_Extension.swift +++ b/EhPanda/App/Tools/Extensions/SwiftUINavigation_Extension.swift @@ -23,7 +23,7 @@ extension NavigationLink { } init( unwrapping enum: Binding, - case casePath: CasePath, + case casePath: AnyCasePath, @ViewBuilder destination: @escaping (Binding) -> WrappedDestination ) where Destination == WrappedDestination?, Label == Text { self.init( @@ -38,7 +38,7 @@ extension View { @ViewBuilder func sheet( unwrapping enum: Binding, - case casePath: CasePath, + case casePath: AnyCasePath, onDismiss: (() -> Void)? = nil, isEnabled: Bool, @ViewBuilder content: @escaping (Binding) -> Content @@ -53,7 +53,7 @@ extension View { func confirmationDialog( message: String, unwrapping enum: Binding, - case casePath: CasePath, + case casePath: AnyCasePath, @ViewBuilder actions: @escaping (Case) -> A ) -> some View { self.confirmationDialog( @@ -67,7 +67,7 @@ extension View { func confirmationDialog( message: String, unwrapping enum: Binding, - case casePath: CasePath, + case casePath: AnyCasePath, matching case: Case, @ViewBuilder actions: @escaping (Case) -> A ) -> some View { @@ -87,7 +87,7 @@ extension View { func progressHUD( config: TTProgressHUDConfig, unwrapping enum: Binding, - case casePath: CasePath + case casePath: AnyCasePath ) -> some View { ZStack { self diff --git a/EhPanda/App/en.lproj/Constant.strings b/EhPanda/App/en.lproj/Constant.strings index 8418542a..899cc9aa 100644 --- a/EhPanda/App/en.lproj/Constant.strings +++ b/EhPanda/App/en.lproj/Constant.strings @@ -39,13 +39,13 @@ "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.link.Jimmy-Prime" = "https://github.com/Jimmy-Prime"; +"app.code_level_contributor.link.remlostime" = "https://github.com/remlostime"; "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"; +"app.code_level_contributor.text.Jimmy-Prime" = "Jimmy Prime"; +"app.code_level_contributor.text.remlostime" = "Kai Chen"; // Translation contributor "app.translation_contributor.link.tatsuz0u" = "https://github.com/tatsuz0u"; diff --git a/EhPanda/DataFlow/AppDelegateReducer.swift b/EhPanda/DataFlow/AppDelegateReducer.swift index 53efa716..02b2f607 100644 --- a/EhPanda/DataFlow/AppDelegateReducer.swift +++ b/EhPanda/DataFlow/AppDelegateReducer.swift @@ -9,7 +9,7 @@ import SwiftUI import SwiftyBeaver import ComposableArchitecture -struct AppDelegateReducer: ReducerProtocol { +struct AppDelegateReducer: Reducer { struct State: Equatable { var migrationState = MigrationReducer.State() } @@ -25,22 +25,22 @@ struct AppDelegateReducer: ReducerProtocol { @Dependency(\.libraryClient) private var libraryClient @Dependency(\.cookieClient) private var cookieClient - var body: some ReducerProtocol { + var body: some Reducer { Reduce { _, action in switch action { case .onLaunchFinish: return .merge( - libraryClient.initializeLogger().fireAndForget(), - libraryClient.initializeWebImage().fireAndForget(), - cookieClient.removeYay().fireAndForget(), - cookieClient.syncExCookies().fireAndForget(), - cookieClient.ignoreOffensive().fireAndForget(), - cookieClient.fulfillAnotherHostField().fireAndForget(), - .init(value: .migration(.prepareDatabase)) + .run(operation: { _ in libraryClient.initializeLogger() }), + .run(operation: { _ in libraryClient.initializeWebImage() }), + .run(operation: { _ in cookieClient.removeYay() }), + .run(operation: { _ in cookieClient.syncExCookies() }), + .run(operation: { _ in cookieClient.ignoreOffensive() }), + .run(operation: { _ in cookieClient.fulfillAnotherHostField() }), + .send(.migration(.prepareDatabase)) ) case .removeExpiredImageURLs: - return databaseClient.removeExpiredImageURLs().fireAndForget() + return .run(operation: { _ in await databaseClient.removeExpiredImageURLs() }) case .migration: return .none @@ -53,11 +53,10 @@ struct AppDelegateReducer: ReducerProtocol { // MARK: AppDelegate class AppDelegate: UIResponder, UIApplicationDelegate { - let store = Store( - initialState: .init(), - reducer: AppReducer() - ) - lazy var viewStore = ViewStore(store) + let store = Store(initialState: .init()) { + AppReducer() + } + lazy var viewStore = ViewStore(store, observe: { $0 }) static var orientationMask: UIInterfaceOrientationMask = DeviceUtil.isPad ? .all : [.portrait, .portraitUpsideDown] diff --git a/EhPanda/DataFlow/AppLockReducer.swift b/EhPanda/DataFlow/AppLockReducer.swift index daafcc15..7bc0a1a0 100644 --- a/EhPanda/DataFlow/AppLockReducer.swift +++ b/EhPanda/DataFlow/AppLockReducer.swift @@ -8,7 +8,7 @@ import SwiftUI import ComposableArchitecture -struct AppLockReducer: ReducerProtocol { +struct AppLockReducer: Reducer { struct State: Equatable { @BindingState var blurRadius: Double = 0 var becameInactiveDate: Date? @@ -31,7 +31,7 @@ struct AppLockReducer: ReducerProtocol { @Dependency(\.authorizationClient) private var authorizationClient - var body: some ReducerProtocol { + var body: some Reducer { Reduce { state, action in switch action { case .onBecomeActive(let threshold, let blurRadius): @@ -39,11 +39,11 @@ struct AppLockReducer: ReducerProtocol { Date.now.timeIntervalSince(date) >= Double(threshold) { return .merge( - .init(value: .authorize), - .init(value: .lockApp(blurRadius)) + .send(.authorize), + .send(.lockApp(blurRadius)) ) } else { - return .init(value: .unlockApp) + return .send(.unlockApp) } case .onBecomeInactive(let blurRadius): @@ -63,11 +63,13 @@ struct AppLockReducer: ReducerProtocol { return .none case .authorize: - return authorizationClient.localAuthroize(L10n.Localizable.LocalAuthorization.reason) - .map(Action.authorizeDone) + return .run { send in + let success = await authorizationClient.localAuthroize(L10n.Localizable.LocalAuthorization.reason) + await send(.authorizeDone(success)) + } case .authorizeDone(let isSucceeded): - return isSucceeded ? .init(value: .unlockApp) : .none + return isSucceeded ? .send(.unlockApp) : .none } } } diff --git a/EhPanda/DataFlow/AppReducer.swift b/EhPanda/DataFlow/AppReducer.swift index f67bca86..2cea5826 100644 --- a/EhPanda/DataFlow/AppReducer.swift +++ b/EhPanda/DataFlow/AppReducer.swift @@ -8,16 +8,16 @@ import SwiftUI import ComposableArchitecture -struct AppReducer: ReducerProtocol { +struct AppReducer: Reducer { struct State: Equatable { var appDelegateState = AppDelegateReducer.State() - var appRouteState = AppRouteReducer.State() + @BindingState 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() + @BindingState var settingState = SettingReducer.State() } enum Action: BindableAction { @@ -40,18 +40,22 @@ struct AppReducer: ReducerProtocol { @Dependency(\.cookieClient) private var cookieClient @Dependency(\.deviceClient) private var deviceClient - var body: some ReducerProtocol { + var body: some Reducer { LoggingReducer { BindingReducer() + .onChange(of: \.appRouteState.route) { _, newValue in + Reduce { _, _ in + return newValue == nil ? .send(.appRoute(.clearSubStates)) : .none + } + } + .onChange(of: \.settingState.setting) { _, _ in + Reduce { _, _ in + return .send(.setting(.syncSetting)) + } + } 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 @@ -62,11 +66,11 @@ struct AppReducer: ReducerProtocol { case .active: let threshold = state.settingState.setting.autoLockPolicy.rawValue let blurRadius = state.settingState.setting.backgroundBlurRadius - return .init(value: .appLock(.onBecomeActive(threshold, blurRadius))) + return .send(.appLock(.onBecomeActive(threshold, blurRadius))) case .inactive: let blurRadius = state.settingState.setting.backgroundBlurRadius - return .init(value: .appLock(.onBecomeInactive(blurRadius))) + return .send(.appLock(.onBecomeInactive(blurRadius))) default: return .none @@ -74,18 +78,18 @@ struct AppReducer: ReducerProtocol { case .appDelegate(.migration(.onDatabasePreparationSuccess)): return .merge( - .init(value: .appDelegate(.removeExpiredImageURLs)), - .init(value: .setting(.loadUserSettings)) + .send(.appDelegate(.removeExpiredImageURLs)), + .send(.setting(.loadUserSettings)) ) case .appDelegate: return .none case .appRoute(.clearSubStates): - var effects = [EffectTask]() + var effects = [Effect]() if deviceClient.isPad() { state.settingState.route = nil - effects.append(.init(value: .setting(.clearSubStates))) + effects.append(.send(.setting(.clearSubStates))) } return effects.isEmpty ? .none : .merge(effects) @@ -93,11 +97,11 @@ struct AppReducer: ReducerProtocol { return .none case .appLock(.unlockApp): - var effects: [EffectTask] = [ - .init(value: .setting(.fetchGreeting)) + var effects: [Effect] = [ + .send(.setting(.fetchGreeting)) ] if state.settingState.setting.detectsLinksFromClipboard { - effects.append(.init(value: .appRoute(.detectClipboardURL))) + effects.append(.send(.appRoute(.detectClipboardURL))) } return .merge(effects) @@ -105,33 +109,33 @@ struct AppReducer: ReducerProtocol { return .none case .tabBar(.setTabBarItemType(let type)): - var effects = [EffectTask]() - let hapticEffect: EffectTask = .fireAndForget({ hapticsClient.generateFeedback(.soft) }) + var effects = [Effect]() + let hapticEffect: Effect = .run(operation: { _ in hapticsClient.generateFeedback(.soft) }) if type == state.tabBarState.tabBarItemType { switch type { case .home: if state.homeState.route != nil { - effects.append(.init(value: .home(.setNavigation(nil)))) + effects.append(.send(.home(.setNavigation(nil)))) } else { - effects.append(.init(value: .home(.fetchAllGalleries))) + effects.append(.send(.home(.fetchAllGalleries))) } case .favorites: if state.favoritesState.route != nil { - effects.append(.init(value: .favorites(.setNavigation(nil)))) + effects.append(.send(.favorites(.setNavigation(nil)))) effects.append(hapticEffect) } else if cookieClient.didLogin { - effects.append(.init(value: .favorites(.fetchGalleries()))) + effects.append(.send(.favorites(.fetchGalleries()))) effects.append(hapticEffect) } case .search: if state.searchRootState.route != nil { - effects.append(.init(value: .searchRoot(.setNavigation(nil)))) + effects.append(.send(.searchRoot(.setNavigation(nil)))) } else { - effects.append(.init(value: .searchRoot(.fetchDatabaseInfos))) + effects.append(.send(.searchRoot(.fetchDatabaseInfos))) } case .setting: if state.settingState.route != nil { - effects.append(.init(value: .setting(.setNavigation(nil)))) + effects.append(.send(.setting(.setNavigation(nil)))) effects.append(hapticEffect) } } @@ -140,7 +144,7 @@ struct AppReducer: ReducerProtocol { } } if type == .setting && deviceClient.isPad() { - effects.append(.init(value: .appRoute(.setNavigation(.setting)))) + effects.append(.send(.appRoute(.setNavigation(.setting)))) } return effects.isEmpty ? .none : .merge(effects) @@ -148,19 +152,18 @@ struct AppReducer: ReducerProtocol { return .none case .home(.watched(.onNotLoginViewButtonTapped)), .favorites(.onNotLoginViewButtonTapped): - var effects: [EffectTask] = [ - .fireAndForget({ hapticsClient.generateFeedback(.soft) }), - .init(value: .tabBar(.setTabBarItemType(.setting))) + var effects: [Effect] = [ + .run(operation: { _ in hapticsClient.generateFeedback(.soft) }), + .send(.tabBar(.setTabBarItemType(.setting))) ] - effects.append(.init(value: .setting(.setNavigation(.account)))) + effects.append(.send(.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() + .run { send in + let delay = UInt64(deviceClient.isPad() ? 1200 : 200) + try await Task.sleep(for: .milliseconds(delay)) + await send(.setting(.account(.setNavigation(.login)))) + } ) } return .merge(effects) @@ -175,20 +178,20 @@ struct AppReducer: ReducerProtocol { return .none case .setting(.loadUserSettingsDone): - var effects = [EffectTask]() + var effects = [Effect]() 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)))) + effects.append(.send(.appLock(.onBecomeActive(threshold, blurRadius)))) } if state.settingState.setting.detectsLinksFromClipboard { - effects.append(.init(value: .appRoute(.detectClipboardURL))) + effects.append(.send(.appRoute(.detectClipboardURL))) } return effects.isEmpty ? .none : .merge(effects) case .setting(.fetchGreetingDone(let result)): - return .init(value: .appRoute(.fetchGreetingDone(result))) + return .send(.appRoute(.fetchGreetingDone(result))) case .setting: return .none diff --git a/EhPanda/DataFlow/AppRouteReducer.swift b/EhPanda/DataFlow/AppRouteReducer.swift index 9b410b04..0639fce4 100644 --- a/EhPanda/DataFlow/AppRouteReducer.swift +++ b/EhPanda/DataFlow/AppRouteReducer.swift @@ -9,7 +9,7 @@ import SwiftUI import TTProgressHUD import ComposableArchitecture -struct AppRouteReducer: ReducerProtocol { +struct AppRouteReducer: Reducer { enum Route: Equatable, Hashable { case hud case setting @@ -53,20 +53,20 @@ struct AppRouteReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient @Dependency(\.urlClient) private var urlClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding: return .none case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .setHUDConfig(let config): state.hudConfig = config @@ -74,18 +74,17 @@ struct AppRouteReducer: ReducerProtocol { case .clearSubStates: state.detailState = .init() - return .init(value: .detail(.teardown)) + return .send(.detail(.teardown)) case .detectClipboardURL: let currentChangeCount = clipboardClient.changeCount() guard currentChangeCount != userDefaultsClient .getValue(.clipboardChangeCount) else { return .none } - var effects: [EffectTask] = [ - userDefaultsClient - .setValue(currentChangeCount, .clipboardChangeCount).fireAndForget() + var effects: [Effect] = [ + .run(operation: { _ in userDefaultsClient.setValue(currentChangeCount, .clipboardChangeCount) }) ] if let url = clipboardClient.url() { - effects.append(.init(value: .handleDeepLink(url))) + effects.append(.send(.handleDeepLink(url))) } return .merge(effects) @@ -101,60 +100,76 @@ struct AppRouteReducer: ReducerProtocol { 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 .run { [delay] send in + try await Task.sleep(for: .milliseconds(delay + 250)) + await send(.handleGalleryLink(url)) + } + } + return .run { [delay] send in + try await Task.sleep(for: .milliseconds(delay)) + await send(.fetchGallery(url, isGalleryImageURL)) } - 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]() + var effects = [Effect]() state.detailState = .init() - effects.append(.init(value: .detail(.fetchDatabaseInfos(gid)))) + effects.append(.send(.detail(.fetchDatabaseInfos(gid)))) if let pageIndex = pageIndex { - effects.append(.init(value: .updateReadingProgress(gid, pageIndex))) + effects.append(.send(.updateReadingProgress(gid, pageIndex))) effects.append( - .init(value: .detail(.setNavigation(.reading))) - .delay(for: .milliseconds(500), scheduler: DispatchQueue.main).eraseToEffect() + .run { send in + try await Task.sleep(for: .milliseconds(500)) + await send(.detail(.setNavigation(.reading))) + } ) } 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() + .run { send in + try await Task.sleep(for: .milliseconds(500)) + await send(.detail(.setNavigation(.comments(url)))) + } ) } - effects.append(.init(value: .setNavigation(.detail(gid)))) + effects.append(.send(.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() + return .run { _ in + await databaseClient.updateReadingProgress(gid: gid, progress: progress) + } case .fetchGallery(let url, let isGalleryImageURL): state.route = .hud - return GalleryReverseRequest(url: url, isGalleryImageURL: isGalleryImageURL) - .effect.map({ Action.fetchGalleryDone(url, $0) }) + return .run { send in + let response = await GalleryReverseRequest( + url: url, isGalleryImageURL: isGalleryImageURL + ) + .response() + await send(.fetchGalleryDone(url, response)) + } 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)) + .run(operation: { _ in await databaseClient.cacheGalleries([gallery]) }), + .send(.handleGalleryLink(url)) ) case .failure: - return .init(value: .setHUDConfig(.error)) - .delay(for: .milliseconds(500), scheduler: DispatchQueue.main).eraseToEffect() + return .run { send in + try await Task.sleep(for: .milliseconds(500)) + await send(.setHUDConfig(.error)) + } } case .fetchGreetingDone(let result): if case .success(let greeting) = result, !greeting.gainedNothing { - return .init(value: .setNavigation(.newDawn(greeting))) + return .send(.setNavigation(.newDawn(greeting))) } return .none diff --git a/EhPanda/Models/Tags/TagDetail.swift b/EhPanda/Models/Tags/TagDetail.swift index 3300a0ee..fb7a13c9 100644 --- a/EhPanda/Models/Tags/TagDetail.swift +++ b/EhPanda/Models/Tags/TagDetail.swift @@ -12,11 +12,4 @@ struct TagDetail: Equatable { let description: String let imageURLs: [URL] let links: [URL] - - init(title: String, description: String, imageURLs: [URL], links: [URL]) { - self.title = title - self.description = description - self.imageURLs = imageURLs - self.links = links - } } diff --git a/EhPanda/Network/Request.swift b/EhPanda/Network/Request.swift index d739ee09..7765e44f 100644 --- a/EhPanda/Network/Request.swift +++ b/EhPanda/Network/Request.swift @@ -16,8 +16,8 @@ protocol Request { var publisher: AnyPublisher { get } } extension Request { - var effect: EffectTask> { - publisher.receive(on: DispatchQueue.main).catchToEffect() + func response() async -> Result { + await publisher.receive(on: DispatchQueue.main).async() } func mapAppError(error: Error) -> AppError { @@ -41,6 +41,28 @@ private extension Publisher { func genericRetry() -> Publishers.Retry { retry(3) } + + func async() async -> Result where Failure == AppError { + await withCheckedContinuation { continuation in + var cancellable: AnyCancellable? + var finishedWithoutValue = true + cancellable = first() + .sink { result in + switch result { + case .finished: + if finishedWithoutValue { + continuation.resume(returning: .failure(.unknown)) + } + case let .failure(error): + continuation.resume(returning: .failure(error)) + } + cancellable?.cancel() + } receiveValue: { value in + finishedWithoutValue = false + continuation.resume(returning: .success(value)) + } + } + } } private extension URLRequest { mutating func setURLEncodedContentType() { diff --git a/EhPanda/View/Detail/Archives/ArchivesReducer.swift b/EhPanda/View/Detail/Archives/ArchivesReducer.swift index 3e180958..cd843ec1 100644 --- a/EhPanda/View/Detail/Archives/ArchivesReducer.swift +++ b/EhPanda/View/Detail/Archives/ArchivesReducer.swift @@ -9,7 +9,7 @@ import Foundation import TTProgressHUD import ComposableArchitecture -struct ArchivesReducer: ReducerProtocol { +struct ArchivesReducer: Reducer { enum Route { case messageHUD case communicatingHUD @@ -49,7 +49,7 @@ struct ArchivesReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient @Dependency(\.cookieClient) private var cookieClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in @@ -62,18 +62,21 @@ struct ArchivesReducer: ReducerProtocol { return .none case .syncGalleryFunds(let galleryPoints, let credits): - return databaseClient - .updateGalleryFunds(galleryPoints: galleryPoints, credits: credits).fireAndForget() + return .run { _ in + await databaseClient.updateGalleryFunds(galleryPoints: galleryPoints, credits: credits) + } case .teardown: - return .cancel(ids: CancelID.allCases) + return .merge(CancelID.allCases.map(Effect.cancel(id:))) 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.fetchArchive) + return .run { send in + let response = await GalleryArchiveRequest(archiveURL: archiveURL).response() + await send(.fetchArchiveDone(gid, galleryURL, response)) + } + .cancellable(id: CancelID.fetchArchive) case .fetchArchiveDone(let gid, let galleryURL, let result): state.loadingState = .idle @@ -85,9 +88,9 @@ struct ArchivesReducer: ReducerProtocol { } state.hathArchives = archive.hathArchives if let galleryPoints = galleryPoints, let credits = credits { - return .init(value: .syncGalleryFunds(galleryPoints, credits)) + return .send(.syncGalleryFunds(galleryPoints, credits)) } else if cookieClient.isSameAccount { - return .init(value: .fetchArchiveFunds(gid, galleryURL)) + return .send(.fetchArchiveFunds(gid, galleryURL)) } else { return .none } @@ -98,12 +101,15 @@ 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.fetchArchiveFunds) + return .run { send in + let response = await GalleryArchiveFundsRequest(gid: gid, galleryURL: galleryURL).response() + await send(.fetchArchiveFundsDone(response)) + } + .cancellable(id: CancelID.fetchArchiveFunds) case .fetchArchiveFundsDone(let result): if case .success(let (galleryPoints, credits)) = result { - return .init(value: .syncGalleryFunds(galleryPoints, credits)) + return .send(.syncGalleryFunds(galleryPoints, credits)) } return .none @@ -112,10 +118,15 @@ struct ArchivesReducer: ReducerProtocol { state.route != .communicatingHUD else { return .none } state.route = .communicatingHUD - return SendDownloadCommandRequest( - archiveURL: archiveURL, resolution: selectedArchive.resolution.parameter - ) - .effect.map(Action.fetchDownloadResponseDone).cancellable(id: CancelID.fetchDownloadResponse) + return .run {send in + let response = await SendDownloadCommandRequest( + archiveURL: archiveURL, + resolution: selectedArchive.resolution.parameter + ) + .response() + await send(.fetchDownloadResponseDone(response)) + } + .cancellable(id: CancelID.fetchDownloadResponse) case .fetchDownloadResponseDone(let result): state.route = .messageHUD @@ -140,7 +151,9 @@ struct ArchivesReducer: ReducerProtocol { state.messageHUDConfig = .error isSuccess = false } - return .fireAndForget({ hapticsClient.generateNotificationFeedback(isSuccess ? .success : .error) }) + return .run { _ in + hapticsClient.generateNotificationFeedback(isSuccess ? .success : .error) + } } } } diff --git a/EhPanda/View/Detail/Archives/ArchivesView.swift b/EhPanda/View/Detail/Archives/ArchivesView.swift index 9ae731f3..6ba83b28 100644 --- a/EhPanda/View/Detail/Archives/ArchivesView.swift +++ b/EhPanda/View/Detail/Archives/ArchivesView.swift @@ -21,7 +21,7 @@ struct ArchivesView: View { gid: String, user: User, galleryURL: URL, archiveURL: URL ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.gid = gid self.user = user self.galleryURL = galleryURL @@ -33,7 +33,7 @@ struct ArchivesView: View { NavigationView { ZStack { VStack { - HathArchivesView(archives: viewStore.hathArchives, selection: viewStore.binding(\.$selectedArchive)) + HathArchivesView(archives: viewStore.hathArchives, selection: viewStore.$selectedArchive) Spacer() if let credits = Int(user.credits ?? ""), let galleryPoints = Int(user.galleryPoints ?? "") { ArchiveFundsView(credits: credits, galleryPoints: galleryPoints) @@ -55,12 +55,12 @@ struct ArchivesView: View { } .progressHUD( config: viewStore.communicatingHUDConfig, - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /ArchivesReducer.Route.communicatingHUD ) .progressHUD( config: viewStore.messageHUDConfig, - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /ArchivesReducer.Route.messageHUD ) .animation(.default, value: viewStore.hathArchives) @@ -221,10 +221,7 @@ private struct DownloadButton: View { struct ArchivesView_Previews: PreviewProvider { static var previews: some View { ArchivesView( - store: .init( - initialState: .init(), - reducer: ArchivesReducer() - ), + store: .init(initialState: .init(), reducer: ArchivesReducer.init), gid: .init(), user: .init(), galleryURL: .mock, diff --git a/EhPanda/View/Detail/Comments/CommentsReducer.swift b/EhPanda/View/Detail/Comments/CommentsReducer.swift index 98ebffaf..ac45611a 100644 --- a/EhPanda/View/Detail/Comments/CommentsReducer.swift +++ b/EhPanda/View/Detail/Comments/CommentsReducer.swift @@ -9,7 +9,7 @@ import Foundation import TTProgressHUD import ComposableArchitecture -struct CommentsReducer: ReducerProtocol { +struct CommentsReducer: Reducer { enum Route: Equatable { case hud case detail(String) @@ -70,26 +70,26 @@ struct CommentsReducer: ReducerProtocol { @Dependency(\.cookieClient) private var cookieClient @Dependency(\.urlClient) private var urlClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding: return .none case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: state.detailState = .init() state.commentContent = .init() state.postCommentFocused = false - return .init(value: .detail(.teardown)) + return .send(.detail(.teardown)) case .clearScrollCommentID: state.scrollCommentID = nil @@ -113,104 +113,145 @@ struct CommentsReducer: ReducerProtocol { 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() + .run { send in + try await Task.sleep(for: .milliseconds(750)) + await send(.setScrollRowOpacity(0.25)) + }, + .run { send in + try await Task.sleep(for: .milliseconds(1250)) + await send(.setScrollRowOpacity(1)) + }, + .run { send in + try await Task.sleep(for: .milliseconds(2000)) + await send(.clearScrollCommentID) + } ) case .handleCommentLink(let url): guard urlClient.checkIfHandleable(url) else { - return uiApplicationClient.openURL(url).fireAndForget() + return .run(operation: { _ in await uiApplicationClient.openURL(url) }) } let (isGalleryImageURL, _, _) = urlClient.analyzeURL(url) let gid = urlClient.parseGalleryID(url) guard databaseClient.fetchGallery(gid: gid) == nil else { - return .init(value: .handleGalleryLink(url)) + return .send(.handleGalleryLink(url)) } - return .init(value: .fetchGallery(url, isGalleryImageURL)) + return .send(.fetchGallery(url, isGalleryImageURL)) case .handleGalleryLink(let url): let (_, pageIndex, commentID) = urlClient.analyzeURL(url) let gid = urlClient.parseGalleryID(url) - var effects = [EffectTask]() + var effects = [Effect]() if let pageIndex = pageIndex { - effects.append(.init(value: .updateReadingProgress(gid, pageIndex))) + effects.append(.send(.updateReadingProgress(gid, pageIndex))) effects.append( - .init(value: .detail(.setNavigation(.reading))) - .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect() + .run { send in + try await Task.sleep(for: .milliseconds(750)) + await send(.detail(.setNavigation(.reading))) + } ) } 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() + .run { send in + try await Task.sleep(for: .milliseconds(750)) + await send(.detail(.setNavigation(.comments(url)))) + } ) } - effects.append(.init(value: .setNavigation(.detail(gid)))) + effects.append(.send(.setNavigation(.detail(gid)))) return .merge(effects) case .onPostCommentAppear: - return .init(value: .setPostCommentFocused(true)) - .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect() + return .run { send in + try await Task.sleep(for: .milliseconds(750)) + await send(.setPostCommentFocused(true)) + } case .onAppear: if state.detailState == nil { state.detailState = .init() } - return state.scrollCommentID != nil ? .init(value: .performScrollOpacityEffect) : .none + return state.scrollCommentID != nil ? .send(.performScrollOpacityEffect) : .none case .updateReadingProgress(let gid, let progress): guard !gid.isEmpty else { return .none } - return databaseClient - .updateReadingProgress(gid: gid, progress: progress).fireAndForget() + return .run { _ in + await databaseClient.updateReadingProgress(gid: gid, progress: progress) + } case .teardown: - return .cancel(ids: CancelID.allCases) + return .merge(CancelID.allCases.map(Effect.cancel(id:))) 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.postComment) + return .run { [commentContent = state.commentContent] send in + let response = await EditGalleryCommentRequest( + commentID: commentID, + content: commentContent, + galleryURL: galleryURL + ) + .response() + await send(.performCommentActionDone(response)) + } + .cancellable(id: CancelID.postComment) } else { - return CommentGalleryRequest(content: state.commentContent, galleryURL: galleryURL) - .effect.map(Action.performCommentActionDone).cancellable(id: CancelID.postComment) + return .run { [commentContent = state.commentContent] send in + let response = await CommentGalleryRequest( + content: commentContent, galleryURL: galleryURL + ) + .response() + await send(.performCommentActionDone(response)) + } + .cancellable(id: CancelID.postComment) } 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.voteComment) + return .run { send in + let response = await VoteGalleryCommentRequest( + apiuid: apiuid, + apikey: apiKey, + gid: gid, + token: token, + commentID: commentID, + commentVote: vote + ) + .response() + await send(.performCommentActionDone(response)) + } + .cancellable(id: CancelID.voteComment) 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.fetchGallery) + return .run { send in + let response = await GalleryReverseRequest( + url: url, isGalleryImageURL: isGalleryImageURL + ) + .response() + await send(.fetchGalleryDone(url, response)) + } + .cancellable(id: CancelID.fetchGallery) 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)) + .run(operation: { _ in await databaseClient.cacheGalleries([gallery]) }), + .send(.handleGalleryLink(url)) ) case .failure: - return .init(value: .setHUDConfig(.error)) - .delay(for: .milliseconds(500), scheduler: DispatchQueue.main).eraseToEffect() + return .run { send in + try await Task.sleep(for: .milliseconds(500)) + await send(.setHUDConfig(.error)) + } } case .detail: diff --git a/EhPanda/View/Detail/Comments/CommentsView.swift b/EhPanda/View/Detail/Comments/CommentsView.swift index 27534aab..50cdd5a5 100644 --- a/EhPanda/View/Detail/Comments/CommentsView.swift +++ b/EhPanda/View/Detail/Comments/CommentsView.swift @@ -29,7 +29,7 @@ struct CommentsView: View { blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.gid = gid self.token = token self.apiKey = apiKey @@ -92,14 +92,14 @@ struct CommentsView: View { } } } - .sheet(unwrapping: viewStore.binding(\.$route), case: /CommentsReducer.Route.postComment) { route in + .sheet(unwrapping: viewStore.$route, case: /CommentsReducer.Route.postComment) { route in let hasCommentID = !route.wrappedValue.isEmpty PostCommentView( title: hasCommentID ? L10n.Localizable.PostCommentView.Title.editComment : L10n.Localizable.PostCommentView.Title.postComment, - content: viewStore.binding(\.$commentContent), - isFocused: viewStore.binding(\.$postCommentFocused), + content: viewStore.$commentContent, + isFocused: viewStore.$postCommentFocused, postAction: { if hasCommentID { viewStore.send(.postComment(galleryURL, route.wrappedValue)) @@ -116,7 +116,7 @@ struct CommentsView: View { } .progressHUD( config: viewStore.hudConfig, - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /CommentsReducer.Route.hud ) .animation(.default, value: viewStore.scrollRowOpacity) @@ -143,7 +143,7 @@ struct CommentsView: View { // MARK: NavigationLinks private extension CommentsView { @ViewBuilder var navigationLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /CommentsReducer.Route.detail) { route in + NavigationLink(unwrapping: viewStore.$route, case: /CommentsReducer.Route.detail) { route in DetailView( store: store.scope(state: \.detailState, action: CommentsReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -272,10 +272,7 @@ struct CommentsView_Previews: PreviewProvider { static var previews: some View { NavigationView { CommentsView( - store: .init( - initialState: .init(), - reducer: CommentsReducer() - ), + store: .init(initialState: .init(), reducer: CommentsReducer.init), gid: .init(), token: .init(), apiKey: .init(), diff --git a/EhPanda/View/Detail/DetailReducer.swift b/EhPanda/View/Detail/DetailReducer.swift index 67b0de73..55bfe3ae 100644 --- a/EhPanda/View/Detail/DetailReducer.swift +++ b/EhPanda/View/Detail/DetailReducer.swift @@ -9,7 +9,7 @@ import SwiftUI import Foundation import ComposableArchitecture -struct DetailReducer: ReducerProtocol { +struct DetailReducer: Reducer { enum Route: Equatable { case reading case archives(URL, URL) @@ -115,21 +115,21 @@ struct DetailReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient @Dependency(\.cookieClient) private var cookieClient - var body: some ReducerProtocol { + var body: some Reducer { RecurseReducer { (self) in BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding: return .none case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: state.readingState = .init() @@ -142,17 +142,19 @@ struct DetailReducer: ReducerProtocol { 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)) + .send(.reading(.teardown)), + .send(.archives(.teardown)), + .send(.torrents(.teardown)), + .send(.previews(.teardown)), + .send(.comments(.teardown)), + .send(.detailSearch(.teardown)) ) case .onPostCommentAppear: - return .init(value: .setPostCommentFocused(true)) - .delay(for: .milliseconds(750), scheduler: DispatchQueue.main).eraseToEffect() + return .run { send in + try await Task.sleep(for: .milliseconds(750)) + await send(.setPostCommentFocused(true)) + } case .onAppear(let gid, let showsNewDawnGreeting): state.showsNewDawnGreeting = showsNewDawnGreeting @@ -162,15 +164,15 @@ struct DetailReducer: ReducerProtocol { if state.commentsState == nil { state.commentsState = .init() } - return .init(value: .fetchDatabaseInfos(gid)) + return .send(.fetchDatabaseInfos(gid)) case .toggleShowFullTitle: state.showsFullTitle.toggle() - return .fireAndForget({ hapticsClient.generateFeedback(.soft) }) + return .run(operation: { _ in hapticsClient.generateFeedback(.soft) }) case .toggleShowUserRating: state.showsUserRating.toggle() - return .fireAndForget({ hapticsClient.generateFeedback(.soft) }) + return .run(operation: { _ in hapticsClient.generateFeedback(.soft) }) case .setCommentContent(let content): state.commentContent = content @@ -187,9 +189,12 @@ struct DetailReducer: ReducerProtocol { case .confirmRating(let value): state.updateRating(value: value) return .merge( - .init(value: .rateGallery), - .fireAndForget({ hapticsClient.generateFeedback(.soft) }), - .init(value: .confirmRatingDone).delay(for: 1, scheduler: DispatchQueue.main).eraseToEffect() + .send(.rateGallery), + .run(operation: { _ in hapticsClient.generateFeedback(.soft) }), + .run { send in + try await Task.sleep(for: .seconds(1)) + await send(.confirmRatingDone) + } ) case .confirmRatingDone: @@ -197,37 +202,45 @@ struct DetailReducer: ReducerProtocol { return .none case .syncGalleryTags: - return databaseClient - .updateGalleryTags(gid: state.gallery.id, tags: state.galleryTags).fireAndForget() + return .run { [state] _ in + await databaseClient.updateGalleryTags(gid: state.gallery.id, tags: state.galleryTags) + } case .syncGalleryDetail: guard let detail = state.galleryDetail else { return .none } - return databaseClient.cacheGalleryDetail(detail).fireAndForget() + return .run(operation: { _ in await databaseClient.cacheGalleryDetail(detail) }) case .syncGalleryPreviewURLs: - return databaseClient - .updatePreviewURLs(gid: state.gallery.id, previewURLs: state.galleryPreviewURLs).fireAndForget() + return .run { [state] _ in + await databaseClient + .updatePreviewURLs(gid: state.gallery.id, previewURLs: state.galleryPreviewURLs) + } case .syncGalleryComments: - return databaseClient - .updateComments(gid: state.gallery.id, comments: state.galleryComments).fireAndForget() + return .run { [state] _ in + await databaseClient.updateComments(gid: state.gallery.id, comments: state.galleryComments) + } case .syncGreeting(let greeting): - return databaseClient.updateGreeting(greeting).fireAndForget() + return .run(operation: { _ in await databaseClient.updateGreeting(greeting) }) case .syncPreviewConfig(let config): - return databaseClient - .updatePreviewConfig(gid: state.gallery.id, config: config).fireAndForget() + return .run { [state] _ in + await databaseClient.updatePreviewConfig(gid: state.gallery.id, config: config) + } case .saveGalleryHistory: - return databaseClient.updateLastOpenDate(gid: state.gallery.id).fireAndForget() + return .run { [state] _ in + await databaseClient.updateLastOpenDate(gid: state.gallery.id) + } case .updateReadingProgress(let progress): - return databaseClient - .updateReadingProgress(gid: state.gallery.id, progress: progress).fireAndForget() + return .run { [state] _ in + await databaseClient.updateReadingProgress(gid: state.gallery.id, progress: progress) + } case .teardown: - return .cancel(ids: CancelID.allCases) + return .merge(CancelID.allCases.map(Effect.cancel(id:))) case .fetchDatabaseInfos(let gid): guard let gallery = databaseClient.fetchGallery(gid: gid) else { return .none } @@ -236,34 +249,40 @@ struct DetailReducer: ReducerProtocol { state.galleryDetail = detail } return .merge( - .init(value: .saveGalleryHistory), - databaseClient.fetchGalleryState(gid: state.gallery.id) - .map(Action.fetchDatabaseInfosDone).cancellable(id: CancelID.fetchDatabaseInfos) + .send(.saveGalleryHistory), + .run { [galleryID = state.gallery.id] send in + guard let dbState = await databaseClient.fetchGalleryState(gid: galleryID) else { return } + await send(.fetchDatabaseInfosDone(dbState)) + } + .cancellable(id: CancelID.fetchDatabaseInfos) ) case .fetchDatabaseInfosDone(let galleryState): state.galleryTags = galleryState.tags state.galleryPreviewURLs = galleryState.previewURLs state.galleryComments = galleryState.comments - return .init(value: .fetchGalleryDetail) + return .send(.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.fetchGalleryDetail) + return .run { [galleryID = state.gallery.id] send in + let response = await GalleryDetailRequest(gid: galleryID, galleryURL: galleryURL).response() + await send(.fetchGalleryDetailDone(response)) + } + .cancellable(id: CancelID.fetchGalleryDetail) case .fetchGalleryDetailDone(let result): state.loadingState = .idle switch result { case .success(let (galleryDetail, galleryState, apiKey, greeting)): - var effects: [EffectTask] = [ - .init(value: .syncGalleryTags), - .init(value: .syncGalleryDetail), - .init(value: .syncGalleryPreviewURLs), - .init(value: .syncGalleryComments) + var effects: [Effect] = [ + .send(.syncGalleryTags), + .send(.syncGalleryDetail), + .send(.syncGalleryPreviewURLs), + .send(.syncGalleryComments) ] state.apiKey = apiKey state.galleryDetail = galleryDetail @@ -272,13 +291,13 @@ struct DetailReducer: ReducerProtocol { state.galleryComments = galleryState.comments state.userRating = Int(galleryDetail.userRating) * 2 if let greeting = greeting { - effects.append(.init(value: .syncGreeting(greeting))) + effects.append(.send(.syncGreeting(greeting))) if !greeting.gainedNothing && state.showsNewDawnGreeting { - effects.append(.init(value: .setNavigation(.newDawn(greeting)))) + effects.append(.send(.setNavigation(.newDawn(greeting)))) } } if let config = galleryState.previewConfig { - effects.append(.init(value: .syncPreviewConfig(config))) + effects.append(.send(.syncPreviewConfig(config))) } return .merge(effects) case .failure(let error): @@ -289,44 +308,76 @@ struct DetailReducer: ReducerProtocol { 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.rateGallery) + return .run { [state] send in + let response = await RateGalleryRequest( + apiuid: apiuid, + apikey: state.apiKey, + gid: gid, + token: state.gallery.token, + rating: state.userRating + ) + .response() + await send(.anyGalleryOpsDone(response)) + }.cancellable(id: CancelID.rateGallery) case .favorGallery(let favIndex): - return FavorGalleryRequest(gid: state.gallery.id, token: state.gallery.token, favIndex: favIndex) - .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID.favorGallery) + return .run { [state] send in + let response = await FavorGalleryRequest( + gid: state.gallery.id, + token: state.gallery.token, + favIndex: favIndex + ) + .response() + await send(.anyGalleryOpsDone(response)) + } + .cancellable(id: CancelID.favorGallery) case .unfavorGallery: - return UnfavorGalleryRequest(gid: state.gallery.id).effect.map(Action.anyGalleryOpsDone) - .cancellable(id: CancelID.unfavorGallery) + return .run { [galleryID = state.gallery.id] send in + let response = await UnfavorGalleryRequest(gid: galleryID).response() + await send(.anyGalleryOpsDone(response)) + } + .cancellable(id: CancelID.unfavorGallery) case .postComment(let galleryURL): guard !state.commentContent.isEmpty else { return .none } - return CommentGalleryRequest(content: state.commentContent, galleryURL: galleryURL) - .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID.postComment) + return .run { [commentContent = state.commentContent] send in + let response = await CommentGalleryRequest( + content: commentContent, galleryURL: galleryURL + ) + .response() + await send(.anyGalleryOpsDone(response)) + } + .cancellable(id: CancelID.postComment) case .voteTag(let tag, let vote): guard let apiuid = Int(cookieClient.apiuid), let gid = Int(state.gallery.id) else { return .none } - return VoteGalleryTagRequest( - apiuid: apiuid, apikey: state.apiKey, gid: gid, token: state.gallery.token, tag: tag, vote: vote - ) - .effect.map(Action.anyGalleryOpsDone).cancellable(id: CancelID.voteTag) + return .run { [state] send in + let response = await VoteGalleryTagRequest( + apiuid: apiuid, + apikey: state.apiKey, + gid: gid, + token: state.gallery.token, + tag: tag, + vote: vote + ) + .response() + await send(.anyGalleryOpsDone(response)) + } + .cancellable(id: CancelID.voteTag) case .anyGalleryOpsDone(let result): if case .success = result { return .merge( - .init(value: .fetchGalleryDetail), - .fireAndForget({ hapticsClient.generateNotificationFeedback(.success) }) + .send(.fetchGalleryDetail), + .run(operation: { _ in hapticsClient.generateNotificationFeedback(.success) }) ) } - return .fireAndForget({ hapticsClient.generateNotificationFeedback(.error) }) + return .run(operation: { _ in hapticsClient.generateNotificationFeedback(.error) }) case .reading(.onPerformDismiss): - return .init(value: .setNavigation(nil)) + return .send(.setNavigation(nil)) case .reading: return .none @@ -341,7 +392,7 @@ struct DetailReducer: ReducerProtocol { return .none case .comments(.performCommentActionDone(let result)): - return .init(value: .anyGalleryOpsDone(result)) + return .send(.anyGalleryOpsDone(result)) case .comments(.detail(let recursiveAction)): guard state.commentsState != nil else { return .none } diff --git a/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift b/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift index d1aa1d7b..87ccc722 100644 --- a/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift +++ b/EhPanda/View/Detail/DetailSearch/DetailSearchReducer.swift @@ -7,7 +7,7 @@ import ComposableArchitecture -struct DetailSearchReducer: ReducerProtocol { +struct DetailSearchReducer: Reducer { enum Route: Equatable { case filters case quickSearch @@ -64,13 +64,13 @@ struct DetailSearchReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient @Dependency(\.hapticsClient) private var hapticsClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding(\.$keyword): if !state.keyword.isEmpty { @@ -83,19 +83,19 @@ struct DetailSearchReducer: ReducerProtocol { case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: state.detailState = .init() state.filtersState = .init() state.quickDetailSearchState = .init() return .merge( - .init(value: .detail(.teardown)), - .init(value: .quickSearch(.teardown)) + .send(.detail(.teardown)), + .send(.quickSearch(.teardown)) ) case .teardown: - return .cancel(ids: CancelID.allCases) + return .merge(CancelID.allCases.map(Effect.cancel(id:))) case .fetchGalleries(let keyword): guard state.loadingState != .loading else { return .none } @@ -106,8 +106,11 @@ struct DetailSearchReducer: ReducerProtocol { 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.fetchGalleries) + return .run { [lastKeyword = state.lastKeyword] send in + let response = await SearchGalleriesRequest(keyword: lastKeyword, filter: filter).response() + await send(.fetchGalleriesDone(response)) + } + .cancellable(id: CancelID.fetchGalleries) case .fetchGalleriesDone(let result): state.loadingState = .idle @@ -116,11 +119,11 @@ struct DetailSearchReducer: ReducerProtocol { guard !galleries.isEmpty else { state.loadingState = .failed(.notFound) guard pageNumber.hasNextPage() else { return .none } - return .init(value: .fetchMoreGalleries) + return .send(.fetchMoreGalleries) } state.pageNumber = pageNumber state.galleries = galleries - return databaseClient.cacheGalleries(galleries).fireAndForget() + return .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) case .failure(let error): state.loadingState = .failed(error) } @@ -134,9 +137,14 @@ struct DetailSearchReducer: ReducerProtocol { 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.fetchMoreGalleries) + return .run { [lastKeyword = state.lastKeyword] send in + let response = await MoreSearchGalleriesRequest( + keyword: lastKeyword, filter: filter, lastID: lastID + ) + .response() + await send(.fetchMoreGalleriesDone(response)) + } + .cancellable(id: CancelID.fetchMoreGalleries) case .fetchMoreGalleriesDone(let result): state.footerLoadingState = .idle @@ -145,11 +153,11 @@ struct DetailSearchReducer: ReducerProtocol { state.pageNumber = pageNumber state.insertGalleries(galleries) - var effects: [EffectTask] = [ - databaseClient.cacheGalleries(galleries).fireAndForget() + var effects: [Effect] = [ + .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) ] if galleries.isEmpty, pageNumber.hasNextPage() { - effects.append(.init(value: .fetchMoreGalleries)) + effects.append(.send(.fetchMoreGalleries)) } else if !galleries.isEmpty { state.loadingState = .idle } diff --git a/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift b/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift index f8567c73..aca88137 100644 --- a/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift +++ b/EhPanda/View/Detail/DetailSearch/DetailSearchView.swift @@ -22,7 +22,7 @@ struct DetailSearchView: View { keyword: String, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.keyword = keyword self.user = user _setting = setting @@ -45,7 +45,7 @@ struct DetailSearchView: View { } ) .sheet( - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /DetailSearchReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in @@ -58,7 +58,7 @@ struct DetailSearchView: View { } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailSearchReducer.Route.quickSearch) { _ in + .sheet(unwrapping: viewStore.$route, case: /DetailSearchReducer.Route.quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickDetailSearchState, action: DetailSearchReducer.Action.quickSearch) ) { keyword in @@ -68,14 +68,14 @@ struct DetailSearchView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailSearchReducer.Route.filters) { _ in + .sheet(unwrapping: viewStore.$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)) + .searchable(text: viewStore.$keyword) .searchSuggestions { TagSuggestionView( - keyword: viewStore.binding(\.$keyword), translations: tagTranslator.translations, + keyword: viewStore.$keyword, translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion ) } @@ -96,7 +96,7 @@ struct DetailSearchView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailSearchReducer.Route.detail) { route in + NavigationLink(unwrapping: viewStore.$route, case: /DetailSearchReducer.Route.detail) { route in DetailView( store: store.scope(state: \.detailState, action: DetailSearchReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -122,10 +122,7 @@ struct DetailSearchView: View { struct DetailSearchView_Previews: PreviewProvider { static var previews: some View { DetailSearchView( - store: .init( - initialState: .init(), - reducer: DetailSearchReducer() - ), + store: .init(initialState: .init(), reducer: DetailSearchReducer.init), keyword: .init(), user: .init(), setting: .constant(.init()), diff --git a/EhPanda/View/Detail/DetailView.swift b/EhPanda/View/Detail/DetailView.swift index a5946b28..ec601b2c 100644 --- a/EhPanda/View/Detail/DetailView.swift +++ b/EhPanda/View/Detail/DetailView.swift @@ -24,7 +24,7 @@ struct DetailView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.gid = gid self.user = user _setting = setting @@ -121,7 +121,7 @@ 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: /DetailReducer.Route.reading) { _ in + .fullScreenCover(unwrapping: viewStore.$route, case: /DetailReducer.Route.reading) { _ in ReadingView( store: store.scope(state: \.readingState, action: DetailReducer.Action.reading), gid: gid, setting: $setting, blurRadius: blurRadius @@ -129,7 +129,7 @@ struct DetailView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.archives) { route in + .sheet(unwrapping: viewStore.$route, case: /DetailReducer.Route.archives) { route in let (galleryURL, archiveURL) = route.wrappedValue ArchivesView( store: store.scope(state: \.archivesState, action: DetailReducer.Action.archives), @@ -138,7 +138,7 @@ struct DetailView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.torrents) { _ in + .sheet(unwrapping: viewStore.$route, case: /DetailReducer.Route.torrents) { _ in TorrentsView( store: store.scope(state: \.torrentsState, action: DetailReducer.Action.torrents), gid: gid, token: viewStore.gallery.token, blurRadius: blurRadius @@ -146,15 +146,15 @@ struct DetailView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.share) { route in + .sheet(unwrapping: viewStore.$route, case: /DetailReducer.Route.share) { route in ActivityView(activityItems: [route.wrappedValue]) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.postComment) { _ in + .sheet(unwrapping: viewStore.$route, case: /DetailReducer.Route.postComment) { _ in PostCommentView( title: L10n.Localizable.PostCommentView.Title.postComment, - content: viewStore.binding(\.$commentContent), - isFocused: viewStore.binding(\.$postCommentFocused), + content: viewStore.$commentContent, + isFocused: viewStore.$postCommentFocused, postAction: { if let galleryURL = viewStore.gallery.galleryURL { viewStore.send(.postComment(galleryURL)) @@ -167,10 +167,10 @@ struct DetailView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.newDawn) { route in + .sheet(unwrapping: viewStore.$route, case: /DetailReducer.Route.newDawn) { route in NewDawnView(greeting: route.wrappedValue).autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.tagDetail) { route in + .sheet(unwrapping: viewStore.$route, case: /DetailReducer.Route.tagDetail) { route in TagDetailView(detail: route.wrappedValue).autoBlur(radius: blurRadius) } .animation(.default, value: viewStore.showsUserRating) @@ -189,13 +189,13 @@ struct DetailView: View { // MARK: NavigationLinks private extension DetailView { @ViewBuilder var navigationLinks: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.previews) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /DetailReducer.Route.previews) { _ in PreviewsView( store: store.scope(state: \.previewsState, action: DetailReducer.Action.previews), gid: gid, setting: $setting, blurRadius: blurRadius ) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.comments) { route in + NavigationLink(unwrapping: viewStore.$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, @@ -205,7 +205,7 @@ private extension DetailView { ) } } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.detailSearch) { route in + NavigationLink(unwrapping: viewStore.$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, @@ -213,7 +213,7 @@ private extension DetailView { ) } } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /DetailReducer.Route.galleryInfos) { route in + NavigationLink(unwrapping: viewStore.$route, case: /DetailReducer.Route.galleryInfos) { route in let (gallery, galleryDetail) = route.wrappedValue GalleryInfosView( store: store.scope(state: \.galleryInfosState, action: DetailReducer.Action.galleryInfos), @@ -847,10 +847,7 @@ struct DetailView_Previews: PreviewProvider { static var previews: some View { NavigationView { DetailView( - store: .init( - initialState: .init(), - reducer: DetailReducer() - ), + store: .init(initialState: .init(), reducer: DetailReducer.init), gid: .init(), user: .init(), setting: .constant(.init()), diff --git a/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift b/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift index 9433f90b..ed296218 100644 --- a/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift +++ b/EhPanda/View/Detail/GalleryInfos/GalleryInfosReducer.swift @@ -8,7 +8,7 @@ import TTProgressHUD import ComposableArchitecture -struct GalleryInfosReducer: ReducerProtocol { +struct GalleryInfosReducer: Reducer { enum Route { case hud } @@ -26,7 +26,7 @@ struct GalleryInfosReducer: ReducerProtocol { @Dependency(\.clipboardClient) private var clipboardClient @Dependency(\.hapticsClient) private var hapticsClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in @@ -37,8 +37,8 @@ struct GalleryInfosReducer: ReducerProtocol { case .copyText(let text): state.route = .hud return .merge( - clipboardClient.saveText(text).fireAndForget(), - .fireAndForget({ hapticsClient.generateNotificationFeedback(.success) }) + .run(operation: { _ in clipboardClient.saveText(text) }), + .run(operation: { _ in hapticsClient.generateNotificationFeedback(.success) }) ) } } diff --git a/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift b/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift index 09500be6..278dbae8 100644 --- a/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift +++ b/EhPanda/View/Detail/GalleryInfos/GalleryInfosView.swift @@ -16,7 +16,7 @@ struct GalleryInfosView: View { init(store: StoreOf, gallery: Gallery, galleryDetail: GalleryDetail) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.gallery = gallery self.galleryDetail = galleryDetail } @@ -118,7 +118,7 @@ struct GalleryInfosView: View { } .progressHUD( config: viewStore.hudConfig, - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /GalleryInfosReducer.Route.hud ) .navigationTitle(L10n.Localizable.GalleryInfosView.Title.galleryInfos) @@ -135,10 +135,7 @@ struct GalleryInfosView_Previews: PreviewProvider { static var previews: some View { NavigationView { GalleryInfosView( - store: .init( - initialState: .init(), - reducer: GalleryInfosReducer() - ), + store: .init(initialState: .init(), reducer: GalleryInfosReducer.init), gallery: .preview, galleryDetail: .preview ) diff --git a/EhPanda/View/Detail/Previews/PreviewsReducer.swift b/EhPanda/View/Detail/Previews/PreviewsReducer.swift index ec25a37b..a25881c6 100644 --- a/EhPanda/View/Detail/Previews/PreviewsReducer.swift +++ b/EhPanda/View/Detail/Previews/PreviewsReducer.swift @@ -8,7 +8,7 @@ import Foundation import ComposableArchitecture -struct PreviewsReducer: ReducerProtocol { +struct PreviewsReducer: Reducer { enum Route { case reading } @@ -56,41 +56,46 @@ struct PreviewsReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient @Dependency(\.hapticsClient) private var hapticsClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding: return .none case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: state.readingState = .init() - return .init(value: .reading(.teardown)) + return .send(.reading(.teardown)) case .syncPreviewURLs(let previewURLs): - return databaseClient - .updatePreviewURLs(gid: state.gallery.id, previewURLs: previewURLs).fireAndForget() + return .run { [state] _ in + await databaseClient.updatePreviewURLs(gid: state.gallery.id, previewURLs: previewURLs) + } case .updateReadingProgress(let progress): - return databaseClient - .updateReadingProgress(gid: state.gallery.id, progress: progress).fireAndForget() + return .run { [state] _ in + await databaseClient.updateReadingProgress(gid: state.gallery.id, progress: progress) + } case .teardown: - return .cancel(ids: CancelID.allCases) + return .merge(CancelID.allCases.map(Effect.cancel(id:))) case .fetchDatabaseInfos(let gid): guard let gallery = databaseClient.fetchGallery(gid: gid) else { return .none } state.gallery = gallery - return databaseClient.fetchGalleryState(gid: state.gallery.id) - .map(Action.fetchDatabaseInfosDone).cancellable(id: CancelID.fetchDatabaseInfos) + return .run { [state] send in + guard let dbState = await databaseClient.fetchGalleryState(gid: state.gallery.id) else { return } + await send(.fetchDatabaseInfosDone(dbState)) + } + .cancellable(id: CancelID.fetchDatabaseInfos) case .fetchDatabaseInfosDone(let galleryState): if let previewConfig = galleryState.previewConfig { @@ -106,8 +111,11 @@ struct PreviewsReducer: ReducerProtocol { 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.fetchPreviewURLs) + return .run { send in + let response = await GalleryPreviewURLsRequest(galleryURL: galleryURL, pageNum: pageNum).response() + await send(.fetchPreviewURLsDone(response)) + } + .cancellable(id: CancelID.fetchPreviewURLs) case .fetchPreviewURLsDone(let result): state.loadingState = .idle @@ -119,14 +127,14 @@ struct PreviewsReducer: ReducerProtocol { return .none } state.updatePreviewURLs(previewURLs) - return .init(value: .syncPreviewURLs(previewURLs)) + return .send(.syncPreviewURLs(previewURLs)) case .failure(let error): state.loadingState = .failed(error) } return .none case .reading(.onPerformDismiss): - return .init(value: .setNavigation(nil)) + return .send(.setNavigation(nil)) case .reading: return .none diff --git a/EhPanda/View/Detail/Previews/PreviewsView.swift b/EhPanda/View/Detail/Previews/PreviewsView.swift index 1a255c68..5fc13cb0 100644 --- a/EhPanda/View/Detail/Previews/PreviewsView.swift +++ b/EhPanda/View/Detail/Previews/PreviewsView.swift @@ -21,7 +21,7 @@ struct PreviewsView: View { gid: String, setting: Binding, blurRadius: Double ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.gid = gid _setting = setting self.blurRadius = blurRadius @@ -68,7 +68,7 @@ struct PreviewsView: View { .padding(.bottom) .id(viewStore.databaseLoadingState) } - .fullScreenCover(unwrapping: viewStore.binding(\.$route), case: /PreviewsReducer.Route.reading) { _ in + .fullScreenCover(unwrapping: viewStore.$route, case: /PreviewsReducer.Route.reading) { _ in ReadingView( store: store.scope(state: \.readingState, action: PreviewsReducer.Action.reading), gid: gid, setting: $setting, blurRadius: blurRadius @@ -87,10 +87,7 @@ struct PreviewsView_Previews: PreviewProvider { static var previews: some View { NavigationView { PreviewsView( - store: .init( - initialState: .init(gallery: .preview), - reducer: PreviewsReducer() - ), + store: .init(initialState: .init(gallery: .preview), reducer: PreviewsReducer.init), gid: .init(), setting: .constant(.init()), blurRadius: 0 diff --git a/EhPanda/View/Detail/Torrents/TorrentsReducer.swift b/EhPanda/View/Detail/Torrents/TorrentsReducer.swift index 24e2d304..7818e5cf 100644 --- a/EhPanda/View/Detail/Torrents/TorrentsReducer.swift +++ b/EhPanda/View/Detail/Torrents/TorrentsReducer.swift @@ -9,7 +9,7 @@ import Foundation import TTProgressHUD import ComposableArchitecture -struct TorrentsReducer: ReducerProtocol { +struct TorrentsReducer: Reducer { enum Route: Equatable { case hud case share(URL) @@ -44,7 +44,7 @@ struct TorrentsReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient @Dependency(\.fileClient) private var fileClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in @@ -59,34 +59,40 @@ struct TorrentsReducer: ReducerProtocol { case .copyText(let magnetURL): state.route = .hud return .merge( - clipboardClient.saveText(magnetURL).fireAndForget(), - .fireAndForget({ hapticsClient.generateNotificationFeedback(.success) }) + .run(operation: { _ in clipboardClient.saveText(magnetURL) }), + .run(operation: { _ in hapticsClient.generateNotificationFeedback(.success) }) ) case .presentTorrentActivity(let hash, let data): if let url = fileClient.saveTorrent(hash: hash, data: data) { - return .init(value: .setNavigation(.share(url))) + return .send(.setNavigation(.share(url))) } return .none case .fetchTorrent(let hash, let torrentURL): - return DataRequest(url: torrentURL).effect.map({ Action.fetchTorrentDone(hash, $0) }) - .cancellable(id: CancelID.fetchTorrent) + return .run { send in + let response = await DataRequest(url: torrentURL).response() + await send(.fetchTorrentDone(hash, response)) + } + .cancellable(id: CancelID.fetchTorrent) case .teardown: - return .cancel(ids: CancelID.allCases) + return .merge(CancelID.allCases.map(Effect.cancel(id:))) case .fetchTorrentDone(let hash, let result): if case .success(let data) = result, !data.isEmpty { - return .init(value: .presentTorrentActivity(hash, data)) + return .send(.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.fetchGalleryTorrents) + return .run { send in + let response = await GalleryTorrentsRequest(gid: gid, token: token).response() + await send(.fetchGalleryTorrentsDone(response)) + } + .cancellable(id: CancelID.fetchGalleryTorrents) case .fetchGalleryTorrentsDone(let result): state.loadingState = .idle diff --git a/EhPanda/View/Detail/Torrents/TorrentsView.swift b/EhPanda/View/Detail/Torrents/TorrentsView.swift index 5a8e1dc3..6eab016e 100644 --- a/EhPanda/View/Detail/Torrents/TorrentsView.swift +++ b/EhPanda/View/Detail/Torrents/TorrentsView.swift @@ -17,7 +17,7 @@ struct TorrentsView: View { init(store: StoreOf, gid: String, token: String, blurRadius: Double) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.gid = gid self.token = token self.blurRadius = blurRadius @@ -45,13 +45,13 @@ struct TorrentsView: View { } .opacity(error != nil && viewStore.torrents.isEmpty ? 1 : 0) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /TorrentsReducer.Route.share) { route in + .sheet(unwrapping: viewStore.$route, case: /TorrentsReducer.Route.share) { route in ActivityView(activityItems: [route.wrappedValue]) .autoBlur(radius: blurRadius) } .progressHUD( config: viewStore.hudConfig, - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /TorrentsReducer.Route.hud ) .animation(.default, value: viewStore.torrents) @@ -118,10 +118,7 @@ private extension TorrentsView { struct TorrentsView_Previews: PreviewProvider { static var previews: some View { TorrentsView( - store: .init( - initialState: .init(), - reducer: TorrentsReducer() - ), + store: .init(initialState: .init(), reducer: TorrentsReducer.init), gid: .init(), token: .init(), blurRadius: 0 diff --git a/EhPanda/View/Favorites/FavoritesReducer.swift b/EhPanda/View/Favorites/FavoritesReducer.swift index c2c694cf..1f2e4269 100644 --- a/EhPanda/View/Favorites/FavoritesReducer.swift +++ b/EhPanda/View/Favorites/FavoritesReducer.swift @@ -9,7 +9,7 @@ import SwiftUI import IdentifiedCollections import ComposableArchitecture -struct FavoritesReducer: ReducerProtocol { +struct FavoritesReducer: Reducer { enum Route: Equatable { case quickSearch case detail(String) @@ -75,29 +75,29 @@ struct FavoritesReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient @Dependency(\.hapticsClient) private var hapticsClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding: return .none case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .setFavoritesIndex(let index): state.index = index guard state.galleries?.isEmpty != false else { return .none } - return .init(value: Action.fetchGalleries()) + return .send(.fetchGalleries()) case .clearSubStates: state.detailState = .init() - return .init(value: .detail(.teardown)) + return .send(.detail(.teardown)) case .onNotLoginViewButtonTapped: return .none @@ -113,10 +113,13 @@ struct FavoritesReducer: ReducerProtocol { } 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) } + return .run { [state] send in + let response = await FavoritesGalleriesRequest( + favIndex: state.index, keyword: state.keyword, sortOrder: sortOrder + ) + .response() + await send(.fetchGalleriesDone(state.index, response)) + } case .fetchGalleriesDone(let targetFavIndex, let result): state.rawLoadingState[targetFavIndex] = .idle @@ -125,12 +128,12 @@ struct FavoritesReducer: ReducerProtocol { guard !galleries.isEmpty else { state.rawLoadingState[targetFavIndex] = .failed(.notFound) guard pageNumber.hasNextPage() else { return .none } - return .init(value: .fetchMoreGalleries) + return .send(.fetchMoreGalleries) } state.rawPageNumber[targetFavIndex] = pageNumber state.rawGalleries[targetFavIndex] = galleries state.sortOrder = sortOrder - return databaseClient.cacheGalleries(galleries).fireAndForget() + return .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) case .failure(let error): state.rawLoadingState[targetFavIndex] = .failed(error) } @@ -144,13 +147,16 @@ struct FavoritesReducer: ReducerProtocol { 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) } + return .run { [state] send in + let response = await MoreFavoritesGalleriesRequest( + favIndex: state.index, + lastID: lastID, + lastTimestamp: lastItemTimestamp, + keyword: state.keyword + ) + .response() + await send(.fetchMoreGalleriesDone(state.index, response)) + } case .fetchMoreGalleriesDone(let targetFavIndex, let result): state.rawFooterLoadingState[targetFavIndex] = .idle @@ -160,11 +166,11 @@ struct FavoritesReducer: ReducerProtocol { state.insertGalleries(index: targetFavIndex, galleries: galleries) state.sortOrder = sortOrder - var effects: [EffectTask] = [ - databaseClient.cacheGalleries(galleries).fireAndForget() + var effects: [Effect] = [ + .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) ] if galleries.isEmpty, pageNumber.hasNextPage() { - effects.append(.init(value: .fetchMoreGalleries)) + effects.append(.send(.fetchMoreGalleries)) } else if !galleries.isEmpty { state.rawLoadingState[targetFavIndex] = .idle } diff --git a/EhPanda/View/Favorites/FavoritesView.swift b/EhPanda/View/Favorites/FavoritesView.swift index fea27eea..4c1afaa0 100644 --- a/EhPanda/View/Favorites/FavoritesView.swift +++ b/EhPanda/View/Favorites/FavoritesView.swift @@ -22,7 +22,7 @@ struct FavoritesView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -56,7 +56,7 @@ struct FavoritesView: View { } } .sheet( - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /FavoritesReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in @@ -69,7 +69,7 @@ struct FavoritesView: View { } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /FavoritesReducer.Route.quickSearch) { _ in + .sheet(unwrapping: viewStore.$route, case: /FavoritesReducer.Route.quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickSearchState, action: FavoritesReducer.Action.quickSearch) ) { keyword in @@ -79,10 +79,10 @@ struct FavoritesView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .searchable(text: viewStore.binding(\.$keyword)) + .searchable(text: viewStore.$keyword) .searchSuggestions { TagSuggestionView( - keyword: viewStore.binding(\.$keyword), translations: tagTranslator.translations, + keyword: viewStore.$keyword, translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion ) } @@ -104,7 +104,7 @@ struct FavoritesView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /FavoritesReducer.Route.detail) { route in + NavigationLink(unwrapping: viewStore.$route, case: /FavoritesReducer.Route.detail) { route in DetailView( store: store.scope(state: \.detailState, action: FavoritesReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -135,10 +135,7 @@ struct FavoritesView: View { struct FavoritesView_Previews: PreviewProvider { static var previews: some View { FavoritesView( - store: .init( - initialState: .init(), - reducer: FavoritesReducer() - ), + store: .init(initialState: .init(), reducer: FavoritesReducer.init), user: .init(), setting: .constant(.init()), blurRadius: 0, diff --git a/EhPanda/View/Home/Frontpage/FrontpageReducer.swift b/EhPanda/View/Home/Frontpage/FrontpageReducer.swift index b4574355..9a80d42a 100644 --- a/EhPanda/View/Home/Frontpage/FrontpageReducer.swift +++ b/EhPanda/View/Home/Frontpage/FrontpageReducer.swift @@ -7,7 +7,7 @@ import ComposableArchitecture -struct FrontpageReducer: ReducerProtocol { +struct FrontpageReducer: Reducer { enum Route: Equatable { case filters case detail(String) @@ -64,37 +64,39 @@ struct FrontpageReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient @Dependency(\.hapticsClient) private var hapticsClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding: return .none case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: state.detailState = .init() state.filtersState = .init() - return .init(value: .detail(.teardown)) + return .send(.detail(.teardown)) case .teardown: - return .cancel(ids: CancelID.allCases) + return .merge(CancelID.allCases.map(Effect.cancel(id:))) 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.fetchGalleries) + return .run { send in + let response = await FrontpageGalleriesRequest(filter: filter).response() + await send(.fetchGalleriesDone(response)) + } + .cancellable(id: CancelID.fetchGalleries) case .fetchGalleriesDone(let result): state.loadingState = .idle @@ -103,11 +105,11 @@ struct FrontpageReducer: ReducerProtocol { guard !galleries.isEmpty else { state.loadingState = .failed(.notFound) guard pageNumber.hasNextPage() else { return .none } - return .init(value: .fetchMoreGalleries) + return .send(.fetchMoreGalleries) } state.pageNumber = pageNumber state.galleries = galleries - return databaseClient.cacheGalleries(galleries).fireAndForget() + return .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) case .failure(let error): state.loadingState = .failed(error) } @@ -121,9 +123,11 @@ struct FrontpageReducer: ReducerProtocol { 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.fetchMoreGalleries) + return .run { send in + let response = await MoreFrontpageGalleriesRequest(filter: filter, lastID: lastID).response() + await send(.fetchMoreGalleriesDone(response)) + } + .cancellable(id: CancelID.fetchMoreGalleries) case .fetchMoreGalleriesDone(let result): state.footerLoadingState = .idle @@ -132,11 +136,11 @@ struct FrontpageReducer: ReducerProtocol { state.pageNumber = pageNumber state.insertGalleries(galleries) - var effects: [EffectTask] = [ - databaseClient.cacheGalleries(galleries).fireAndForget() + var effects: [Effect] = [ + .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) ] if galleries.isEmpty, pageNumber.hasNextPage() { - effects.append(.init(value: .fetchMoreGalleries)) + effects.append(.send(.fetchMoreGalleries)) } else if !galleries.isEmpty { state.loadingState = .idle } diff --git a/EhPanda/View/Home/Frontpage/FrontpageView.swift b/EhPanda/View/Home/Frontpage/FrontpageView.swift index b82e137b..d9d651d7 100644 --- a/EhPanda/View/Home/Frontpage/FrontpageView.swift +++ b/EhPanda/View/Home/Frontpage/FrontpageView.swift @@ -22,7 +22,7 @@ struct FrontpageView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -44,7 +44,7 @@ struct FrontpageView: View { } ) .sheet( - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /FrontpageReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in @@ -57,11 +57,11 @@ struct FrontpageView: View { } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /FrontpageReducer.Route.filters) { _ in + .sheet(unwrapping: viewStore.$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) + .searchable(text: viewStore.$keyword, prompt: L10n.Localizable.Searchable.Prompt.filter) .onAppear { if viewStore.galleries.isEmpty { DispatchQueue.main.async { @@ -76,7 +76,7 @@ struct FrontpageView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /FrontpageReducer.Route.detail) { route in + NavigationLink(unwrapping: viewStore.$route, case: /FrontpageReducer.Route.detail) { route in DetailView( store: store.scope(state: \.detailState, action: FrontpageReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -98,10 +98,7 @@ struct FrontpageView_Previews: PreviewProvider { static var previews: some View { NavigationView { FrontpageView( - store: .init( - initialState: .init(), - reducer: FrontpageReducer() - ), + store: .init(initialState: .init(), reducer: FrontpageReducer.init), user: .init(), setting: .constant(.init()), blurRadius: 0, diff --git a/EhPanda/View/Home/History/HistoryReducer.swift b/EhPanda/View/Home/History/HistoryReducer.swift index 547275c7..4fc76884 100644 --- a/EhPanda/View/Home/History/HistoryReducer.swift +++ b/EhPanda/View/Home/History/HistoryReducer.swift @@ -8,7 +8,7 @@ import Foundation import ComposableArchitecture -struct HistoryReducer: ReducerProtocol { +struct HistoryReducer: Reducer { enum Route: Equatable { case detail(String) case clearHistory @@ -48,36 +48,41 @@ struct HistoryReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient @Dependency(\.hapticsClient) private var hapticsClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding: return .none case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: state.detailState = .init() - return .init(value: .detail(.teardown)) + return .send(.detail(.teardown)) case .clearHistoryGalleries: return .merge( - databaseClient.clearHistoryGalleries().fireAndForget(), - .init(value: .fetchGalleries) - .delay(for: .milliseconds(200), scheduler: DispatchQueue.main).eraseToEffect() + .run(operation: { _ in await databaseClient.clearHistoryGalleries() }), + .run { send in + try await Task.sleep(for: .milliseconds(200)) + await send(.fetchGalleries) + } ) case .fetchGalleries: guard state.loadingState != .loading else { return .none } state.loadingState = .loading - return databaseClient.fetchHistoryGalleries().map(Action.fetchGalleriesDone) + return .run { send in + let historyGalleries = await databaseClient.fetchHistoryGalleries() + await send(.fetchGalleriesDone(historyGalleries)) + } case .fetchGalleriesDone(let galleries): state.loadingState = .idle diff --git a/EhPanda/View/Home/History/HistoryView.swift b/EhPanda/View/Home/History/HistoryView.swift index 59e409bc..df6632f5 100644 --- a/EhPanda/View/Home/History/HistoryView.swift +++ b/EhPanda/View/Home/History/HistoryView.swift @@ -21,7 +21,7 @@ struct HistoryView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -42,7 +42,7 @@ struct HistoryView: View { } ) .sheet( - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /HistoryReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in @@ -55,7 +55,7 @@ struct HistoryView: View { } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .searchable(text: viewStore.binding(\.$keyword), prompt: L10n.Localizable.Searchable.Prompt.filter) + .searchable(text: viewStore.$keyword, prompt: L10n.Localizable.Searchable.Prompt.filter) .onAppear { if viewStore.galleries.isEmpty { DispatchQueue.main.async { @@ -70,7 +70,7 @@ struct HistoryView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /HistoryReducer.Route.detail) { route in + NavigationLink(unwrapping: viewStore.$route, case: /HistoryReducer.Route.detail) { route in DetailView( store: store.scope(state: \.detailState, action: HistoryReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -89,7 +89,7 @@ struct HistoryView: View { .disabled(viewStore.loadingState != .idle || viewStore.galleries.isEmpty) .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.clear, - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /HistoryReducer.Route.clearHistory ) { Button(L10n.Localizable.ConfirmationDialog.Button.clear, role: .destructive) { @@ -104,10 +104,7 @@ struct HistoryView_Previews: PreviewProvider { static var previews: some View { NavigationView { HistoryView( - store: .init( - initialState: .init(), - reducer: HistoryReducer() - ), + store: .init(initialState: .init(), reducer: HistoryReducer.init), user: .init(), setting: .constant(.init()), blurRadius: 0, diff --git a/EhPanda/View/Home/HomeReducer.swift b/EhPanda/View/Home/HomeReducer.swift index 0089bd72..8c5bf2b1 100644 --- a/EhPanda/View/Home/HomeReducer.swift +++ b/EhPanda/View/Home/HomeReducer.swift @@ -10,7 +10,7 @@ import Kingfisher import UIImageColors import ComposableArchitecture -struct HomeReducer: ReducerProtocol { +struct HomeReducer: Reducer { enum Route: Equatable, Hashable { case detail(String) case misc(HomeMiscGridType) @@ -93,28 +93,29 @@ struct HomeReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient @Dependency(\.libraryClient) private var libraryClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.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() + return .run { send in + try await Task.sleep(for: .milliseconds(300)) + await send(.setAllowsCardHitTesting(true)) + } case .binding: return .none case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: state.frontpageState = .init() @@ -124,11 +125,11 @@ struct HomeReducer: ReducerProtocol { 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)) + .send(.frontpage(.teardown)), + .send(.toplists(.teardown)), + .send(.popular(.teardown)), + .send(.watched(.teardown)), + .send(.detail(.teardown)) ) case .setAllowsCardHitTesting(let isAllowed): @@ -137,15 +138,16 @@ struct HomeReducer: ReducerProtocol { case .fetchAllGalleries: return .merge( - .init(value: .fetchPopularGalleries), - .init(value: .fetchFrontpageGalleries), - .init(value: .fetchAllToplistsGalleries) + .send(.fetchPopularGalleries), + .send(.fetchFrontpageGalleries), + .send(.fetchAllToplistsGalleries) ) case .fetchAllToplistsGalleries: return .merge( - ToplistsType.allCases.map({ Action.fetchToplistsGalleries($0.categoryIndex) }) - .map(EffectTask.init) + ToplistsType.allCases + .map { Action.fetchToplistsGalleries($0.categoryIndex) } + .map(Effect.send) ) case .fetchPopularGalleries: @@ -153,8 +155,10 @@ struct HomeReducer: ReducerProtocol { state.popularLoadingState = .loading state.rawCardColors = [String: [Color]]() let filter = databaseClient.fetchFilterSynchronously(range: .global) - return PopularGalleriesRequest(filter: filter) - .effect.map(Action.fetchPopularGalleriesDone) + return .run { send in + let response = await PopularGalleriesRequest(filter: filter).response() + await send(.fetchPopularGalleriesDone(response)) + } case .fetchPopularGalleriesDone(let result): state.popularLoadingState = .idle @@ -165,7 +169,7 @@ struct HomeReducer: ReducerProtocol { return .none } state.setPopularGalleries(galleries) - return databaseClient.cacheGalleries(galleries).fireAndForget() + return .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) case .failure(let error): state.popularLoadingState = .failed(error) } @@ -175,8 +179,10 @@ struct HomeReducer: ReducerProtocol { guard state.frontpageLoadingState != .loading else { return .none } state.frontpageLoadingState = .loading let filter = databaseClient.fetchFilterSynchronously(range: .global) - return FrontpageGalleriesRequest(filter: filter) - .effect.map(Action.fetchFrontpageGalleriesDone) + return .run { send in + let response = await FrontpageGalleriesRequest(filter: filter).response() + await send(.fetchFrontpageGalleriesDone(response)) + } case .fetchFrontpageGalleriesDone(let result): state.frontpageLoadingState = .idle @@ -187,7 +193,7 @@ struct HomeReducer: ReducerProtocol { return .none } state.setFrontpageGalleries(galleries) - return databaseClient.cacheGalleries(galleries).fireAndForget() + return .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) case .failure(let error): state.frontpageLoadingState = .failed(error) } @@ -196,8 +202,10 @@ struct HomeReducer: ReducerProtocol { 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) }) + return .run { send in + let response = await ToplistsGalleriesRequest(catIndex: index, pageNum: pageNum).response() + await send(.fetchToplistsGalleriesDone(index, response)) + } case .fetchToplistsGalleriesDone(let index, let result): state.toplistsLoadingState[index] = .idle @@ -208,7 +216,7 @@ struct HomeReducer: ReducerProtocol { return .none } state.toplistsGalleries[index] = galleries - return databaseClient.cacheGalleries(galleries).fireAndForget() + return .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) case .failure(let error): state.toplistsLoadingState[index] = .failed(error) } @@ -216,8 +224,10 @@ struct HomeReducer: ReducerProtocol { 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) }) + return .run { send in + let colors = await libraryClient.analyzeImageColors(result.image) + await send(.analyzeImageColorsDone(gid, colors)) + } case .analyzeImageColorsDone(let gid, let colors): if let colors = colors { diff --git a/EhPanda/View/Home/HomeView.swift b/EhPanda/View/Home/HomeView.swift index 922ee525..1330ed72 100644 --- a/EhPanda/View/Home/HomeView.swift +++ b/EhPanda/View/Home/HomeView.swift @@ -24,7 +24,7 @@ struct HomeView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -40,7 +40,7 @@ struct HomeView: View { if !viewStore.popularGalleries.isEmpty { CardSlideSection( galleries: viewStore.popularGalleries, - pageIndex: viewStore.binding(\.$cardPageIndex), + pageIndex: viewStore.$cardPageIndex, currentID: viewStore.currentCardID, colors: viewStore.cardColors, navigateAction: navigateTo(gid:), @@ -88,7 +88,7 @@ struct HomeView: View { .zIndex(1) } .sheet( - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /HomeReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in @@ -136,7 +136,7 @@ private extension HomeView { sectionLink } var detailViewLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /HomeReducer.Route.detail) { route in + NavigationLink(unwrapping: viewStore.$route, case: /HomeReducer.Route.detail) { route in DetailView( store: store.scope(state: \.detailState, action: HomeReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -145,7 +145,7 @@ private extension HomeView { } } var miscGridLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /HomeReducer.Route.misc) { route in + NavigationLink(unwrapping: viewStore.$route, case: /HomeReducer.Route.misc) { route in switch route.wrappedValue { case .popular: PopularView( @@ -166,7 +166,7 @@ private extension HomeView { } } var sectionLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /HomeReducer.Route.section) { route in + NavigationLink(unwrapping: viewStore.$route, case: /HomeReducer.Route.section) { route in switch route.wrappedValue { case .frontpage: FrontpageView( @@ -519,10 +519,7 @@ enum HomeSectionType: String, CaseIterable, Identifiable { struct HomeView_Previews: PreviewProvider { static var previews: some View { HomeView( - store: .init( - initialState: .init(), - reducer: HomeReducer() - ), + store: .init(initialState: .init(), reducer: HomeReducer.init), user: .init(), setting: .constant(.init()), blurRadius: 0, diff --git a/EhPanda/View/Home/Popular/PopularReducer.swift b/EhPanda/View/Home/Popular/PopularReducer.swift index a6141703..619aaa4e 100644 --- a/EhPanda/View/Home/Popular/PopularReducer.swift +++ b/EhPanda/View/Home/Popular/PopularReducer.swift @@ -7,7 +7,7 @@ import ComposableArchitecture -struct PopularReducer: ReducerProtocol { +struct PopularReducer: Reducer { enum Route: Equatable { case filters case detail(String) @@ -52,25 +52,25 @@ struct PopularReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient @Dependency(\.hapticsClient) private var hapticsClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding: return .none case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: state.detailState = .init() state.filtersState = .init() - return .init(value: .detail(.teardown)) + return .send(.detail(.teardown)) case .teardown: return .cancel(id: CancelID.fetchGalleries) @@ -79,8 +79,11 @@ struct PopularReducer: ReducerProtocol { 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.fetchGalleries) + return .run { send in + let response = await PopularGalleriesRequest(filter: filter).response() + await send(.fetchGalleriesDone(response)) + } + .cancellable(id: CancelID.fetchGalleries) case .fetchGalleriesDone(let result): state.loadingState = .idle @@ -91,7 +94,7 @@ struct PopularReducer: ReducerProtocol { return .none } state.galleries = galleries - return databaseClient.cacheGalleries(galleries).fireAndForget() + return .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) case .failure(let error): state.loadingState = .failed(error) } diff --git a/EhPanda/View/Home/Popular/PopularView.swift b/EhPanda/View/Home/Popular/PopularView.swift index 77a85236..220388d7 100644 --- a/EhPanda/View/Home/Popular/PopularView.swift +++ b/EhPanda/View/Home/Popular/PopularView.swift @@ -21,7 +21,7 @@ struct PopularView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -41,7 +41,7 @@ struct PopularView: View { } ) .sheet( - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /PopularReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in @@ -54,11 +54,11 @@ struct PopularView: View { } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /PopularReducer.Route.filters) { _ in + .sheet(unwrapping: viewStore.$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) + .searchable(text: viewStore.$keyword, prompt: L10n.Localizable.Searchable.Prompt.filter) .onAppear { if viewStore.galleries.isEmpty { DispatchQueue.main.async { @@ -73,7 +73,7 @@ struct PopularView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /PopularReducer.Route.detail) { route in + NavigationLink(unwrapping: viewStore.$route, case: /PopularReducer.Route.detail) { route in DetailView( store: store.scope(state: \.detailState, action: PopularReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -95,10 +95,7 @@ struct PopularView_Previews: PreviewProvider { static var previews: some View { NavigationView { PopularView( - store: .init( - initialState: .init(), - reducer: PopularReducer() - ), + store: .init(initialState: .init(), reducer: PopularReducer.init), user: .init(), setting: .constant(.init()), blurRadius: 0, diff --git a/EhPanda/View/Home/Toplists/ToplistsReducer.swift b/EhPanda/View/Home/Toplists/ToplistsReducer.swift index 6497de5b..f6d56777 100644 --- a/EhPanda/View/Home/Toplists/ToplistsReducer.swift +++ b/EhPanda/View/Home/Toplists/ToplistsReducer.swift @@ -7,7 +7,7 @@ import ComposableArchitecture -struct ToplistsReducer: ReducerProtocol { +struct ToplistsReducer: Reducer { enum Route: Equatable { case detail(String) } @@ -85,13 +85,13 @@ struct ToplistsReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient @Dependency(\.hapticsClient) private var hapticsClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding(\.$jumpPageAlertPresented): if !state.jumpPageAlertPresented { @@ -104,35 +104,35 @@ struct ToplistsReducer: ReducerProtocol { case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .setToplistsType(let type): state.type = type guard state.galleries?.isEmpty != false else { return .none } - return .init(value: Action.fetchGalleries()) + return .send(.fetchGalleries()) case .clearSubStates: state.detailState = .init() - return .init(value: .detail(.teardown)) + return .send(.detail(.teardown)) case .performJumpPage: guard let index = Int(state.jumpPageIndex), let pageNumber = state.pageNumber, index > 0, index <= pageNumber.maximum + 1 else { - return .fireAndForget({ hapticsClient.generateNotificationFeedback(.error) }) + return .run(operation: { _ in hapticsClient.generateNotificationFeedback(.error) }) } - return .init(value: .fetchGalleries(index - 1)) + return .send(.fetchGalleries(index - 1)) case .presentJumpPageAlert: state.jumpPageAlertPresented = true - return .fireAndForget({ hapticsClient.generateFeedback(.light) }) + return .run(operation: { _ in hapticsClient.generateFeedback(.light) }) case .setJumpPageAlertFocused(let isFocused): state.jumpPageAlertFocused = isFocused return .none case .teardown: - return .cancel(ids: CancelID.allCases) + return .merge(CancelID.allCases.map(Effect.cancel(id:))) case .fetchGalleries(let pageNum): guard state.loadingState != .loading else { return .none } @@ -142,9 +142,14 @@ struct ToplistsReducer: ReducerProtocol { } 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.fetchGalleries) + return .run { [type = state.type] send in + let response = await ToplistsGalleriesRequest( + catIndex: type.categoryIndex, pageNum: pageNum + ) + .response() + await send(.fetchGalleriesDone(type, response)) + } + .cancellable(id: CancelID.fetchGalleries) case .fetchGalleriesDone(let type, let result): state.rawLoadingState[type] = .idle @@ -153,11 +158,11 @@ struct ToplistsReducer: ReducerProtocol { guard !galleries.isEmpty else { state.rawLoadingState[type] = .failed(.notFound) guard pageNumber.hasNextPage() else { return .none } - return .init(value: .fetchMoreGalleries) + return .send(.fetchMoreGalleries) } state.rawPageNumber[type] = pageNumber state.rawGalleries[type] = galleries - return databaseClient.cacheGalleries(galleries).fireAndForget() + return .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) case .failure(let error): state.rawLoadingState[type] = .failed(error) } @@ -170,9 +175,14 @@ struct ToplistsReducer: ReducerProtocol { 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.fetchMoreGalleries) + return .run { [type = state.type] send in + let response = await MoreToplistsGalleriesRequest( + catIndex: type.categoryIndex, pageNum: pageNum + ) + .response() + await send(.fetchMoreGalleriesDone(type, response)) + } + .cancellable(id: CancelID.fetchMoreGalleries) case .fetchMoreGalleriesDone(let type, let result): state.rawFooterLoadingState[type] = .idle @@ -181,11 +191,11 @@ struct ToplistsReducer: ReducerProtocol { state.rawPageNumber[type] = pageNumber state.insertGalleries(type: type, galleries: galleries) - var effects: [EffectTask] = [ - databaseClient.cacheGalleries(galleries).fireAndForget() + var effects: [Effect] = [ + .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) ] if galleries.isEmpty, pageNumber.hasNextPage() { - effects.append(.init(value: .fetchMoreGalleries)) + effects.append(.send(.fetchMoreGalleries)) } else if !galleries.isEmpty { state.rawLoadingState[type] = .idle } diff --git a/EhPanda/View/Home/Toplists/ToplistsView.swift b/EhPanda/View/Home/Toplists/ToplistsView.swift index e68d917a..a58f0bee 100644 --- a/EhPanda/View/Home/Toplists/ToplistsView.swift +++ b/EhPanda/View/Home/Toplists/ToplistsView.swift @@ -21,7 +21,7 @@ struct ToplistsView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -47,7 +47,7 @@ struct ToplistsView: View { } ) .sheet( - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /ToplistsReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in @@ -61,13 +61,13 @@ struct ToplistsView: View { .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } .jumpPageAlert( - index: viewStore.binding(\.$jumpPageIndex), - isPresented: viewStore.binding(\.$jumpPageAlertPresented), - isFocused: viewStore.binding(\.$jumpPageAlertFocused), + index: viewStore.$jumpPageIndex, + isPresented: viewStore.$jumpPageAlertPresented, + isFocused: viewStore.$jumpPageAlertFocused, pageNumber: viewStore.pageNumber ?? .init(), jumpAction: { viewStore.send(.performJumpPage) } ) - .searchable(text: viewStore.binding(\.$keyword), prompt: L10n.Localizable.Searchable.Prompt.filter) + .searchable(text: viewStore.$keyword, prompt: L10n.Localizable.Searchable.Prompt.filter) .navigationBarBackButtonHidden(viewStore.jumpPageAlertPresented) .animation(.default, value: viewStore.jumpPageAlertPresented) .onAppear { @@ -84,7 +84,7 @@ struct ToplistsView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /ToplistsReducer.Route.detail) { route in + NavigationLink(unwrapping: viewStore.$route, case: /ToplistsReducer.Route.detail) { route in DetailView( store: store.scope(state: \.detailState, action: ToplistsReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -153,10 +153,7 @@ struct ToplistsView_Previews: PreviewProvider { static var previews: some View { NavigationView { ToplistsView( - store: .init( - initialState: .init(), - reducer: ToplistsReducer() - ), + store: .init(initialState: .init(), reducer: ToplistsReducer.init), user: .init(), setting: .constant(.init()), blurRadius: 0, diff --git a/EhPanda/View/Home/Watched/WatchedReducer.swift b/EhPanda/View/Home/Watched/WatchedReducer.swift index 6f583d64..50eb9865 100644 --- a/EhPanda/View/Home/Watched/WatchedReducer.swift +++ b/EhPanda/View/Home/Watched/WatchedReducer.swift @@ -7,7 +7,7 @@ import ComposableArchitecture -struct WatchedReducer: ReducerProtocol { +struct WatchedReducer: Reducer { enum Route: Equatable { case filters case quickSearch @@ -64,35 +64,35 @@ struct WatchedReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient @Dependency(\.hapticsClient) private var hapticsClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding: return .none case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: state.detailState = .init() state.filtersState = .init() state.quickSearchState = .init() return .merge( - .init(value: .detail(.teardown)), - .init(value: .quickSearch(.teardown)) + .send(.detail(.teardown)), + .send(.quickSearch(.teardown)) ) case .onNotLoginViewButtonTapped: return .none case .teardown: - return .cancel(ids: CancelID.allCases) + return .merge(CancelID.allCases.map(Effect.cancel(id:))) case .fetchGalleries(let keyword): guard state.loadingState != .loading else { return .none } @@ -102,8 +102,11 @@ struct WatchedReducer: ReducerProtocol { 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.fetchGalleries) + return .run { [keyword = state.keyword] send in + let response = await WatchedGalleriesRequest(filter: filter, keyword: keyword).response() + await send(.fetchGalleriesDone(response)) + } + .cancellable(id: CancelID.fetchGalleries) case .fetchGalleriesDone(let result): state.loadingState = .idle @@ -112,11 +115,11 @@ struct WatchedReducer: ReducerProtocol { guard !galleries.isEmpty else { state.loadingState = .failed(.notFound) guard pageNumber.hasNextPage() else { return .none } - return .init(value: .fetchMoreGalleries) + return .send(.fetchMoreGalleries) } state.pageNumber = pageNumber state.galleries = galleries - return databaseClient.cacheGalleries(galleries).fireAndForget() + return .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) case .failure(let error): state.loadingState = .failed(error) } @@ -130,9 +133,14 @@ struct WatchedReducer: ReducerProtocol { 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.fetchMoreGalleries) + return .run { [keyword = state.keyword] send in + let response = await MoreWatchedGalleriesRequest( + filter: filter, lastID: lastID, keyword: keyword + ) + .response() + await send(.fetchMoreGalleriesDone(response)) + } + .cancellable(id: CancelID.fetchMoreGalleries) case .fetchMoreGalleriesDone(let result): state.footerLoadingState = .idle @@ -141,11 +149,11 @@ struct WatchedReducer: ReducerProtocol { state.pageNumber = pageNumber state.insertGalleries(galleries) - var effects: [EffectTask] = [ - databaseClient.cacheGalleries(galleries).fireAndForget() + var effects: [Effect] = [ + .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) ] if galleries.isEmpty, pageNumber.hasNextPage() { - effects.append(.init(value: .fetchMoreGalleries)) + effects.append(.send(.fetchMoreGalleries)) } else if !galleries.isEmpty { state.loadingState = .idle } diff --git a/EhPanda/View/Home/Watched/WatchedView.swift b/EhPanda/View/Home/Watched/WatchedView.swift index 1612608b..16876b84 100644 --- a/EhPanda/View/Home/Watched/WatchedView.swift +++ b/EhPanda/View/Home/Watched/WatchedView.swift @@ -21,7 +21,7 @@ struct WatchedView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -49,7 +49,7 @@ struct WatchedView: View { } } .sheet( - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /WatchedReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in @@ -62,7 +62,7 @@ struct WatchedView: View { } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /WatchedReducer.Route.quickSearch) { _ in + .sheet(unwrapping: viewStore.$route, case: /WatchedReducer.Route.quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickSearchState, action: WatchedReducer.Action.quickSearch) ) { keyword in @@ -72,14 +72,14 @@ struct WatchedView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /WatchedReducer.Route.filters) { _ in + .sheet(unwrapping: viewStore.$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)) + .searchable(text: viewStore.$keyword) .searchSuggestions { TagSuggestionView( - keyword: viewStore.binding(\.$keyword), translations: tagTranslator.translations, + keyword: viewStore.$keyword, translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion ) } @@ -100,7 +100,7 @@ struct WatchedView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /WatchedReducer.Route.detail) { route in + NavigationLink(unwrapping: viewStore.$route, case: /WatchedReducer.Route.detail) { route in DetailView( store: store.scope(state: \.detailState, action: WatchedReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -127,10 +127,7 @@ struct WatchedView_Previews: PreviewProvider { static var previews: some View { NavigationView { WatchedView( - store: .init( - initialState: .init(), - reducer: WatchedReducer() - ), + store: .init(initialState: .init(), reducer: WatchedReducer.init), user: .init(), setting: .constant(.init()), blurRadius: 0, diff --git a/EhPanda/View/Migration/MigrationReducer.swift b/EhPanda/View/Migration/MigrationReducer.swift index 3488ed04..ceb490a1 100644 --- a/EhPanda/View/Migration/MigrationReducer.swift +++ b/EhPanda/View/Migration/MigrationReducer.swift @@ -8,7 +8,7 @@ import Foundation import ComposableArchitecture -struct MigrationReducer: ReducerProtocol { +struct MigrationReducer: Reducer { enum Route: Equatable { case dropDialog } @@ -31,7 +31,7 @@ struct MigrationReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in @@ -47,7 +47,10 @@ struct MigrationReducer: ReducerProtocol { return .none case .prepareDatabase: - return databaseClient.prepareDatabase().map(Action.prepareDatabaseDone) + return .run { send in + let result = await databaseClient.prepareDatabase() + await send(.prepareDatabaseDone(result.error)) + } case .prepareDatabaseDone(let appError): if let appError { @@ -55,14 +58,16 @@ struct MigrationReducer: ReducerProtocol { return .none } else { state.databaseState = .idle - return .init(value: .onDatabasePreparationSuccess) + return .send(.onDatabasePreparationSuccess) } case .dropDatabase: state.databaseState = .loading - return databaseClient.dropDatabase() - .delay(for: .milliseconds(500), scheduler: DispatchQueue.main) - .eraseToEffect().map(Action.dropDatabaseDone) + return .run { send in + try await Task.sleep(for: .milliseconds(500)) + let result = await databaseClient.dropDatabase() + await send(.dropDatabaseDone(result.error)) + } case .dropDatabaseDone(let appError): if let appError { @@ -70,9 +75,20 @@ struct MigrationReducer: ReducerProtocol { return .none } else { state.databaseState = .idle - return .init(value: .onDatabasePreparationSuccess) + return .send(.onDatabasePreparationSuccess) } } } } } + +private extension Result { + var error: Failure? { + switch self { + case .success: + return nil + case let .failure(error): + return error + } + } +} diff --git a/EhPanda/View/Migration/MigrationView.swift b/EhPanda/View/Migration/MigrationView.swift index 5a2984f1..ed316acf 100644 --- a/EhPanda/View/Migration/MigrationView.swift +++ b/EhPanda/View/Migration/MigrationView.swift @@ -19,7 +19,7 @@ struct MigrationView: View { init(store: StoreOf) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) } var body: some View { @@ -36,7 +36,7 @@ struct MigrationView: View { } .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.dropDatabase, - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /MigrationReducer.Route.dropDialog ) { Button(L10n.Localizable.ConfirmationDialog.Button.dropDatabase, role: .destructive) { @@ -53,11 +53,6 @@ struct MigrationView: View { struct MigrationView_Previews: PreviewProvider { static var previews: some View { - MigrationView( - store: .init( - initialState: .init(), - reducer: MigrationReducer() - ) - ) + MigrationView(store: .init(initialState: .init(), reducer: MigrationReducer.init)) } } diff --git a/EhPanda/View/Reading/ReadingReducer.swift b/EhPanda/View/Reading/ReadingReducer.swift index 96b5560b..1197a095 100644 --- a/EhPanda/View/Reading/ReadingReducer.swift +++ b/EhPanda/View/Reading/ReadingReducer.swift @@ -9,7 +9,7 @@ import SwiftUI import TTProgressHUD import ComposableArchitecture -struct ReadingReducer: ReducerProtocol { +struct ReadingReducer: Reducer { enum Route: Equatable { case hud case share(ShareItem) @@ -184,13 +184,13 @@ struct ReadingReducer: ReducerProtocol { @Dependency(\.imageClient) private var imageClient @Dependency(\.urlClient) private var urlClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$showsSliderPreview): - return .fireAndForget({ hapticsClient.generateFeedback(.soft) }) + return .run(operation: { _ in hapticsClient.generateFeedback(.soft) }) case .binding: return .none @@ -204,24 +204,24 @@ struct ReadingReducer: ReducerProtocol { return .none case .setOrientationPortrait(let isPortrait): - var effects = [EffectTask]() + var effects = [Effect]() if isPortrait { - effects.append(appDelegateClient.setPortraitOrientationMask().fireAndForget()) - effects.append(appDelegateClient.setPortraitOrientation().fireAndForget()) + effects.append(.run(operation: { _ in appDelegateClient.setPortraitOrientationMask() })) + effects.append(.run(operation: { _ in await appDelegateClient.setPortraitOrientation() })) } else { - effects.append(appDelegateClient.setAllOrientationMask().fireAndForget()) + effects.append(.run(operation: { _ in appDelegateClient.setAllOrientationMask() })) } return .merge(effects) case .onPerformDismiss: - return .fireAndForget({ hapticsClient.generateFeedback(.light) }) + return .run(operation: { _ in hapticsClient.generateFeedback(.light) }) case .onAppear(let gid, let enablesLandscape): - var effects: [EffectTask] = [ - .init(value: .fetchDatabaseInfos(gid)) + var effects: [Effect] = [ + .send(.fetchDatabaseInfos(gid)) ] if enablesLandscape { - effects.append(.init(value: .setOrientationPortrait(false))) + effects.append(.send(.setOrientationPortrait(false))) } return .merge(effects) @@ -247,7 +247,9 @@ struct ReadingReducer: ReducerProtocol { state.mpvImageKeys = .init() state.mpvSkipServerIdentifiers = .init() state.forceRefreshID = .init() - return databaseClient.removeImageURLs(gid: state.gallery.id).fireAndForget() + return .run { [state] _ in + await databaseClient.removeImageURLs(gid: state.gallery.id) + } case .retryAllFailedWebImages: state.imageURLLoadingStates.forEach { (index, loadingState) in @@ -263,22 +265,24 @@ struct ReadingReducer: ReducerProtocol { return .none case .copyImage(let imageURL): - return .init(value: .fetchImage(.copy(imageURL.isGIF), imageURL)) + return .send(.fetchImage(.copy(imageURL.isGIF), imageURL)) case .saveImage(let imageURL): - return .init(value: .fetchImage(.save(imageURL.isGIF), imageURL)) + return .send(.fetchImage(.save(imageURL.isGIF), imageURL)) case .saveImageDone(let isSucceeded): state.hudConfig = isSucceeded ? .savedToPhotoLibrary : .error - return .init(value: .setNavigation(.hud)) + return .send(.setNavigation(.hud)) case .shareImage(let imageURL): - return .init(value: .fetchImage(.share(imageURL.isGIF), imageURL)) + return .send(.fetchImage(.share(imageURL.isGIF), imageURL)) case .fetchImage(let action, let imageURL): - return imageClient.fetchImage(url: imageURL) - .map({ Action.fetchImageDone(action, $0) }) - .cancellable(id: CancelID.fetchImage) + return .run { send in + let result = await imageClient.fetchImage(url: imageURL) + await send(.fetchImageDone(action, result)) + } + .cancellable(id: CancelID.fetchImage) case .fetchImageDone(let action, let result): if case .success(let image) = result { @@ -286,47 +290,56 @@ struct ReadingReducer: ReducerProtocol { case .copy(let isAnimated): state.hudConfig = .copiedToClipboardSucceeded return .merge( - .init(value: .setNavigation(.hud)), - clipboardClient.saveImage(image, isAnimated).fireAndForget() + .send(.setNavigation(.hud)), + .run(operation: { _ in clipboardClient.saveImage(image, isAnimated) }) ) case .save(let isAnimated): - return imageClient - .saveImageToPhotoLibrary(image, isAnimated).map(Action.saveImageDone) + return .run { send in + let success = await imageClient.saveImageToPhotoLibrary(image, isAnimated) + await send(.saveImageDone(success)) + } case .share(let isAnimated): if isAnimated, let data = image.kf.data(format: .GIF) { - return .init(value: .setNavigation(.share(.data(data)))) + return .send(.setNavigation(.share(.data(data)))) } else { - return .init(value: .setNavigation(.share(.image(image)))) + return .send(.setNavigation(.share(.image(image)))) } } } else { state.hudConfig = .error - return .init(value: .setNavigation(.hud)) + return .send(.setNavigation(.hud)) } case .syncReadingProgress(let progress): - return databaseClient - .updateReadingProgress(gid: state.gallery.id, progress: progress).fireAndForget() + return .run { [state] _ in + await databaseClient.updateReadingProgress(gid: state.gallery.id, progress: progress) + } case .syncPreviewURLs(let previewURLs): - return databaseClient - .updatePreviewURLs(gid: state.gallery.id, previewURLs: previewURLs).fireAndForget() + return .run { [state] _ in + await databaseClient.updatePreviewURLs(gid: state.gallery.id, previewURLs: previewURLs) + } case .syncThumbnailURLs(let thumbnailURLs): - return databaseClient - .updateThumbnailURLs(gid: state.gallery.id, thumbnailURLs: thumbnailURLs).fireAndForget() + return .run { [state] _ in + await databaseClient.updateThumbnailURLs(gid: state.gallery.id, thumbnailURLs: thumbnailURLs) + } case .syncImageURLs(let imageURLs, let originalImageURLs): - return databaseClient - .updateImageURLs(gid: state.gallery.id, imageURLs: imageURLs, originalImageURLs: originalImageURLs) - .fireAndForget() + return .run { [state] _ in + await databaseClient.updateImageURLs( + gid: state.gallery.id, + imageURLs: imageURLs, + originalImageURLs: originalImageURLs + ) + } case .teardown: - var effects: [EffectTask] = [ - .cancel(ids: CancelID.allCases) + var effects: [Effect] = [ + .merge(CancelID.allCases.map(Effect.cancel(id:))) ] if !deviceClient.isPad() { - effects.append(.init(value: .setOrientationPortrait(true))) + effects.append(.send(.setOrientationPortrait(true))) } return .merge(effects) @@ -334,8 +347,11 @@ struct ReadingReducer: ReducerProtocol { 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.fetchDatabaseInfos) + return .run { [state] send in + guard let dbState = await databaseClient.fetchGalleryState(gid: state.gallery.id) else { return } + await send(.fetchDatabaseInfosDone(dbState)) + } + .cancellable(id: CancelID.fetchDatabaseInfos) case .fetchDatabaseInfosDone(let galleryState): if let previewConfig = galleryState.previewConfig { @@ -355,8 +371,11 @@ struct ReadingReducer: ReducerProtocol { 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.fetchPreviewURLs) + return .run { send in + let response = await GalleryPreviewURLsRequest(galleryURL: galleryURL, pageNum: pageNum).response() + await send(.fetchPreviewURLsDone(index, response)) + } + .cancellable(id: CancelID.fetchPreviewURLs) case .fetchPreviewURLsDone(let index, let result): switch result { @@ -367,7 +386,7 @@ struct ReadingReducer: ReducerProtocol { } state.previewLoadingStates[index] = .idle state.updatePreviewURLs(previewURLs) - return .init(value: .syncPreviewURLs(previewURLs)) + return .send(.syncPreviewURLs(previewURLs)) case .failure(let error): state.previewLoadingStates[index] = .failed(error) } @@ -375,16 +394,16 @@ struct ReadingReducer: ReducerProtocol { case .fetchImageURLs(let index): if state.mpvKey != nil { - return .init(value: .fetchMPVImageURL(index, false)) + return .send(.fetchMPVImageURL(index, false)) } else { - return .init(value: .fetchThumbnailURLs(index)) + return .send(.fetchThumbnailURLs(index)) } case .refetchImageURLs(let index): if state.mpvKey != nil { - return .init(value: .fetchMPVImageURL(index, true)) + return .send(.fetchMPVImageURL(index, true)) } else { - return .init(value: .refetchNormalImageURLs(index)) + return .send(.refetchNormalImageURLs(index)) } case .prefetchImages(let index, let prefetchLimit): @@ -406,7 +425,7 @@ struct ReadingReducer: ReducerProtocol { } var prefetchImageURLs = [URL]() var fetchImageURLIndices = [Int]() - var effects = [EffectTask]() + var effects = [Effect]() let previousUpperBound = max(index - 2, 1) let previousLowerBound = max(previousUpperBound - prefetchLimit / 2, 1) if previousUpperBound - previousLowerBound > 0 { @@ -420,9 +439,13 @@ struct ReadingReducer: ReducerProtocol { fetchImageURLIndices += getFetchImageURLIndices(range: nextLowerBound...nextUpperBound) } fetchImageURLIndices.forEach { - effects.append(.init(value: .fetchImageURLs($0))) + effects.append(.send(.fetchImageURLs($0))) } - effects.append(imageClient.prefetchImages(prefetchImageURLs).fireAndForget()) + effects.append( + .run { [prefetchImageURLs] _ in + imageClient.prefetchImages(prefetchImageURLs) + } + ) return .merge(effects) case .fetchThumbnailURLs(let index): @@ -433,9 +456,11 @@ struct ReadingReducer: ReducerProtocol { 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.fetchThumbnailURLs) + return .run { send in + let response = await ThumbnailURLsRequest(galleryURL: galleryURL, pageNum: pageNum).response() + await send(.fetchThumbnailURLsDone(index, response)) + } + .cancellable(id: CancelID.fetchThumbnailURLs) case .fetchThumbnailURLsDone(let index, let result): let batchRange = state.previewConfig.batchRange(index: index) @@ -448,12 +473,12 @@ struct ReadingReducer: ReducerProtocol { return .none } if let url = thumbnailURLs[index], urlClient.checkIfMPVURL(url) { - return .init(value: .fetchMPVKeys(index, url)) + return .send(.fetchMPVKeys(index, url)) } else { state.updateThumbnailURLs(thumbnailURLs) return .merge( - .init(value: .syncThumbnailURLs(thumbnailURLs)), - .init(value: .fetchNormalImageURLs(index, thumbnailURLs)) + .send(.syncThumbnailURLs(thumbnailURLs)), + .send(.fetchNormalImageURLs(index, thumbnailURLs)) ) } case .failure(let error): @@ -464,9 +489,11 @@ struct ReadingReducer: ReducerProtocol { return .none case .fetchNormalImageURLs(let index, let thumbnailURLs): - return GalleryNormalImageURLsRequest(thumbnailURLs: thumbnailURLs) - .effect.map({ Action.fetchNormalImageURLsDone(index, $0) }) - .cancellable(id: CancelID.fetchNormalImageURLs) + return .run { send in + let response = await GalleryNormalImageURLsRequest(thumbnailURLs: thumbnailURLs).response() + await send(.fetchNormalImageURLsDone(index, response)) + } + .cancellable(id: CancelID.fetchNormalImageURLs) case .fetchNormalImageURLsDone(let index, let result): let batchRange = state.previewConfig.batchRange(index: index) @@ -482,7 +509,7 @@ struct ReadingReducer: ReducerProtocol { state.imageURLLoadingStates[$0] = .idle } state.updateImageURLs(imageURLs, originalImageURLs) - return .init(value: .syncImageURLs(imageURLs, originalImageURLs)) + return .send(.syncImageURLs(imageURLs, originalImageURLs)) case .failure(let error): batchRange.forEach { state.imageURLLoadingStates[$0] = .failed(error) @@ -497,21 +524,25 @@ struct ReadingReducer: ReducerProtocol { 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) }) + return .run { [thumbnailURL = state.thumbnailURLs[index]] send in + let response = await GalleryNormalImageURLRefetchRequest( + index: index, + pageNum: pageNum, + galleryURL: galleryURL, + thumbnailURL: thumbnailURL, + storedImageURL: imageURL + ) + .response() + await send(.refetchNormalImageURLsDone(index, response)) + } .cancellable(id: CancelID.refetchNormalImageURLs) case .refetchNormalImageURLsDone(let index, let result): switch result { case .success(let (imageURLs, response)): - var effects = [EffectTask]() + var effects = [Effect]() if let response = response { - effects.append(cookieClient.setSkipServer(response: response).fireAndForget()) + effects.append(.run(operation: { _ in cookieClient.setSkipServer(response: response) })) } guard !imageURLs.isEmpty else { state.imageURLLoadingStates[index] = .failed(.notFound) @@ -519,7 +550,7 @@ struct ReadingReducer: ReducerProtocol { } state.imageURLLoadingStates[index] = .idle state.updateImageURLs(imageURLs, [:]) - effects.append(.init(value: .syncImageURLs(imageURLs, [:]))) + effects.append(.send(.syncImageURLs(imageURLs, [:]))) return .merge(effects) case .failure(let error): state.imageURLLoadingStates[index] = .failed(error) @@ -527,8 +558,11 @@ struct ReadingReducer: ReducerProtocol { return .none case .fetchMPVKeys(let index, let mpvURL): - return MPVKeysRequest(mpvURL: mpvURL) - .effect.map({ Action.fetchMPVKeysDone(index, $0) }).cancellable(id: CancelID.fetchMPVKeys) + return .run { send in + let response = await MPVKeysRequest(mpvURL: mpvURL).response() + await send(.fetchMPVKeysDone(index, response)) + } + .cancellable(id: CancelID.fetchMPVKeys) case .fetchMPVKeysDone(let index, let result): let batchRange = state.previewConfig.batchRange(index: index) @@ -548,7 +582,7 @@ struct ReadingReducer: ReducerProtocol { state.mpvImageKeys = mpvImageKeys return .merge( Array(1...min(3, max(1, pageCount))).map { - .init(value: .fetchMPVImageURL($0, false)) + .send(.fetchMPVImageURL($0, false)) } ) case .failure(let error): @@ -565,11 +599,18 @@ struct ReadingReducer: ReducerProtocol { 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.fetchMPVImageURL) + return .run { send in + let response = await GalleryMPVImageURLRequest( + gid: gidInteger, + index: index, + mpvKey: mpvKey, + mpvImageKey: mpvImageKey, + skipServerIdentifier: skipServerIdentifier + ) + .response() + await send(.fetchMPVImageURLDone(index, response)) + } + .cancellable(id: CancelID.fetchMPVImageURL) case .fetchMPVImageURLDone(let index, let result): switch result { @@ -582,7 +623,7 @@ struct ReadingReducer: ReducerProtocol { state.imageURLLoadingStates[index] = .idle state.mpvSkipServerIdentifiers[index] = skipServerIdentifier state.updateImageURLs(imageURLs, originalImageURLs) - return .init(value: .syncImageURLs(imageURLs, originalImageURLs)) + return .send(.syncImageURLs(imageURLs, originalImageURLs)) case .failure(let error): state.imageURLLoadingStates[index] = .failed(error) } diff --git a/EhPanda/View/Reading/ReadingView.swift b/EhPanda/View/Reading/ReadingView.swift index cb361eca..00bae5c8 100644 --- a/EhPanda/View/Reading/ReadingView.swift +++ b/EhPanda/View/Reading/ReadingView.swift @@ -30,7 +30,7 @@ struct ReadingView: View { gid: String, setting: Binding, blurRadius: Double ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.gid = gid _setting = setting self.blurRadius = blurRadius @@ -67,8 +67,8 @@ struct ReadingView: View { .id(viewStore.databaseLoadingState) .id(viewStore.forceRefreshID) ControlPanel( - showsPanel: viewStore.binding(\.$showsPanel), - showsSliderPreview: viewStore.binding(\.$showsSliderPreview), + showsPanel: viewStore.$showsPanel, + showsSliderPreview: viewStore.$showsSliderPreview, sliderValue: $pageHandler.sliderValue, setting: $setting, enablesLiveText: $liveTextHandler.enablesLiveText, autoPlayPolicy: .init(get: { autoPlayHandler.policy }, set: setAutoPlayPolocy), @@ -81,7 +81,7 @@ struct ReadingView: View { fetchPreviewURLsAction: { viewStore.send(.fetchPreviewURLs($0)) } ) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /ReadingReducer.Route.readingSetting) { _ in + .sheet(unwrapping: viewStore.$route, case: /ReadingReducer.Route.readingSetting) { _ in NavigationView { ReadingSettingView( readingDirection: $setting.readingDirection, @@ -106,13 +106,13 @@ struct ReadingView: View { .accentColor(setting.accentColor).tint(setting.accentColor) .autoBlur(radius: blurRadius).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /ReadingReducer.Route.share) { route in + .sheet(unwrapping: viewStore.$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), + unwrapping: viewStore.$route, case: /ReadingReducer.Route.hud ) @@ -598,10 +598,7 @@ struct ReadingView_Previews: PreviewProvider { Text("") .fullScreenCover(isPresented: .constant(true)) { ReadingView( - store: .init( - initialState: .init(gallery: .empty), - reducer: ReadingReducer() - ), + store: .init(initialState: .init(gallery: .empty), reducer: ReadingReducer.init), gid: .init(), setting: .constant(.init()), blurRadius: 0 diff --git a/EhPanda/View/Search/SearchReducer.swift b/EhPanda/View/Search/SearchReducer.swift index f4ea5499..8ff7e274 100644 --- a/EhPanda/View/Search/SearchReducer.swift +++ b/EhPanda/View/Search/SearchReducer.swift @@ -7,7 +7,7 @@ import ComposableArchitecture -struct SearchReducer: ReducerProtocol { +struct SearchReducer: Reducer { enum Route: Equatable { case filters case quickSearch @@ -64,13 +64,13 @@ struct SearchReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient @Dependency(\.hapticsClient) private var hapticsClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding(\.$keyword): if !state.keyword.isEmpty { @@ -83,19 +83,19 @@ struct SearchReducer: ReducerProtocol { case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: state.detailState = .init() state.filtersState = .init() state.quickSearchState = .init() return .merge( - .init(value: .detail(.teardown)), - .init(value: .quickSearch(.teardown)) + .send(.detail(.teardown)), + .send(.quickSearch(.teardown)) ) case .teardown: - return .cancel(ids: CancelID.allCases) + return .merge(CancelID.allCases.map(Effect.cancel(id:))) case .fetchGalleries(let keyword): guard state.loadingState != .loading else { return .none } @@ -106,9 +106,11 @@ struct SearchReducer: ReducerProtocol { 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.fetchGalleries) + return .run { [lastKeyword = state.lastKeyword] send in + let response = await SearchGalleriesRequest(keyword: lastKeyword, filter: filter).response() + await send(.fetchGalleriesDone(response)) + } + .cancellable(id: CancelID.fetchGalleries) case .fetchGalleriesDone(let result): state.loadingState = .idle @@ -117,11 +119,11 @@ struct SearchReducer: ReducerProtocol { guard !galleries.isEmpty else { state.loadingState = .failed(.notFound) guard pageNumber.hasNextPage() else { return .none } - return .init(value: .fetchMoreGalleries) + return .send(.fetchMoreGalleries) } state.pageNumber = pageNumber state.galleries = galleries - return databaseClient.cacheGalleries(galleries).fireAndForget() + return .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) case .failure(let error): state.loadingState = .failed(error) } @@ -135,9 +137,14 @@ struct SearchReducer: ReducerProtocol { 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.fetchMoreGalleries) + return .run { [lastKeyword = state.lastKeyword] send in + let response = await MoreSearchGalleriesRequest( + keyword: lastKeyword, filter: filter, lastID: lastID + ) + .response() + await send(.fetchMoreGalleriesDone(response)) + } + .cancellable(id: CancelID.fetchMoreGalleries) case .fetchMoreGalleriesDone(let result): state.footerLoadingState = .idle @@ -146,11 +153,11 @@ struct SearchReducer: ReducerProtocol { state.pageNumber = pageNumber state.insertGalleries(galleries) - var effects: [EffectTask] = [ - databaseClient.cacheGalleries(galleries).fireAndForget() + var effects: [Effect] = [ + .run(operation: { _ in await databaseClient.cacheGalleries(galleries) }) ] if galleries.isEmpty, pageNumber.hasNextPage() { - effects.append(.init(value: .fetchMoreGalleries)) + effects.append(.send(.fetchMoreGalleries)) } else if !galleries.isEmpty { state.loadingState = .idle } diff --git a/EhPanda/View/Search/SearchRootReducer.swift b/EhPanda/View/Search/SearchRootReducer.swift index a3e23634..8250c1b0 100644 --- a/EhPanda/View/Search/SearchRootReducer.swift +++ b/EhPanda/View/Search/SearchRootReducer.swift @@ -7,7 +7,7 @@ import ComposableArchitecture -struct SearchRootReducer: ReducerProtocol { +struct SearchRootReducer: Reducer { enum Route: Equatable { case search case filters @@ -86,7 +86,7 @@ struct SearchRootReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient @Dependency(\.hapticsClient) private var hapticsClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in @@ -94,8 +94,8 @@ struct SearchRootReducer: ReducerProtocol { case .binding(\.$route): return state.route == nil ? .merge( - .init(value: .clearSubStates), - .init(value: .fetchDatabaseInfos) + .send(.clearSubStates), + .send(.fetchDatabaseInfos) ) : .none @@ -106,8 +106,8 @@ struct SearchRootReducer: ReducerProtocol { state.route = route return route == nil ? .merge( - .init(value: .clearSubStates), - .init(value: .fetchDatabaseInfos) + .send(.clearSubStates), + .send(.fetchDatabaseInfos) ) : .none @@ -121,16 +121,21 @@ struct SearchRootReducer: ReducerProtocol { state.filtersState = .init() state.quickSearchState = .init() return .merge( - .init(value: .search(.teardown)), - .init(value: .quickSearch(.teardown)), - .init(value: .detail(.teardown)) + .send(.search(.teardown)), + .send(.quickSearch(.teardown)), + .send(.detail(.teardown)) ) case .syncHistoryKeywords: - return databaseClient.updateHistoryKeywords(state.historyKeywords).fireAndForget() + return .run { [state] _ in + await databaseClient.updateHistoryKeywords(state.historyKeywords) + } case .fetchDatabaseInfos: - return databaseClient.fetchAppEnv().map(Action.fetchDatabaseInfosDone) + return .run { send in + let appEnv = await databaseClient.fetchAppEnv() + await send(.fetchDatabaseInfosDone(appEnv)) + } case .fetchDatabaseInfosDone(let appEnv): state.historyKeywords = appEnv.historyKeywords @@ -139,14 +144,17 @@ struct SearchRootReducer: ReducerProtocol { case .appendHistoryKeyword(let keyword): state.appendHistoryKeywords([keyword]) - return .init(value: .syncHistoryKeywords) + return .send(.syncHistoryKeywords) case .removeHistoryKeyword(let keyword): state.removeHistoryKeyword(keyword) - return .init(value: .syncHistoryKeywords) + return .send(.syncHistoryKeywords) case .fetchHistoryGalleries: - return databaseClient.fetchHistoryGalleries(fetchLimit: 10).map(Action.fetchHistoryGalleriesDone) + return .run { send in + let historyGalleries = await databaseClient.fetchHistoryGalleries(fetchLimit: 10) + await send(.fetchHistoryGalleriesDone(historyGalleries)) + } case .fetchHistoryGalleriesDone(let galleries): state.historyGalleries = Array(galleries.prefix(min(galleries.count, 10))) @@ -158,7 +166,7 @@ struct SearchRootReducer: ReducerProtocol { } else { state.appendHistoryKeywords([state.searchState.lastKeyword]) } - return .init(value: .syncHistoryKeywords) + return .send(.syncHistoryKeywords) case .search: return .none diff --git a/EhPanda/View/Search/SearchRootView.swift b/EhPanda/View/Search/SearchRootView.swift index 52e0a7f0..89d8bc70 100644 --- a/EhPanda/View/Search/SearchRootView.swift +++ b/EhPanda/View/Search/SearchRootView.swift @@ -21,7 +21,7 @@ struct SearchRootView: View { user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.user = user _setting = setting self.blurRadius = blurRadius @@ -45,7 +45,7 @@ struct SearchRootView: View { ) } .sheet( - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /SearchRootReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in @@ -58,11 +58,11 @@ struct SearchRootView: View { } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /SearchRootReducer.Route.filters) { _ in + .sheet(unwrapping: viewStore.$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: /SearchRootReducer.Route.quickSearch) { _ in + .sheet(unwrapping: viewStore.$route, case: /SearchRootReducer.Route.quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickSearchState, action: SearchRootReducer.Action.quickSearch) ) { keyword in @@ -75,10 +75,10 @@ struct SearchRootView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .searchable(text: viewStore.binding(\.$keyword)) + .searchable(text: viewStore.$keyword) .searchSuggestions { TagSuggestionView( - keyword: viewStore.binding(\.$keyword), translations: tagTranslator.translations, + keyword: viewStore.$keyword, translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion ) } @@ -117,7 +117,7 @@ private extension SearchRootView { searchViewLink } var detailViewLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SearchRootReducer.Route.detail) { route in + NavigationLink(unwrapping: viewStore.$route, case: /SearchRootReducer.Route.detail) { route in DetailView( store: store.scope(state: \.detailState, action: SearchRootReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -126,7 +126,7 @@ private extension SearchRootView { } } var searchViewLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SearchRootReducer.Route.search) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /SearchRootReducer.Route.search) { _ in SearchView( store: store.scope(state: \.searchState, action: SearchRootReducer.Action.search), keyword: viewStore.keyword, user: user, setting: $setting, @@ -404,10 +404,7 @@ private struct WrappedKeyword: Hashable { struct SearchRootView_Previews: PreviewProvider { static var previews: some View { SearchRootView( - store: .init( - initialState: .init(), - reducer: SearchRootReducer() - ), + store: .init(initialState: .init(), reducer: SearchRootReducer.init), user: .init(), setting: .constant(.init()), blurRadius: 0, diff --git a/EhPanda/View/Search/SearchView.swift b/EhPanda/View/Search/SearchView.swift index 9a637652..e78a70b2 100644 --- a/EhPanda/View/Search/SearchView.swift +++ b/EhPanda/View/Search/SearchView.swift @@ -22,7 +22,7 @@ struct SearchView: View { keyword: String, user: User, setting: Binding, blurRadius: Double, tagTranslator: TagTranslator ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.keyword = keyword self.user = user _setting = setting @@ -45,7 +45,7 @@ struct SearchView: View { } ) .sheet( - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /SearchReducer.Route.detail, isEnabled: DeviceUtil.isPad ) { route in @@ -58,7 +58,7 @@ struct SearchView: View { } .autoBlur(radius: blurRadius).environment(\.inSheet, true).navigationViewStyle(.stack) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /SearchReducer.Route.quickSearch) { _ in + .sheet(unwrapping: viewStore.$route, case: /SearchReducer.Route.quickSearch) { _ in QuickSearchView( store: store.scope(state: \.quickSearchState, action: SearchReducer.Action.quickSearch) ) { keyword in @@ -68,14 +68,14 @@ struct SearchView: View { .accentColor(setting.accentColor) .autoBlur(radius: blurRadius) } - .sheet(unwrapping: viewStore.binding(\.$route), case: /SearchReducer.Route.filters) { _ in + .sheet(unwrapping: viewStore.$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)) + .searchable(text: viewStore.$keyword) .searchSuggestions { TagSuggestionView( - keyword: viewStore.binding(\.$keyword), translations: tagTranslator.translations, + keyword: viewStore.$keyword, translations: tagTranslator.translations, showsImages: setting.showsImagesInTags, isEnabled: setting.showsTagsSearchSuggestion ) } @@ -96,7 +96,7 @@ struct SearchView: View { @ViewBuilder private var navigationLink: some View { if DeviceUtil.isPhone { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SearchReducer.Route.detail) { route in + NavigationLink(unwrapping: viewStore.$route, case: /SearchReducer.Route.detail) { route in DetailView( store: store.scope(state: \.detailState, action: SearchReducer.Action.detail), gid: route.wrappedValue, user: user, setting: $setting, @@ -122,10 +122,7 @@ struct SearchView: View { struct SearchView_Previews: PreviewProvider { static var previews: some View { SearchView( - store: .init( - initialState: .init(), - reducer: SearchReducer() - ), + store: .init(initialState: .init(), reducer: SearchReducer.init), keyword: .init(), user: .init(), setting: .constant(.init()), diff --git a/EhPanda/View/Search/Support/QuickSearchReducer.swift b/EhPanda/View/Search/Support/QuickSearchReducer.swift index a1449c90..93b8460f 100644 --- a/EhPanda/View/Search/Support/QuickSearchReducer.swift +++ b/EhPanda/View/Search/Support/QuickSearchReducer.swift @@ -8,7 +8,7 @@ import SwiftUI import ComposableArchitecture -struct QuickSearchReducer: ReducerProtocol { +struct QuickSearchReducer: Reducer { enum Route: Equatable { case newWord case editWord @@ -61,20 +61,20 @@ struct QuickSearchReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding: return .none case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: state.focusedField = nil @@ -82,7 +82,9 @@ struct QuickSearchReducer: ReducerProtocol { return .none case .syncQuickSearchWords: - return databaseClient.updateQuickSearchWords(state.quickSearchWords).fireAndForget() + return .run { [state] _ in + await databaseClient.updateQuickSearchWords(state.quickSearchWords) + } case .toggleListEditing: state.isListEditing.toggle() @@ -94,35 +96,37 @@ struct QuickSearchReducer: ReducerProtocol { case .appendWord: state.quickSearchWords.append(state.editingWord) - return .init(value: .syncQuickSearchWords) + return .send(.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 .send(.syncQuickSearchWords) } return .none case .deleteWord(let word): state.quickSearchWords = state.quickSearchWords.filter({ $0 != word }) - return .init(value: .syncQuickSearchWords) + return .send(.syncQuickSearchWords) case .deleteWordWithOffsets(let offsets): state.quickSearchWords.remove(atOffsets: offsets) - return .init(value: .syncQuickSearchWords) + return .send(.syncQuickSearchWords) case .moveWord(let source, let destination): state.quickSearchWords.move(fromOffsets: source, toOffset: destination) - return .init(value: .syncQuickSearchWords) + return .send(.syncQuickSearchWords) case .teardown: return .cancel(id: CancelID.fetchQuickSearchWords) case .fetchQuickSearchWords: state.loadingState = .loading - return databaseClient.fetchQuickSearchWords() - .map(Action.fetchQuickSearchWordsDone) - .cancellable(id: CancelID.fetchQuickSearchWords) + return .run { send in + let quickSearchWords = await databaseClient.fetchQuickSearchWords() + await send(.fetchQuickSearchWordsDone(quickSearchWords)) + } + .cancellable(id: CancelID.fetchQuickSearchWords) case .fetchQuickSearchWordsDone(let words): state.loadingState = .idle diff --git a/EhPanda/View/Search/Support/QuickSearchView.swift b/EhPanda/View/Search/Support/QuickSearchView.swift index 62e8e591..605c548d 100644 --- a/EhPanda/View/Search/Support/QuickSearchView.swift +++ b/EhPanda/View/Search/Support/QuickSearchView.swift @@ -17,7 +17,7 @@ struct QuickSearchView: View { init(store: StoreOf, searchAction: @escaping (String) -> Void) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.searchAction = searchAction } @@ -54,7 +54,7 @@ struct QuickSearchView: View { .withArrow(isVisible: !viewStore.isListEditing).padding(5) .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.delete, - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /QuickSearchReducer.Route.deleteWord, matching: word ) { route in @@ -82,8 +82,8 @@ struct QuickSearchView: View { && viewStore.quickSearchWords.isEmpty ? 1 : 0 ) } - .synchronize(viewStore.binding(\.$focusedField), $focusedField) - .environment(\.editMode, viewStore.binding(\.$listEditMode)) + .synchronize(viewStore.$focusedField, $focusedField) + .environment(\.editMode, viewStore.$listEditMode) .animation(.default, value: viewStore.quickSearchWords) .animation(.default, value: viewStore.listEditMode) .onAppear { @@ -123,10 +123,10 @@ struct QuickSearchView: View { } } @ViewBuilder private var navigationLinks: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /QuickSearchReducer.Route.newWord) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /QuickSearchReducer.Route.newWord) { _ in EditWordView( title: L10n.Localizable.QuickSearchView.Title.newWord, - word: viewStore.binding(\.$editingWord), + word: viewStore.$editingWord, focusedField: $focusedField, submitAction: onTextFieldSubmitted, confirmAction: { @@ -135,10 +135,10 @@ struct QuickSearchView: View { } ) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /QuickSearchReducer.Route.editWord) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /QuickSearchReducer.Route.editWord) { _ in EditWordView( title: L10n.Localizable.QuickSearchView.Title.editWord, - word: viewStore.binding(\.$editingWord), + word: viewStore.$editingWord, focusedField: $focusedField, submitAction: onTextFieldSubmitted, confirmAction: { @@ -202,10 +202,7 @@ extension QuickSearchView { struct QuickSearchView_Previews: PreviewProvider { static var previews: some View { QuickSearchView( - store: .init( - initialState: .init(), - reducer: QuickSearchReducer() - ), + store: .init(initialState: .init(), reducer: QuickSearchReducer.init), searchAction: { _ in } ) } diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift index 5cc9b7b6..9346f064 100644 --- a/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingReducer.swift @@ -9,7 +9,7 @@ import Foundation import TTProgressHUD import ComposableArchitecture -struct AccountSettingReducer: ReducerProtocol { +struct AccountSettingReducer: Reducer { enum Route: Equatable { case hud case login @@ -43,7 +43,7 @@ struct AccountSettingReducer: ReducerProtocol { @Dependency(\.cookieClient) private var cookieClient @Dependency(\.hapticsClient) private var hapticsClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in @@ -52,10 +52,14 @@ struct AccountSettingReducer: ReducerProtocol { return state.route == nil ? .send(.clearSubStates) : .none case .binding(\.$ehCookiesState): - return cookieClient.setCookies(state: state.ehCookiesState).fireAndForget() + return .run { [state] _ in + cookieClient.setCookies(state: state.ehCookiesState) + } case .binding(\.$exCookiesState): - return cookieClient.setCookies(state: state.exCookiesState).fireAndForget() + return .run { [state] _ in + cookieClient.setCookies(state: state.exCookiesState) + } case .binding: return .none @@ -84,8 +88,8 @@ struct AccountSettingReducer: ReducerProtocol { let cookiesDescription = cookieClient.getCookiesDescription(host: host) return .merge( .send(.setNavigation(.hud)), - clipboardClient.saveText(cookiesDescription).fireAndForget(), - .fireAndForget({ hapticsClient.generateNotificationFeedback(.success) }) + .run(operation: { _ in clipboardClient.saveText(cookiesDescription) }), + .run(operation: { _ in hapticsClient.generateNotificationFeedback(.success) }) ) case .login(.loginDone): diff --git a/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift b/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift index 6e796bd2..de1606a4 100644 --- a/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift +++ b/EhPanda/View/Setting/AccountSetting/AccountSettingView.swift @@ -22,7 +22,7 @@ struct AccountSettingView: View { bypassesSNIFiltering: Bool, blurRadius: Double ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) _galleryHost = galleryHost _showsNewDawnGreeting = showsNewDawnGreeting self.bypassesSNIFiltering = bypassesSNIFiltering @@ -40,7 +40,7 @@ struct AccountSettingView: View { } .pickerStyle(.segmented) AccountSection( - route: viewStore.binding(\.$route), + route: viewStore.$route, showsNewDawnGreeting: $showsNewDawnGreeting, bypassesSNIFiltering: bypassesSNIFiltering, loginAction: { viewStore.send(.setNavigation(.login)) }, @@ -55,17 +55,17 @@ struct AccountSettingView: View { ) } CookieSection( - ehCookiesState: viewStore.binding(\.$ehCookiesState), - exCookiesState: viewStore.binding(\.$exCookiesState), + ehCookiesState: viewStore.$ehCookiesState, + exCookiesState: viewStore.$exCookiesState, copyAction: { viewStore.send(.copyCookies($0)) } ) } .progressHUD( config: viewStore.hudConfig, - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /AccountSettingReducer.Route.hud ) - .sheet(unwrapping: viewStore.binding(\.$route), case: /AccountSettingReducer.Route.webView) { route in + .sheet(unwrapping: viewStore.$route, case: /AccountSettingReducer.Route.webView) { route in WebView(url: route.wrappedValue) .autoBlur(radius: blurRadius) } @@ -78,13 +78,13 @@ struct AccountSettingView: View { // MARK: NavigationLinks private extension AccountSettingView { @ViewBuilder var navigationLinks: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /AccountSettingReducer.Route.login) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /AccountSettingReducer.Route.login) { _ in LoginView( store: store.scope(state: \.loginState, action: AccountSettingReducer.Action.login), bypassesSNIFiltering: bypassesSNIFiltering, blurRadius: blurRadius ) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /AccountSettingReducer.Route.ehSetting) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /AccountSettingReducer.Route.ehSetting) { _ in EhSettingView( store: store.scope(state: \.ehSettingState, action: AccountSettingReducer.Action.ehSetting), bypassesSNIFiltering: bypassesSNIFiltering, blurRadius: blurRadius @@ -222,10 +222,7 @@ struct AccountSettingView_Previews: PreviewProvider { static var previews: some View { NavigationView { AccountSettingView( - store: .init( - initialState: .init(), - reducer: AccountSettingReducer() - ), + store: .init(initialState: .init(), reducer: AccountSettingReducer.init), galleryHost: .constant(.ehentai), showsNewDawnGreeting: .constant(false), bypassesSNIFiltering: false, diff --git a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift index 6eaa6b3d..375c95e6 100644 --- a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift +++ b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingReducer.swift @@ -7,7 +7,7 @@ import ComposableArchitecture -struct AppearanceSettingReducer: ReducerProtocol { +struct AppearanceSettingReducer: Reducer { enum Route { case appIcon } @@ -21,7 +21,7 @@ struct AppearanceSettingReducer: ReducerProtocol { case setNavigation(Route?) } - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in diff --git a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift index 82fdcb6b..4d1ee0cc 100644 --- a/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift +++ b/EhPanda/View/Setting/AppearanceSetting/AppearanceSettingView.swift @@ -31,7 +31,7 @@ struct AppearanceSettingView: View { displaysJapaneseTitle: Binding ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) _preferredColorScheme = preferredColorScheme _accentColor = accentColor _appIconType = appIconType @@ -107,7 +107,7 @@ struct AppearanceSettingView: View { } private var navigationLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /AppearanceSettingReducer.Route.appIcon) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /AppearanceSettingReducer.Route.appIcon) { _ in AppIconView(appIconType: $appIconType) } } @@ -228,10 +228,7 @@ struct AppearanceSettingView_Previews: PreviewProvider { static var previews: some View { NavigationView { AppearanceSettingView( - store: .init( - initialState: .init(), - reducer: AppearanceSettingReducer() - ), + store: .init(initialState: .init(), reducer: AppearanceSettingReducer.init), preferredColorScheme: .constant(.automatic), accentColor: .constant(.blue), appIconType: .constant(.default), diff --git a/EhPanda/View/Setting/Components/AboutView.swift b/EhPanda/View/Setting/Components/AboutView.swift index b01c48b5..ecdc5158 100644 --- a/EhPanda/View/Setting/Components/AboutView.swift +++ b/EhPanda/View/Setting/Components/AboutView.swift @@ -115,12 +115,12 @@ struct AboutView: View { text: L10n.Constant.App.CodeLevelContributor.Text.xioxin ), .init( - urlString: L10n.Constant.App.CodeLevelContributor.Link.ethanChinCN, - text: L10n.Constant.App.CodeLevelContributor.Text.ethanChinCN + urlString: L10n.Constant.App.CodeLevelContributor.Link.jimmyPrime, + text: L10n.Constant.App.CodeLevelContributor.Text.jimmyPrime ), .init( - urlString: L10n.Constant.App.CodeLevelContributor.Link.lengYue, - text: L10n.Constant.App.CodeLevelContributor.Text.lengYue + urlString: L10n.Constant.App.CodeLevelContributor.Link.remlostime, + text: L10n.Constant.App.CodeLevelContributor.Text.remlostime ) ]}() diff --git a/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift b/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift index c2ece436..059d109f 100644 --- a/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift +++ b/EhPanda/View/Setting/EhSetting/EhSettingReducer.swift @@ -8,7 +8,7 @@ import Foundation import ComposableArchitecture -struct EhSettingReducer: ReducerProtocol { +struct EhSettingReducer: Reducer { enum Route: Equatable { case webView(URL) case deleteProfile @@ -54,7 +54,7 @@ struct EhSettingReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient @Dependency(\.cookieClient) private var cookieClient - public var body: some ReducerProtocol { + public var body: some Reducer { BindingReducer() Reduce { state, action in @@ -67,22 +67,26 @@ struct EhSettingReducer: ReducerProtocol { return .none case .setKeyboardHidden: - return uiApplicationClient.hideKeyboard().fireAndForget() + return .run(operation: { _ in uiApplicationClient.hideKeyboard() }) case .setDefaultProfile(let profileSet): - return cookieClient.setOrEditCookie( - for: Defaults.URL.host, key: Defaults.Cookie.selectedProfile, value: String(profileSet) - ) - .fireAndForget() + return .run { _ in + cookieClient.setOrEditCookie( + for: Defaults.URL.host, key: Defaults.Cookie.selectedProfile, value: String(profileSet) + ) + } case .teardown: - return .cancel(ids: CancelID.allCases) + return .merge(CancelID.allCases.map(Effect.cancel(id:))) case .fetchEhSetting: guard state.loadingState != .loading else { return .none } state.loadingState = .loading - return EhSettingRequest().effect.map(Action.fetchEhSettingDone) - .cancellable(id: CancelID.fetchEhSetting) + return .run { send in + let response = await EhSettingRequest().response() + await send(.fetchEhSettingDone(response)) + } + .cancellable(id: CancelID.fetchEhSetting) case .fetchEhSettingDone(let result): state.loadingState = .idle @@ -101,8 +105,11 @@ struct EhSettingReducer: ReducerProtocol { else { return .none } state.submittingState = .loading - return SubmitEhSettingChangesRequest(ehSetting: ehSetting) - .effect.map(Action.submitChangesDone).cancellable(id: CancelID.submitChanges) + return .run { send in + let response = await SubmitEhSettingChangesRequest(ehSetting: ehSetting).response() + await send(.submitChangesDone(response)) + } + .cancellable(id: CancelID.submitChanges) case .submitChangesDone(let result): state.submittingState = .idle @@ -118,8 +125,11 @@ struct EhSettingReducer: ReducerProtocol { 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.performAction) + return .run { send in + let response = await EhProfileRequest(action: action, name: name, set: set).response() + await send(.performActionDone(response)) + } + .cancellable(id: CancelID.performAction) case .performActionDone(let result): state.submittingState = .idle diff --git a/EhPanda/View/Setting/EhSetting/EhSettingView.swift b/EhPanda/View/Setting/EhSetting/EhSettingView.swift index 102c79b7..ff1c0540 100644 --- a/EhPanda/View/Setting/EhSetting/EhSettingView.swift +++ b/EhPanda/View/Setting/EhSetting/EhSettingView.swift @@ -16,7 +16,7 @@ struct EhSettingView: View { init(store: StoreOf, bypassesSNIFiltering: Bool, blurRadius: Double) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.bypassesSNIFiltering = bypassesSNIFiltering self.blurRadius = blurRadius } @@ -33,8 +33,8 @@ struct EhSettingView: View { .tint(nil) } // Using `Binding.init` will crash the app - else if let ehSetting = Binding(unwrapping: viewStore.binding(\.$ehSetting)), - let ehProfile = Binding(unwrapping: viewStore.binding(\.$ehProfile)) + else if let ehSetting = Binding(unwrapping: viewStore.$ehSetting), + let ehProfile = Binding(unwrapping: viewStore.$ehProfile) { form(ehSetting: ehSetting, ehProfile: ehProfile) .transition(.opacity.animation(.default)) @@ -50,7 +50,7 @@ struct EhSettingView: View { viewStore.send(.setDefaultProfile(profileSet)) } } - .sheet(unwrapping: viewStore.binding(\.$route), case: /EhSettingReducer.Route.webView) { route in + .sheet(unwrapping: viewStore.$route, case: /EhSettingReducer.Route.webView) { route in WebView(url: route.wrappedValue) .autoBlur(radius: blurRadius) } @@ -62,10 +62,10 @@ struct EhSettingView: View { Form { Group { EhProfileSection( - route: viewStore.binding(\.$route), + route: viewStore.$route, ehSetting: ehSetting, ehProfile: ehProfile, - editingProfileName: viewStore.binding(\.$editingProfileName), + editingProfileName: viewStore.$editingProfileName, deleteAction: { if let value = viewStore.ehProfile?.value { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { @@ -1021,7 +1021,7 @@ struct EhSettingView_Previews: PreviewProvider { EhSettingView( store: .init( initialState: .init(ehSetting: .empty, ehProfile: .empty, loadingState: .idle), - reducer: EhSettingReducer() + reducer: EhSettingReducer.init ), bypassesSNIFiltering: false, blurRadius: 0 diff --git a/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift b/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift index 1fcb65d4..a6f244c7 100644 --- a/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift +++ b/EhPanda/View/Setting/GeneralSetting/GeneralSettingReducer.swift @@ -9,7 +9,7 @@ import Kingfisher import LocalAuthentication import ComposableArchitecture -struct GeneralSettingReducer: ReducerProtocol { +struct GeneralSettingReducer: Reducer { enum Route { case logs case clearCache @@ -47,24 +47,24 @@ struct GeneralSettingReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient @Dependency(\.libraryClient) private var libraryClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$route): - return state.route == nil ? .init(value: .clearSubStates) : .none + return state.route == nil ? .send(.clearSubStates) : .none case .binding: return .none case .setNavigation(let route): state.route = route - return route == nil ? .init(value: .clearSubStates) : .none + return route == nil ? .send(.clearSubStates) : .none case .clearSubStates: state.logsState = .init() - return .init(value: .logs(.teardown)) + return .send(.logs(.teardown)) case .onTranslationsFilePicked: return .none @@ -74,9 +74,9 @@ struct GeneralSettingReducer: ReducerProtocol { case .clearWebImageCache: return .merge( - libraryClient.clearWebImageDiskCache().fireAndForget(), - databaseClient.removeImageURLs().fireAndForget(), - .init(value: .calculateWebImageDiskCache) + .run(operation: { _ in libraryClient.clearWebImageDiskCache() }), + .run(operation: { _ in await databaseClient.removeImageURLs() }), + .send(.calculateWebImageDiskCache) ) case .checkPasscodeSetting: @@ -84,11 +84,13 @@ struct GeneralSettingReducer: ReducerProtocol { return .none case .navigateToSystemSetting: - return uiApplicationClient.openSettings().fireAndForget() + return .run(operation: { _ in await uiApplicationClient.openSettings() }) case .calculateWebImageDiskCache: - return libraryClient.calculateWebImageDiskCacheSize() - .map(Action.calculateWebImageDiskCacheDone) + return .run { send in + let size = await libraryClient.calculateWebImageDiskCacheSize() + await send(.calculateWebImageDiskCacheDone(size)) + } case .calculateWebImageDiskCacheDone(let bytes): guard let bytes = bytes else { return .none } diff --git a/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift b/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift index f85c77db..3b5bb09c 100644 --- a/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift +++ b/EhPanda/View/Setting/GeneralSetting/GeneralSettingView.swift @@ -34,7 +34,7 @@ struct GeneralSettingView: View { autoLockPolicy: Binding ) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.tagTranslatorLoadingState = tagTranslatorLoadingState self.tagTranslatorEmpty = tagTranslatorEmpty self.tagTranslatorHasCustomTranslations = tagTranslatorHasCustomTranslations @@ -106,7 +106,7 @@ struct GeneralSettingView: View { ) .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.removeCustomTranslations, - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /GeneralSettingReducer.Route.removeCustomTranslations ) { Button(L10n.Localizable.ConfirmationDialog.Button.remove, role: .destructive) { @@ -164,7 +164,7 @@ struct GeneralSettingView: View { } .confirmationDialog( message: L10n.Localizable.ConfirmationDialog.Title.clear, - unwrapping: viewStore.binding(\.$route), + unwrapping: viewStore.$route, case: /GeneralSettingReducer.Route.clearCache ) { Button(L10n.Localizable.ConfirmationDialog.Button.clear, role: .destructive) { @@ -186,7 +186,7 @@ struct GeneralSettingView: View { } private var navigationLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /GeneralSettingReducer.Route.logs) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /GeneralSettingReducer.Route.logs) { _ in LogsView(store: store.scope(state: \.logsState, action: GeneralSettingReducer.Action.logs)) } } @@ -196,10 +196,7 @@ struct GeneralSettingView_Previews: PreviewProvider { static var previews: some View { NavigationView { GeneralSettingView( - store: .init( - initialState: .init(), - reducer: GeneralSettingReducer() - ), + store: .init(initialState: .init(), reducer: GeneralSettingReducer.init), tagTranslatorLoadingState: .idle, tagTranslatorEmpty: false, tagTranslatorHasCustomTranslations: false, diff --git a/EhPanda/View/Setting/Login/LoginReducer.swift b/EhPanda/View/Setting/Login/LoginReducer.swift index fe2c7803..9bd9cbe9 100644 --- a/EhPanda/View/Setting/Login/LoginReducer.swift +++ b/EhPanda/View/Setting/Login/LoginReducer.swift @@ -8,7 +8,7 @@ import SwiftUI import ComposableArchitecture -struct LoginReducer: ReducerProtocol { +struct LoginReducer: Reducer { private enum CancelID: Hashable { case login } @@ -50,7 +50,7 @@ struct LoginReducer: ReducerProtocol { @Dependency(\.hapticsClient) private var hapticsClient @Dependency(\.cookieClient) private var cookieClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in @@ -63,30 +63,33 @@ struct LoginReducer: ReducerProtocol { return .none case .teardown: - return .cancel(id: CancelID.self) + return .cancel(id: CancelID.login) case .login: guard !state.loginButtonDisabled || state.loginState == .loading else { return .none } state.focusedField = nil state.loginState = .loading return .merge( - .fireAndForget({ hapticsClient.generateFeedback(.soft) }), - LoginRequest(username: state.username, password: state.password) - .effect.map(Action.loginDone).cancellable(id: CancelID.login) + .run(operation: { _ in hapticsClient.generateFeedback(.soft) }), + .run { [state] send in + let response = await LoginRequest(username: state.username, password: state.password).response() + await send(.loginDone(response)) + } + .cancellable(id: CancelID.login) ) case .loginDone(let result): state.route = nil - var effects = [EffectTask]() + var effects = [Effect]() if cookieClient.didLogin { state.loginState = .idle - effects.append(.fireAndForget({ hapticsClient.generateNotificationFeedback(.success) })) + effects.append(.run(operation: { _ in hapticsClient.generateNotificationFeedback(.success) })) } else { state.loginState = .failed(.unknown) - effects.append(.fireAndForget({ hapticsClient.generateNotificationFeedback(.error) })) + effects.append(.run(operation: { _ in hapticsClient.generateNotificationFeedback(.error) })) } if case .success(let response) = result, let response = response { - effects.append(cookieClient.setCredentials(response: response).fireAndForget()) + effects.append(.run(operation: { _ in cookieClient.setCredentials(response: response) })) } return .merge(effects) } diff --git a/EhPanda/View/Setting/Login/LoginView.swift b/EhPanda/View/Setting/Login/LoginView.swift index eb5bde54..9a300d58 100644 --- a/EhPanda/View/Setting/Login/LoginView.swift +++ b/EhPanda/View/Setting/Login/LoginView.swift @@ -18,7 +18,7 @@ struct LoginView: View { init(store: StoreOf, bypassesSNIFiltering: Bool, blurRadius: Double) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.bypassesSNIFiltering = bypassesSNIFiltering self.blurRadius = blurRadius } @@ -35,11 +35,11 @@ struct LoginView: View { VStack(spacing: 15) { Group { LoginTextField( - focusedField: $focusedField, text: viewStore.binding(\.$username), + focusedField: $focusedField, text: viewStore.$username, description: L10n.Localizable.LoginView.Title.username, isPassword: false ) LoginTextField( - focusedField: $focusedField, text: viewStore.binding(\.$password), + focusedField: $focusedField, text: viewStore.$password, description: L10n.Localizable.LoginView.Title.password, isPassword: true ) } @@ -55,8 +55,8 @@ struct LoginView: View { } } } - .synchronize(viewStore.binding(\.$focusedField), $focusedField) - .sheet(unwrapping: viewStore.binding(\.$route), case: /LoginReducer.Route.webView) { route in + .synchronize(viewStore.$focusedField, $focusedField) + .sheet(unwrapping: viewStore.$route, case: /LoginReducer.Route.webView) { route in WebView(url: route.wrappedValue) { viewStore.send(.loginDone(.success(nil))) } @@ -134,10 +134,7 @@ struct LoginView_Previews: PreviewProvider { static var previews: some View { NavigationView { LoginView( - store: .init( - initialState: .init(), - reducer: LoginReducer() - ), + store: .init(initialState: .init(), reducer: LoginReducer.init), bypassesSNIFiltering: false, blurRadius: 0 ) diff --git a/EhPanda/View/Setting/Logs/LogsReducer.swift b/EhPanda/View/Setting/Logs/LogsReducer.swift index 2b9e72d1..cc095860 100644 --- a/EhPanda/View/Setting/Logs/LogsReducer.swift +++ b/EhPanda/View/Setting/Logs/LogsReducer.swift @@ -7,7 +7,7 @@ import ComposableArchitecture -struct LogsReducer: ReducerProtocol { +struct LogsReducer: Reducer { enum Route: Equatable { case log(Log) } @@ -37,7 +37,7 @@ struct LogsReducer: ReducerProtocol { @Dependency(\.uiApplicationClient) private var uiApplicationClient @Dependency(\.fileClient) private var fileClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in @@ -50,7 +50,7 @@ struct LogsReducer: ReducerProtocol { return .none case .navigateToFileApp: - return uiApplicationClient.openFileApp().fireAndForget() + return .run(operation: { _ in await uiApplicationClient.openFileApp() }) case .teardown: return .cancel(id: CancelID.fetchLogs) @@ -58,7 +58,11 @@ struct LogsReducer: ReducerProtocol { case .fetchLogs: guard state.loadingState != .loading else { return .none } state.loadingState = .loading - return fileClient.fetchLogs().map(Action.fetchLogsDone).cancellable(id: CancelID.fetchLogs) + return .run { send in + let result = await fileClient.fetchLogs() + await send(.fetchLogsDone(result)) + } + .cancellable(id: CancelID.fetchLogs) case .fetchLogsDone(let result): switch result { @@ -71,7 +75,10 @@ struct LogsReducer: ReducerProtocol { return .none case .deleteLog(let fileName): - return fileClient.deleteLog(fileName).map(Action.deleteLogDone) + return .run { send in + let result = await fileClient.deleteLog(fileName) + await send(.deleteLogDone(result)) + } case .deleteLogDone(let result): if case .success(let fileName) = result { diff --git a/EhPanda/View/Setting/Logs/LogsView.swift b/EhPanda/View/Setting/Logs/LogsView.swift index 0d519b11..7ab24546 100644 --- a/EhPanda/View/Setting/Logs/LogsView.swift +++ b/EhPanda/View/Setting/Logs/LogsView.swift @@ -14,7 +14,7 @@ struct LogsView: View { init(store: StoreOf) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) } var body: some View { @@ -56,7 +56,7 @@ struct LogsView: View { } private var navigationLink: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /LogsReducer.Route.log) { route in + NavigationLink(unwrapping: viewStore.$route, case: /LogsReducer.Route.log) { route in LogView(log: route.wrappedValue) } } @@ -174,12 +174,7 @@ extension Log: CustomStringConvertible { struct LogsView_Previews: PreviewProvider { static var previews: some View { NavigationView { - LogsView( - store: .init( - initialState: .init(), - reducer: LogsReducer() - ) - ) + LogsView(store: .init(initialState: .init(), reducer: LogsReducer.init)) } } } diff --git a/EhPanda/View/Setting/SettingReducer.swift b/EhPanda/View/Setting/SettingReducer.swift index 6aec4aac..76419b2f 100644 --- a/EhPanda/View/Setting/SettingReducer.swift +++ b/EhPanda/View/Setting/SettingReducer.swift @@ -8,7 +8,7 @@ import Foundation import ComposableArchitecture -struct SettingReducer: ReducerProtocol { +struct SettingReducer: Reducer { enum Route: Int, Equatable, Hashable, Identifiable, CaseIterable { var id: Int { rawValue } @@ -109,95 +109,112 @@ struct SettingReducer: ReducerProtocol { @Dependency(\.fileClient) private var fileClient @Dependency(\.dfClient) private var dfClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() - - 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)) + .onChange(of: \.setting.galleryHost) { _, newValue in + Reduce { _, _ in + .merge( + .send(.syncSetting), + .run(operation: { _ in userDefaultsClient.setValue(newValue.rawValue, .galleryHost) }) + ) } - 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 + } + .onChange(of: \.setting.enablesTagsExtension) { _, newValue in + Reduce { _, _ in + var effects: [Effect] = [ + .send(.syncSetting) + ] + if newValue { + effects.append(.send(.fetchTagTranslator)) + } + return .merge(effects) } - return .init(value: .syncSetting) - - case .binding(\.$setting.backgroundBlurRadius): - if state.setting.autoLockPolicy != .never - && state.setting.backgroundBlurRadius == 0 - { - state.setting.autoLockPolicy = .never + } + .onChange(of: \.setting.preferredColorScheme) { _, _ in + Reduce { _, _ in + .merge( + .send(.syncSetting), + .send(.syncUserInterfaceStyle) + ) } - 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()) + } + .onChange(of: \.setting.appIconType) { _, newValue in + Reduce { _, _ in + .merge( + .send(.syncSetting), + .run { send in + _ = await uiApplicationClient.setAlternateIconName(newValue.filename) + await send(.syncAppIconType) + } + ) } - return .merge(effects) - - case .binding(\.$setting.maximumScaleFactor): - if state.setting.doubleTapScaleFactor > state.setting.maximumScaleFactor { - state.setting.doubleTapScaleFactor = state.setting.maximumScaleFactor + } + .onChange(of: \.setting.autoLockPolicy) { _, newValue in + Reduce { state, _ in + if newValue != .never && state.setting.backgroundBlurRadius == 0 { + state.setting.backgroundBlurRadius = 10 + } + return .send(.syncSetting) } - return .init(value: .syncSetting) - - case .binding(\.$setting.doubleTapScaleFactor): - if state.setting.maximumScaleFactor < state.setting.doubleTapScaleFactor { - state.setting.maximumScaleFactor = state.setting.doubleTapScaleFactor + } + .onChange(of: \.setting.backgroundBlurRadius) { _, newValue in + Reduce { state, _ in + if state.setting.autoLockPolicy != .never && newValue == 0 { + state.setting.autoLockPolicy = .never + } + return .send(.syncSetting) } - return .init(value: .syncSetting) - - case .binding(\.$setting.bypassesSNIFiltering): - return .merge( - .init(value: .syncSetting), - .fireAndForget({ hapticsClient.generateFeedback(.soft) }), - dfClient.setActive(state.setting.bypassesSNIFiltering).fireAndForget() - ) + } + .onChange(of: \.setting.enablesLandscape) { _, newValue in + Reduce { _, _ in + var effects: [Effect] = [ + .send(.syncSetting) + ] + if !newValue && !deviceClient.isPad() { + effects.append(.run(operation: { _ in appDelegateClient.setPortraitOrientationMask() })) + } + return .merge(effects) + } + } + .onChange(of: \.setting.maximumScaleFactor) { _, newValue in + Reduce { state, _ in + if state.setting.doubleTapScaleFactor > newValue { + state.setting.doubleTapScaleFactor = newValue + } + return .send(.syncSetting) + } + } + .onChange(of: \.setting.doubleTapScaleFactor) { _, newValue in + Reduce { state, _ in + if state.setting.maximumScaleFactor < newValue { + state.setting.maximumScaleFactor = newValue + } + return .send(.syncSetting) + } + } + .onChange(of: \.setting.bypassesSNIFiltering) { _, newValue in + Reduce { _, _ in + .merge( + .send(.syncSetting), + .run(operation: { _ in hapticsClient.generateFeedback(.soft) }), + .run(operation: { _ in dfClient.setActive(newValue) }) + ) + } + } + Reduce { state, action in + switch action { case .binding(\.$setting): - return .init(value: .syncSetting) + return .send(.syncSetting) case .binding(\.$route): return .none case .binding: return .merge( - .init(value: .syncUser), - .init(value: .syncSetting), - .init(value: .syncTagTranslator) + .send(.syncUser), + .send(.syncSetting), + .send(.syncTagTranslator) ) case .setNavigation(let route): @@ -220,28 +237,38 @@ struct SettingReducer: ReducerProtocol { case .syncUserInterfaceStyle: let style = state.setting.preferredColorScheme.userInterfaceStyle - return uiApplicationClient.setUserInterfaceStyle(style) - .subscribe(on: DispatchQueue.main).fireAndForget() + return .run(operation: { _ in await uiApplicationClient.setUserInterfaceStyle(style) }) case .syncSetting: - return databaseClient.updateSetting(state.setting).fireAndForget() + return .run { [state] _ in + await databaseClient.updateSetting(state.setting) + } case .syncTagTranslator: - return databaseClient.updateTagTranslator(state.tagTranslator).fireAndForget() + return .run { [state] _ in + await databaseClient.updateTagTranslator(state.tagTranslator) + } case .syncUser: - return databaseClient.updateUser(state.user).fireAndForget() + return .run { [state] _ in + await databaseClient.updateUser(state.user) + } case .loadUserSettings: - return databaseClient.fetchAppEnv().map(Action.onLoadUserSettings) + return .run { send in + let appEnv = await databaseClient.fetchAppEnv() + await send(.onLoadUserSettings(appEnv)) + } 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() + var effects: [Effect] = [ + .send(.syncAppIconType), + .send(.loadUserSettingsDone), + .send(.syncUserInterfaceStyle), + .run { [state] _ in + dfClient.setActive(state.setting.bypassesSNIFiltering) + } ] if let value: String = userDefaultsClient.getValue(.galleryHost), let galleryHost = GalleryHost(rawValue: value) @@ -249,18 +276,18 @@ struct SettingReducer: ReducerProtocol { state.setting.galleryHost = galleryHost } if cookieClient.shouldFetchIgneous { - effects.append(.init(value: .fetchIgneous)) + effects.append(.send(.fetchIgneous)) } if cookieClient.didLogin { effects.append(contentsOf: [ - .init(value: .fetchUserInfo), - .init(value: .fetchGreeting), - .init(value: .fetchFavoriteCategories), - .init(value: .fetchEhProfileIndex) + .send(.fetchUserInfo), + .send(.fetchGreeting), + .send(.fetchFavoriteCategories), + .send(.fetchEhProfileIndex) ]) } if state.setting.enablesTagsExtension { - effects.append(.init(value: .fetchTagTranslator)) + effects.append(.send(.fetchTagTranslator)) } return .merge(effects) @@ -269,18 +296,21 @@ struct SettingReducer: ReducerProtocol { return .none case .createDefaultEhProfile: - return EhProfileRequest(action: .create, name: "EhPanda").effect.fireAndForget() + return .run(operation: { _ in _ = await EhProfileRequest(action: .create, name: "EhPanda").response() }) case .fetchIgneous: guard cookieClient.didLogin else { return .none } - return IgneousRequest().effect.map(Action.fetchIgneousDone) + return .run { send in + let response = await IgneousRequest().response() + await send(.fetchIgneousDone(response)) + } case .fetchIgneousDone(let result): - var effects = [EffectTask]() + var effects = [Effect]() if case .success(let response) = result { - effects.append(cookieClient.setCredentials(response: response).fireAndForget()) + effects.append(.run(operation: { _ in cookieClient.setCredentials(response: response) })) } - effects.append(.init(value: .account(.loadCookies))) + effects.append(.send(.account(.loadCookies))) return .merge(effects) case .fetchUserInfo: @@ -288,14 +318,17 @@ struct SettingReducer: ReducerProtocol { let uid = cookieClient .getCookie(Defaults.URL.host, Defaults.Cookie.ipbMemberId).rawValue if !uid.isEmpty { - return UserInfoRequest(uid: uid).effect.map(Action.fetchUserInfoDone) + return .run { send in + let response = await UserInfoRequest(uid: uid).response() + await send(.fetchUserInfoDone(response)) + } } return .none case .fetchUserInfoDone(let result): if case .success(let user) = result { state.updateUser(user) - return .init(value: .syncUser) + return .send(.syncUser) } return .none @@ -320,8 +353,10 @@ struct SettingReducer: ReducerProtocol { guard cookieClient.didLogin, state.setting.showsNewDawnGreeting else { return .none } - let requestEffect = GreetingRequest().effect - .map(Action.fetchGreetingDone) + let requestEffect = Effect.run { send in + let response = await GreetingRequest().response() + await send(Action.fetchGreetingDone(response)) + } if let greeting = state.user.greeting { if verifyDate(with: greeting.updateTime) { return requestEffect @@ -335,13 +370,13 @@ struct SettingReducer: ReducerProtocol { switch result { case .success(let greeting): state.setGreeting(greeting) - return .init(value: .syncUser) + return .send(.syncUser) case .failure(let error): if case .parseFailed = error { var greeting = Greeting() greeting.updateTime = Date() state.setGreeting(greeting) - return .init(value: .syncUser) + return .send(.syncUser) } } return .none @@ -353,14 +388,16 @@ struct SettingReducer: ReducerProtocol { else { return .none } state.tagTranslatorLoadingState = .loading - var databaseEffect: EffectTask? + var databaseEffect: Effect? if state.tagTranslator.language != language { state.tagTranslator = TagTranslator(language: language) - databaseEffect = .init(value: .syncTagTranslator) + databaseEffect = .send(.syncTagTranslator) } let updatedDate = state.tagTranslator.updatedDate - let requestEffect = TagTranslatorRequest(language: language, updatedDate: updatedDate) - .effect.map(Action.fetchTagTranslatorDone) + let requestEffect = Effect.run { send in + let response = await TagTranslatorRequest(language: language, updatedDate: updatedDate).response() + await send(Action.fetchTagTranslatorDone(response)) + } if let databaseEffect = databaseEffect { return .merge(databaseEffect, requestEffect) } else { @@ -372,7 +409,7 @@ struct SettingReducer: ReducerProtocol { switch result { case .success(let tagTranslator): state.tagTranslator = tagTranslator - return .init(value: .syncTagTranslator) + return .send(.syncTagTranslator) case .failure(let error): state.tagTranslatorLoadingState = .failed(error) } @@ -380,10 +417,13 @@ struct SettingReducer: ReducerProtocol { case .fetchEhProfileIndex: guard cookieClient.didLogin else { return .none } - return VerifyEhProfileRequest().effect.map(Action.fetchEhProfileIndexDone) + return .run { send in + let response = await VerifyEhProfileRequest().response() + await send(.fetchEhProfileIndexDone(response)) + } case .fetchEhProfileIndexDone(let result): - var effects = [EffectTask]() + var effects = [Effect]() if case .success(let response) = result { if let profileValue = response.profileValue { @@ -394,24 +434,28 @@ struct SettingReducer: ReducerProtocol { let cookieValue = cookieClient.getCookie(hostURL, selectedProfileKey) if cookieValue.rawValue != profileValueString { effects.append( - cookieClient.setOrEditCookie( - for: hostURL, key: selectedProfileKey, value: profileValueString - ) - .fireAndForget() + .run { _ in + cookieClient.setOrEditCookie( + for: hostURL, key: selectedProfileKey, value: profileValueString + ) + } ) } } else if response.isProfileNotFound { - effects.append(.init(value: .createDefaultEhProfile)) + effects.append(.send(.createDefaultEhProfile)) } else { let message = "Found profile but failed in parsing value." - effects.append(loggerClient.error(message, nil).fireAndForget()) + effects.append(.run(operation: { _ in loggerClient.error(message, nil) })) } } return effects.isEmpty ? .none : .merge(effects) case .fetchFavoriteCategories: guard cookieClient.didLogin else { return .none } - return FavoriteCategoriesRequest().effect.map(Action.fetchFavoriteCategoriesDone) + return .run { send in + let response = await FavoriteCategoriesRequest().response() + await send(.fetchFavoriteCategoriesDone(response)) + } case .fetchFavoriteCategoriesDone(let result): if case .success(let categories) = result { @@ -421,34 +465,37 @@ struct SettingReducer: ReducerProtocol { case .account(.login(.loginDone)): return .merge( - cookieClient.removeYay().fireAndForget(), - cookieClient.syncExCookies().fireAndForget(), - cookieClient.fulfillAnotherHostField().fireAndForget(), - .init(value: .fetchIgneous), - .init(value: .fetchUserInfo), - .init(value: .fetchFavoriteCategories), - .init(value: .fetchEhProfileIndex) + .run(operation: { _ in cookieClient.removeYay() }), + .run(operation: { _ in cookieClient.syncExCookies() }), + .run(operation: { _ in cookieClient.fulfillAnotherHostField() }), + .send(.fetchIgneous), + .send(.fetchUserInfo), + .send(.fetchFavoriteCategories), + .send(.fetchEhProfileIndex) ) case .account(.onLogoutConfirmButtonTapped): state.user = User() return .merge( - .init(value: .syncUser), - cookieClient.clearAll().fireAndForget(), - databaseClient.removeImageURLs().fireAndForget(), - libraryClient.clearWebImageDiskCache().fireAndForget() + .send(.syncUser), + .run(operation: { _ in cookieClient.clearAll() }), + .run(operation: { _ in await databaseClient.removeImageURLs() }), + .run(operation: { _ in libraryClient.clearWebImageDiskCache() }) ) case .account: return .none case .general(.onTranslationsFilePicked(let url)): - return fileClient.importTagTranslator(url).map(Action.fetchTagTranslatorDone) + return .run { send in + let result = await fileClient.importTagTranslator(url) + await send(.fetchTagTranslatorDone(result)) + } case .general(.onRemoveCustomTranslations): state.tagTranslator.hasCustomTranslations = false state.tagTranslator.translations = .init() - return .init(value: .syncTagTranslator) + return .send(.syncTagTranslator) case .general: return .none diff --git a/EhPanda/View/Setting/SettingView.swift b/EhPanda/View/Setting/SettingView.swift index f4d267b2..9a77b565 100644 --- a/EhPanda/View/Setting/SettingView.swift +++ b/EhPanda/View/Setting/SettingView.swift @@ -16,7 +16,7 @@ struct SettingView: View { init(store: StoreOf, blurRadius: Double) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) self.blurRadius = blurRadius } @@ -42,64 +42,64 @@ struct SettingView: View { // MARK: NavigationLinks private extension SettingView { @ViewBuilder var navigationLinks: some View { - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingReducer.Route.account) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /SettingReducer.Route.account) { _ in AccountSettingView( store: store.scope(state: \.accountSettingState, action: SettingReducer.Action.account), - galleryHost: viewStore.binding(\.$setting.galleryHost), - showsNewDawnGreeting: viewStore.binding(\.$setting.showsNewDawnGreeting), + galleryHost: viewStore.$setting.galleryHost, + showsNewDawnGreeting: viewStore.$setting.showsNewDawnGreeting, bypassesSNIFiltering: viewStore.setting.bypassesSNIFiltering, blurRadius: blurRadius ) .tint(viewStore.setting.accentColor) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingReducer.Route.general) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /SettingReducer.Route.general) { _ in GeneralSettingView( store: store.scope(state: \.generalSettingState, action: SettingReducer.Action.general), tagTranslatorLoadingState: viewStore.tagTranslatorLoadingState, tagTranslatorEmpty: viewStore.tagTranslator.translations.isEmpty, tagTranslatorHasCustomTranslations: viewStore.tagTranslator.hasCustomTranslations, - enablesTagsExtension: viewStore.binding(\.$setting.enablesTagsExtension), - translatesTags: viewStore.binding(\.$setting.translatesTags), - showsTagsSearchSuggestion: viewStore.binding(\.$setting.showsTagsSearchSuggestion), - showsImagesInTags: viewStore.binding(\.$setting.showsImagesInTags), - redirectsLinksToSelectedHost: viewStore.binding(\.$setting.redirectsLinksToSelectedHost), - detectsLinksFromClipboard: viewStore.binding(\.$setting.detectsLinksFromClipboard), - backgroundBlurRadius: viewStore.binding(\.$setting.backgroundBlurRadius), - autoLockPolicy: viewStore.binding(\.$setting.autoLockPolicy) + enablesTagsExtension: viewStore.$setting.enablesTagsExtension, + translatesTags: viewStore.$setting.translatesTags, + showsTagsSearchSuggestion: viewStore.$setting.showsTagsSearchSuggestion, + showsImagesInTags: viewStore.$setting.showsImagesInTags, + redirectsLinksToSelectedHost: viewStore.$setting.redirectsLinksToSelectedHost, + detectsLinksFromClipboard: viewStore.$setting.detectsLinksFromClipboard, + backgroundBlurRadius: viewStore.$setting.backgroundBlurRadius, + autoLockPolicy: viewStore.$setting.autoLockPolicy ) .tint(viewStore.setting.accentColor) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingReducer.Route.appearance) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /SettingReducer.Route.appearance) { _ in AppearanceSettingView( store: store.scope(state: \.appearanceSettingState, action: SettingReducer.Action.appearance), - preferredColorScheme: viewStore.binding(\.$setting.preferredColorScheme), - accentColor: viewStore.binding(\.$setting.accentColor), - appIconType: viewStore.binding(\.$setting.appIconType), - listDisplayMode: viewStore.binding(\.$setting.listDisplayMode), - showsTagsInList: viewStore.binding(\.$setting.showsTagsInList), - listTagsNumberMaximum: viewStore.binding(\.$setting.listTagsNumberMaximum), - displaysJapaneseTitle: viewStore.binding(\.$setting.displaysJapaneseTitle) + preferredColorScheme: viewStore.$setting.preferredColorScheme, + accentColor: viewStore.$setting.accentColor, + appIconType: viewStore.$setting.appIconType, + listDisplayMode: viewStore.$setting.listDisplayMode, + showsTagsInList: viewStore.$setting.showsTagsInList, + listTagsNumberMaximum: viewStore.$setting.listTagsNumberMaximum, + displaysJapaneseTitle: viewStore.$setting.displaysJapaneseTitle ) .tint(viewStore.setting.accentColor) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingReducer.Route.reading) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /SettingReducer.Route.reading) { _ in ReadingSettingView( - readingDirection: viewStore.binding(\.$setting.readingDirection), - prefetchLimit: viewStore.binding(\.$setting.prefetchLimit), - enablesLandscape: viewStore.binding(\.$setting.enablesLandscape), - contentDividerHeight: viewStore.binding(\.$setting.contentDividerHeight), - maximumScaleFactor: viewStore.binding(\.$setting.maximumScaleFactor), - doubleTapScaleFactor: viewStore.binding(\.$setting.doubleTapScaleFactor) + readingDirection: viewStore.$setting.readingDirection, + prefetchLimit: viewStore.$setting.prefetchLimit, + enablesLandscape: viewStore.$setting.enablesLandscape, + contentDividerHeight: viewStore.$setting.contentDividerHeight, + maximumScaleFactor: viewStore.$setting.maximumScaleFactor, + doubleTapScaleFactor: viewStore.$setting.doubleTapScaleFactor ) .tint(viewStore.setting.accentColor) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingReducer.Route.laboratory) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /SettingReducer.Route.laboratory) { _ in LaboratorySettingView( - bypassesSNIFiltering: viewStore.binding(\.$setting.bypassesSNIFiltering) + bypassesSNIFiltering: viewStore.$setting.bypassesSNIFiltering ) .tint(viewStore.setting.accentColor) } - NavigationLink(unwrapping: viewStore.binding(\.$route), case: /SettingReducer.Route.about) { _ in + NavigationLink(unwrapping: viewStore.$route, case: /SettingReducer.Route.about) { _ in AboutView().tint(viewStore.setting.accentColor) } } @@ -183,10 +183,7 @@ extension SettingReducer.Route { struct SettingView_Previews: PreviewProvider { static var previews: some View { SettingView( - store: .init( - initialState: .init(), - reducer: SettingReducer() - ), + store: .init(initialState: .init(), reducer: SettingReducer.init), blurRadius: 0 ) } diff --git a/EhPanda/View/Support/FiltersReducer.swift b/EhPanda/View/Support/FiltersReducer.swift index 83121f01..ba25fc34 100644 --- a/EhPanda/View/Support/FiltersReducer.swift +++ b/EhPanda/View/Support/FiltersReducer.swift @@ -7,7 +7,7 @@ import ComposableArchitecture -struct FiltersReducer: ReducerProtocol { +struct FiltersReducer: Reducer { enum Route { case resetFilters } @@ -40,22 +40,22 @@ struct FiltersReducer: ReducerProtocol { @Dependency(\.databaseClient) private var databaseClient - var body: some ReducerProtocol { + var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.$searchFilter): state.searchFilter.fixInvalidData() - return .init(value: .syncFilter(.search)) + return .send(.syncFilter(.search)) case .binding(\.$globalFilter): state.globalFilter.fixInvalidData() - return .init(value: .syncFilter(.global)) + return .send(.syncFilter(.global)) case .binding(\.$watchedFilter): state.watchedFilter.fixInvalidData() - return .init(value: .syncFilter(.watched)) + return .send(.syncFilter(.watched)) case .binding: return .none @@ -85,23 +85,26 @@ struct FiltersReducer: ReducerProtocol { case .watched: filter = state.watchedFilter } - return databaseClient.updateFilter(filter, range: range).fireAndForget() + return .run(operation: { _ in await databaseClient.updateFilter(filter, range: range) }) case .resetFilters: switch state.filterRange { case .search: state.searchFilter = .init() - return .init(value: .syncFilter(.search)) + return .send(.syncFilter(.search)) case .global: state.globalFilter = .init() - return .init(value: .syncFilter(.global)) + return .send(.syncFilter(.global)) case .watched: state.watchedFilter = .init() - return .init(value: .syncFilter(.watched)) + return .send(.syncFilter(.watched)) } case .fetchFilters: - return databaseClient.fetchAppEnv().map(Action.fetchFiltersDone) + return .run { send in + let appEnv = await databaseClient.fetchAppEnv() + await send(.fetchFiltersDone(appEnv)) + } case .fetchFiltersDone(let appEnv): state.searchFilter = appEnv.searchFilter diff --git a/EhPanda/View/Support/FiltersView.swift b/EhPanda/View/Support/FiltersView.swift index 5ac42d18..c7dc475a 100644 --- a/EhPanda/View/Support/FiltersView.swift +++ b/EhPanda/View/Support/FiltersView.swift @@ -16,17 +16,17 @@ struct FiltersView: View { init(store: StoreOf) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) } private var filter: Binding { switch viewStore.filterRange { case .search: - return viewStore.binding(\.$searchFilter) + return viewStore.$searchFilter case .global: - return viewStore.binding(\.$globalFilter) + return viewStore.$globalFilter case .watched: - return viewStore.binding(\.$watchedFilter) + return viewStore.$watchedFilter } } @@ -35,8 +35,8 @@ struct FiltersView: View { NavigationView { Form { BasicSection( - route: viewStore.binding(\.$route), - filter: filter, filterRange: viewStore.binding(\.$filterRange), + route: viewStore.$route, + filter: filter, filterRange: viewStore.$filterRange, resetFiltersAction: { viewStore.send(.resetFilters) }, resetFiltersDialogAction: { viewStore.send(.setNavigation(.resetFilters)) } ) @@ -45,7 +45,7 @@ struct FiltersView: View { submitAction: { viewStore.send(.onTextFieldSubmitted) } ) } - .synchronize(viewStore.binding(\.$focusedBound), $focusedBound) + .synchronize(viewStore.$focusedBound, $focusedBound) .navigationTitle(L10n.Localizable.FiltersView.Title.filters) .onAppear { viewStore.send(.fetchFilters) } } @@ -239,11 +239,6 @@ extension FilterRange { struct FiltersView_Previews: PreviewProvider { static var previews: some View { - FiltersView( - store: .init( - initialState: .init(), - reducer: FiltersReducer() - ) - ) + FiltersView(store: .init(initialState: .init(), reducer: FiltersReducer.init)) } } diff --git a/EhPanda/View/TabBar/TabBarReducer.swift b/EhPanda/View/TabBar/TabBarReducer.swift index a60089ee..3cd712e4 100644 --- a/EhPanda/View/TabBar/TabBarReducer.swift +++ b/EhPanda/View/TabBar/TabBarReducer.swift @@ -7,7 +7,7 @@ import ComposableArchitecture -struct TabBarReducer: ReducerProtocol { +struct TabBarReducer: Reducer { struct State: Equatable { var tabBarItemType: TabBarItemType = .home } @@ -18,7 +18,7 @@ struct TabBarReducer: ReducerProtocol { @Dependency(\.deviceClient) private var deviceClient - var body: some ReducerProtocol { + var body: some Reducer { Reduce { state, action in switch action { case .setTabBarItemType(let type): diff --git a/EhPanda/View/TabBar/TabBarView.swift b/EhPanda/View/TabBar/TabBarView.swift index 5c4a83a3..62ee7e9a 100644 --- a/EhPanda/View/TabBar/TabBarView.swift +++ b/EhPanda/View/TabBar/TabBarView.swift @@ -16,7 +16,7 @@ struct TabBarView: View { init(store: StoreOf) { self.store = store - viewStore = ViewStore(store) + viewStore = ViewStore(store, observe: { $0 }) } var body: some View { @@ -34,7 +34,7 @@ struct TabBarView: View { HomeView( store: store.scope(state: \.homeState, action: AppReducer.Action.home), user: viewStore.settingState.user, - setting: viewStore.binding(\.settingState.$setting), + setting: viewStore.$settingState.setting, blurRadius: viewStore.appLockState.blurRadius, tagTranslator: viewStore.settingState.tagTranslator ) @@ -42,7 +42,7 @@ struct TabBarView: View { FavoritesView( store: store.scope(state: \.favoritesState, action: AppReducer.Action.favorites), user: viewStore.settingState.user, - setting: viewStore.binding(\.settingState.$setting), + setting: viewStore.$settingState.setting, blurRadius: viewStore.appLockState.blurRadius, tagTranslator: viewStore.settingState.tagTranslator ) @@ -50,7 +50,7 @@ struct TabBarView: View { SearchRootView( store: store.scope(state: \.searchRootState, action: AppReducer.Action.searchRoot), user: viewStore.settingState.user, - setting: viewStore.binding(\.settingState.$setting), + setting: viewStore.$settingState.setting, blurRadius: viewStore.appLockState.blurRadius, tagTranslator: viewStore.settingState.tagTranslator ) @@ -73,11 +73,11 @@ struct TabBarView: View { } .font(.system(size: 80)).opacity(viewStore.appLockState.isAppLocked ? 1 : 0) } - .sheet(unwrapping: viewStore.binding(\.appRouteState.$route), case: /AppRouteReducer.Route.newDawn) { route in + .sheet(unwrapping: viewStore.$appRouteState.route, case: /AppRouteReducer.Route.newDawn) { route in NewDawnView(greeting: route.wrappedValue) .autoBlur(radius: viewStore.appLockState.blurRadius) } - .sheet(unwrapping: viewStore.binding(\.appRouteState.$route), case: /AppRouteReducer.Route.setting) { _ in + .sheet(unwrapping: viewStore.$appRouteState.route, case: /AppRouteReducer.Route.setting) { _ in SettingView( store: store.scope(state: \.settingState, action: AppReducer.Action.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: /AppRouteReducer.Route.detail) { route in + .sheet(unwrapping: viewStore.$appRouteState.route, case: /AppRouteReducer.Route.detail) { route in NavigationView { DetailView( store: store.scope( @@ -93,7 +93,7 @@ struct TabBarView: View { action: { AppReducer.Action.appRoute(.detail($0)) } ), gid: route.wrappedValue, user: viewStore.settingState.user, - setting: viewStore.binding(\.settingState.$setting), + setting: viewStore.$settingState.setting, blurRadius: viewStore.appLockState.blurRadius, tagTranslator: viewStore.settingState.tagTranslator ) @@ -105,7 +105,7 @@ struct TabBarView: View { } .progressHUD( config: viewStore.appRouteState.hudConfig, - unwrapping: viewStore.binding(\.appRouteState.$route), + unwrapping: viewStore.$appRouteState.route, case: /AppRouteReducer.Route.hud ) .onChange(of: scenePhase) { viewStore.send(.onScenePhaseChange($0)) } @@ -155,11 +155,6 @@ extension TabBarItemType { struct TabBarView_Previews: PreviewProvider { static var previews: some View { - TabBarView( - store: .init( - initialState: .init(), - reducer: AppReducer() - ) - ) + TabBarView(store: .init(initialState: .init(), reducer: AppReducer.init)) } }