diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 4c5c33e..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: lint - -on: - push: - branches: - - '*' - pull_request: - branches: - - '*' - -jobs: - run-swiftlint: - runs-on: ubuntu-latest - container: - image: ghcr.io/realm/swiftlint:0.54.0 - steps: - - uses: actions/checkout@v3 - - name: Lint code using SwiftLint - run: | - swiftlint --version - swiftlint lint --reporter github-actions-logging - - run-swiftformat: - runs-on: ubuntu-latest - container: - image: ghcr.io/nicklockwood/swiftformat:0.52.11 - steps: - - uses: actions/checkout@v3 - - name: Lint code using SwiftFormat - run: | - swiftformat --version - swiftformat --config .swiftformat.yml --lint --quiet --reporter github-actions-log . diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..1b031af --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,44 @@ +name: Build and Export Unsigned IPA + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Ruby + uses: actions/setup-ruby@v1 + with: + ruby-version: '2.7' + + - name: Install CocoaPods + run: | + gem install cocoapods + pod install --project-directory=App + + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.0.0' + + - name: Build and Export IPA + run: | + xcodebuild clean -project App/Mochi.xcodeproj -scheme Mochi -configuration Release + xcodebuild archive -project App/Mochi.xcodeproj -scheme Mochi -configuration Release -archivePath $PWD/build/Mochi.xcarchive + xcodebuild -exportArchive -archivePath $PWD/build/Mochi.xcarchive -exportOptionsPlist App/exportOptions.plist -exportPath $PWD/build + + - name: Upload IPA as Artifact + uses: actions/upload-artifact@v2 + with: + name: Mochi.ipa + path: build/Mochi.ipa diff --git a/App/Mochi.xcodeproj/project.pbxproj b/App/Mochi.xcodeproj/project.pbxproj index 445edd9..6368b10 100644 --- a/App/Mochi.xcodeproj/project.pbxproj +++ b/App/Mochi.xcodeproj/project.pbxproj @@ -164,6 +164,9 @@ Base, ); mainGroup = 13C18B8829CE6CC100C14F26; + packageReferences = ( + BB94AE842C14B2DF004E1ADB /* XCRemoteSwiftPackageReference "pillarbox-apple" */, + ); productRefGroup = 13C18B9229CE6CC200C14F26 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -203,7 +206,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint > /dev/null; then\n swiftlint --config ../.swiftlint.yml ../\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + shellScript = "#export PATH=\"$PATH:/opt/homebrew/bin\"\n#if which swiftlint > /dev/null; then\n# swiftlint --config ../.swiftlint.yml ../\n#else\n# echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n#fi\n"; }; 13BC25D22AD895AE001DAE2A /* Run SwiftFormat Script */ = { isa = PBXShellScriptBuildPhase; @@ -371,10 +374,12 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = Shared/mochi.entitlements; - CURRENT_PROJECT_VERSION = 5; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 8; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ..; - DEVELOPMENT_TEAM = A6HC4Y86NJ; + DEVELOPMENT_TEAM = Z994R8374W; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; @@ -392,7 +397,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 0.0.1; - PRODUCT_BUNDLE_IDENTIFIER = dev.errorerrorerror.mochi; + PRODUCT_BUNDLE_IDENTIFIER = "com.mochi-team.mochi"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = auto; @@ -411,10 +416,12 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = Shared/mochi.entitlements; - CURRENT_PROJECT_VERSION = 5; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 8; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ..; - DEVELOPMENT_TEAM = A6HC4Y86NJ; + DEVELOPMENT_TEAM = Z994R8374W; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; @@ -432,7 +439,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 0.0.1; - PRODUCT_BUNDLE_IDENTIFIER = dev.errorerrorerror.mochi; + PRODUCT_BUNDLE_IDENTIFIER = "com.mochi-team.mochi"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = auto; @@ -467,6 +474,17 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + BB94AE842C14B2DF004E1ADB /* XCRemoteSwiftPackageReference "pillarbox-apple" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SRGSSR/pillarbox-apple"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 13F11CBF2B11617D006FFF63 /* App */ = { isa = XCSwiftPackageProductDependency; diff --git a/App/Mochi.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/App/Mochi.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..394275f --- /dev/null +++ b/App/Mochi.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,302 @@ +{ + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version" : "1.0.0" + } + }, + { + "identity" : "comscore-swift-package-manager", + "kind" : "remoteSourceControl", + "location" : "https://github.com/comScore/Comscore-Swift-Package-Manager.git", + "state" : { + "revision" : "c2f74c7cc02f8bb01b51c08297f7cbc486f3ca65", + "version" : "6.12.3" + } + }, + { + "identity" : "cwlcatchexception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlCatchException.git", + "state" : { + "revision" : "3ef6999c73b6938cc0da422f2c912d0158abb0a0", + "version" : "2.2.0" + } + }, + { + "identity" : "cwlpreconditiontesting", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", + "state" : { + "revision" : "2ef56b2caf25f55fa7eef8784c30d5a767550f54", + "version" : "2.2.1" + } + }, + { + "identity" : "difference", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzysztofzablocki/Difference.git", + "state" : { + "revision" : "02fe1111edc8318c4f8a0da96336fcbcc201f38b", + "version" : "1.0.1" + } + }, + { + "identity" : "fluidgradient", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Cindori/FluidGradient.git", + "state" : { + "revision" : "9ddda4cf23671ef0228e88681ec6210cb3e0d7f7", + "version" : "1.0.0" + } + }, + { + "identity" : "flyingfox", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swhitty/FlyingFox.git", + "state" : { + "revision" : "c9e0d358e6fcab4b4a4589bc88d110875c96d739", + "version" : "0.14.0" + } + }, + { + "identity" : "iosv5", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CommandersAct/iOSV5.git", + "state" : { + "revision" : "b65eb27cde41d3c40b05520f8713d55a52821553", + "version" : "5.4.9" + } + }, + { + "identity" : "nimble", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Nimble.git", + "state" : { + "revision" : "1c49fc1243018f81a7ea99cb5e0985b00096e9f4", + "version" : "13.3.0" + } + }, + { + "identity" : "nuke", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Nuke.git", + "state" : { + "revision" : "3f666f120b63ea7de57d42e9a7c9b47f8e7a290b", + "version" : "12.1.6" + } + }, + { + "identity" : "pillarbox-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SRGSSR/pillarbox-apple", + "state" : { + "revision" : "59afca0cd1a8ddbba75eee1f63b699b7b9262b44", + "version" : "2.0.0" + } + }, + { + "identity" : "semaphore", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/Semaphore", + "state" : { + "revision" : "f1c4a0acabeb591068dea6cffdd39660b86dec28", + "version" : "0.0.8" + } + }, + { + "identity" : "semver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kutchie-pelaez/Semver.git", + "state" : { + "revision" : "2b515fb1b5fc653e5f2140386f57b873853661e2", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "8d712376c99fc0267aa0e41fea732babe365270a", + "version" : "1.3.3" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-composable-architecture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-composable-architecture", + "state" : { + "revision" : "3568f01377c6c668aad40d066acf97ce670a1dad", + "version" : "1.5.6" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "350e1e119babe8525f9bd155b76640a5de270184", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "d533cd18b0b456b106694a9899f917ee595f2666", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" + } + }, + { + "identity" : "swift-parsing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-parsing", + "state" : { + "revision" : "a0e7d73f462c1c38c59dc40a3969ac40cea42950", + "version" : "0.13.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax", + "state" : { + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" + } + }, + { + "identity" : "swift-tagged", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-tagged", + "state" : { + "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", + "version" : "0.10.0" + } + }, + { + "identity" : "swiftbackports", + "kind" : "remoteSourceControl", + "location" : "https://github.com/shaps80/SwiftBackports", + "state" : { + "revision" : "ddca6a237c1ba2291d5a3cc47ec8480ce6e9f805", + "version" : "1.0.3" + } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "028487d4a8a291b2fe1b4392b5425b6172056148", + "version" : "2.7.2" + } + }, + { + "identity" : "swiftui-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swiftui-navigation", + "state" : { + "revision" : "2ec6c3a15293efff6083966b38439a4004f25565", + "version" : "1.3.0" + } + }, + { + "identity" : "swiftuibackports", + "kind" : "remoteSourceControl", + "location" : "https://github.com/shaps80/SwiftUIBackports.git", + "state" : { + "revision" : "f5f23b016eeda6642a0fe1020241af19c9c05556", + "version" : "2.8.1" + } + }, + { + "identity" : "timelanecombine", + "kind" : "remoteSourceControl", + "location" : "https://github.com/icanzilb/TimelaneCombine.git", + "state" : { + "revision" : "e6837bcbb19332866d5e37d501c05d68fbf985f2", + "version" : "2.0.0" + } + }, + { + "identity" : "timelanecore", + "kind" : "remoteSourceControl", + "location" : "https://github.com/icanzilb/TimelaneCore", + "state" : { + "revision" : "c554d6d61be14bd7acb8b10161fe5d9dc20d7fbc", + "version" : "2.0.2" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", + "version" : "1.1.2" + } + }, + { + "identity" : "xmlcoder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CoreOffice/XMLCoder.git", + "state" : { + "revision" : "b1e944cbd0ef33787b13f639a5418d55b3bed501", + "version" : "0.17.1" + } + } + ], + "version" : 2 +} diff --git a/App/Mochi.xcodeproj/project.xcworkspace/xcuserdata/babyyoda777.xcuserdatad/UserInterfaceState.xcuserstate b/App/Mochi.xcodeproj/project.xcworkspace/xcuserdata/babyyoda777.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..2a7a7fe Binary files /dev/null and b/App/Mochi.xcodeproj/project.xcworkspace/xcuserdata/babyyoda777.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/App/Mochi.xcodeproj/xcuserdata/babyyoda777.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/App/Mochi.xcodeproj/xcuserdata/babyyoda777.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..cf574ae --- /dev/null +++ b/App/Mochi.xcodeproj/xcuserdata/babyyoda777.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/App/Mochi.xcodeproj/xcuserdata/babyyoda777.xcuserdatad/xcschemes/xcschememanagement.plist b/App/Mochi.xcodeproj/xcuserdata/babyyoda777.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..425d768 --- /dev/null +++ b/App/Mochi.xcodeproj/xcuserdata/babyyoda777.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,56 @@ + + + + + SchemeUserState + + Mochi.xcscheme_^#shared#^_ + + orderHint + 0 + + Parsing (Playground) 1.xcscheme + + isShown + + orderHint + 19 + + Parsing (Playground) 2.xcscheme + + isShown + + orderHint + 20 + + Parsing (Playground).xcscheme + + isShown + + orderHint + 18 + + Tagged (Playground) 1.xcscheme + + isShown + + orderHint + 22 + + Tagged (Playground) 2.xcscheme + + isShown + + orderHint + 23 + + Tagged (Playground).xcscheme + + isShown + + orderHint + 21 + + + + diff --git a/App/Shared/AppDelegate 2.swift b/App/Shared/AppDelegate 2.swift new file mode 100644 index 0000000..02f1394 --- /dev/null +++ b/App/Shared/AppDelegate 2.swift @@ -0,0 +1,52 @@ +// +// AppDelegate.swift +// mochi +// +// Created by ErrorErrorError on 5/19/23. +// +// + +import App +import Architecture +import Foundation + +#if canImport(UIKit) +import UIKit + +class AppDelegate: UIResponder, UIApplicationDelegate { + let store = Store( + initialState: .init(), + reducer: { AppFeature() } + ) + + func application( + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + store.send(.internal(.appDelegate(.didFinishLaunching))) + UserDefaults.standard.register(defaults: [ + "userSettings.fastForwardAmount": 15, + "userSettings.fastBackwardAmount": 5 + ]) + return true + } +} + +#elseif canImport(AppKit) +import AppKit + +class AppDelegate: NSObject, NSApplicationDelegate { + let store = Store( + initialState: .init(), + reducer: { AppFeature() } + ) + + func applicationDidFinishLaunching(_: Notification) { + store.send(.internal(.appDelegate(.didFinishLaunching))) + } + + func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply { + .terminateNow + } +} +#endif diff --git a/App/Shared/AppDelegate.swift b/App/Shared/AppDelegate.swift index 9a5f818..0c2a3b7 100644 --- a/App/Shared/AppDelegate.swift +++ b/App/Shared/AppDelegate.swift @@ -2,7 +2,7 @@ // AppDelegate.swift // mochi // -// Created by ErrorErrorError on 5/19/23. +// Created by MochiTeam on 5/19/23. // // @@ -24,6 +24,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { store.send(.internal(.appDelegate(.didFinishLaunching))) + UserDefaults.standard.register(defaults: [ + "userSettings.fastForwardAmount": 15, + "userSettings.fastBackwardAmount": 5 + ]) return true } } diff --git a/App/Shared/MochiApp.swift b/App/Shared/MochiApp.swift index d2c3bf9..84eecd7 100644 --- a/App/Shared/MochiApp.swift +++ b/App/Shared/MochiApp.swift @@ -2,7 +2,7 @@ // MochiApp.swift // mochi // -// Created by ErrorErrorError on 3/24/23. +// Created by MochiTeam on 3/24/23. // // diff --git a/App/Shared/mochi-info.plist b/App/Shared/mochi-info.plist index f26dfc4..7edbf4e 100644 --- a/App/Shared/mochi-info.plist +++ b/App/Shared/mochi-info.plist @@ -8,7 +8,7 @@ CFBundleTypeRole Viewer CFBundleURLName - dev.errorerrorerror.mochi + dev.MochiTeam.mochi CFBundleURLSchemes mochi diff --git a/App/Shared/mochi.entitlements b/App/Shared/mochi.entitlements index ee95ab7..8fa9cdb 100644 --- a/App/Shared/mochi.entitlements +++ b/App/Shared/mochi.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.files.downloads.read-write + + com.apple.security.files.user-selected.read-write + com.apple.security.network.client diff --git a/App/iOS/PreferenceHostingController 2.swift b/App/iOS/PreferenceHostingController 2.swift new file mode 100644 index 0000000..df94d3c --- /dev/null +++ b/App/iOS/PreferenceHostingController 2.swift @@ -0,0 +1,67 @@ +// +// PreferenceHostingController.swift +// mochi +// +// Created by ErrorErrorError on 6/27/23. +// +// + +#if canImport(UIKit) +import Foundation +import SwiftUI +import UIKit +import ViewComponents + +final class PreferenceHostingController: UIHostingController>, OpaquePreferenceHostingController { + override var prefersHomeIndicatorAutoHidden: Bool { _homeIndicatorAutoHidden } + + var _homeIndicatorAutoHidden = false { + didSet { + setNeedsUpdateOfHomeIndicatorAutoHidden() + } + } + + private let box: Box + + init(rootView: @escaping () -> Root) { + self.box = .init() + super.init(rootView: .init(box: box, content: rootView)) + box.object = self + } + + @available(*, unavailable) + dynamic required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +struct BoxedView: View { + let box: Box + + init(box: Box, content: @escaping () -> Content) { + self.content = content + self.box = box + } + + let content: () -> Content + + var body: some View { + content() + .onPreferenceChange(HomeIndicatorAutoHiddenPreferenceKey.self) { isHidden in + box.object?._homeIndicatorAutoHidden = isHidden + } + } +} + +final class Box { + weak var object: OpaquePreferenceHostingController? +} + +@MainActor +protocol OpaquePreferenceProperties { + var _homeIndicatorAutoHidden: Bool { get set } +} + +@MainActor +protocol OpaquePreferenceHostingController: OpaquePreferenceProperties, UIViewController {} +#endif diff --git a/App/iOS/PreferenceHostingController.swift b/App/iOS/PreferenceHostingController.swift index df94d3c..f353da3 100644 --- a/App/iOS/PreferenceHostingController.swift +++ b/App/iOS/PreferenceHostingController.swift @@ -2,7 +2,7 @@ // PreferenceHostingController.swift // mochi // -// Created by ErrorErrorError on 6/27/23. +// Created by MochiTeam on 6/27/23. // // diff --git a/App/iOS/PreferenceHostingView.swift b/App/iOS/PreferenceHostingView.swift index 3f9fcad..93b0898 100644 --- a/App/iOS/PreferenceHostingView.swift +++ b/App/iOS/PreferenceHostingView.swift @@ -2,7 +2,7 @@ // PreferenceHostingView.swift // Mochi // -// Created by ErrorErrorError on 11/28/23. +// Created by MochiTeam on 11/28/23. // // diff --git a/Package.swift b/Package.swift index 9f537b2..c9e666b 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,7 @@ extension [TestTarget]: TestTargets { // CSettingsBuilder.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -100,7 +100,7 @@ extension LanguageTag { // Macro.swift // // -// Created by ErrorErrorError on 10/11/23. +// Created by MochiTeam on 10/11/23. // // @@ -531,7 +531,7 @@ protocol TestTargets: Sequence where Element == TestTarget { // Testable.swift // // -// Created by ErrorErrorError on 10/13/23. +// Created by MochiTeam on 10/13/23. // // @@ -673,7 +673,7 @@ extension _PackageDescription_Target { // _Path.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -692,7 +692,7 @@ extension _Path { // AnalyticsClient.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // @@ -707,7 +707,7 @@ struct AnalyticsClient: _Client { // BuildClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -723,7 +723,7 @@ struct BuildClient: _Client { // ClipboardClient.swift // // -// Created by ErrorErrorError on 12/15/23. +// Created by MochiTeam on 12/15/23. // // @@ -738,7 +738,7 @@ struct ClipboardClient: _Client { // DatabaseClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -760,7 +760,7 @@ struct DatabaseClient: _Client { // DeviceClient.swift // // -// Created by ErrorErrorError on 11/29/23. +// Created by MochiTeam on 11/29/23. // // @@ -773,20 +773,21 @@ struct DeviceClient: _Client { // FileClient.swift // // -// Created by ErrorErrorError on 10/6/23. +// Created by MochiTeam on 10/6/23. // // struct FileClient: _Client { var dependencies: any Dependencies { ComposableArchitecture() + SharedModels() } } // // LocalizableClient.swift // // -// Created by ErrorErrorError on 12/1/23. +// Created by MochiTeam on 12/1/23. // // @@ -805,7 +806,7 @@ struct LocalizableClient: _Client { // LoggerClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -821,7 +822,7 @@ struct LoggerClient: _Client { // ModuleClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -860,10 +861,27 @@ extension ModuleClient: Testable { } } // +// OfflineManagerClient.swift +// +// +// Created by MochiTeam on 06.04.2024. +// + +import Foundation + +struct OfflineManagerClient: _Client { + var dependencies: any Dependencies { + FileClient() + SharedModels() + ComposableArchitecture() + FlyingFox() + } +} +// // PlayerClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -885,7 +903,7 @@ struct PlayerClient: _Client { // PlaylistHistoryClient.swift // // -// Created by DeNeRr on 29.01.2024. +// Created by MochiTeam on 29.01.2024. // import Foundation @@ -903,7 +921,7 @@ struct PlaylistHistoryClient: _Client { // RepoClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -923,7 +941,7 @@ struct RepoClient: _Client { // UserDefaultsClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -938,7 +956,7 @@ struct UserDefaultsClient: _Client { // UserSettingsClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -955,7 +973,7 @@ struct UserSettingsClient: _Client { // _Client.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -974,7 +992,7 @@ extension _Client { // ComposableArchitecture.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // @@ -987,7 +1005,7 @@ struct ComposableArchitecture: PackageDependency { // CustomDump.swift // // -// Created by ErrorErrorError on 1/1/24. +// Created by MochiTeam on 1/1/24. // // @@ -995,14 +1013,14 @@ import Foundation struct CustomDump: PackageDependency { var dependency: Package.Dependency { - .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0") + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.2.1") } } // // FluidGradient.swift // // -// Created by ErrorErrorError on 10/11/23. +// Created by MochiTeam on 10/11/23. // // @@ -1014,10 +1032,24 @@ struct FluidGradient: PackageDependency { } } // +// FlyingFox.swift +// +// +// Created by MochiTeam on 09.05.2024. +// + +import Foundation + +struct FlyingFox: PackageDependency { + var dependency: Package.Dependency { + .package(url: "https://github.com/swhitty/FlyingFox.git", .upToNextMajor(from: "0.14.0")) + } +} +// // Nuke.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // @@ -1043,7 +1075,7 @@ struct NukeUI: PackageDependency { // Parsing.swift // // -// Created by ErrorErrorError on 12/17/23. +// Created by MochiTeam on 12/17/23. // // @@ -1056,7 +1088,7 @@ struct Parsing: PackageDependency { // Semaphore.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // @@ -1069,7 +1101,7 @@ struct Semaphore: PackageDependency { // Semver.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // @@ -1082,7 +1114,7 @@ struct Semver: PackageDependency { // SwiftLog.swift // // -// Created by ErrorErrorError on 11/9/23. +// Created by MochiTeam on 11/9/23. // // @@ -1112,7 +1144,7 @@ struct Logging: _Depending, Dependency { // SwiftSoup.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // @@ -1125,7 +1157,7 @@ struct SwiftSoup: PackageDependency { // SwiftSyntax.swift // // -// Created by ErrorErrorError on 10/11/23. +// Created by MochiTeam on 10/11/23. // // @@ -1166,7 +1198,7 @@ struct SwiftCompilerPlugin: _Depending, Dependency { // SwiftUIBackports.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // @@ -1179,7 +1211,7 @@ struct SwiftUIBackports: PackageDependency { // Tagged.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1194,7 +1226,7 @@ struct Tagged: PackageDependency { // XMLCoder.swift // // -// Created by ErrorErrorError on 12/27/23. +// Created by MochiTeam on 12/27/23. // // @@ -1207,7 +1239,7 @@ struct XMLCoder: PackageDependency { // ContentCore.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1218,6 +1250,8 @@ struct ContentCore: _Feature { Architecture() FoundationHelpers() ModuleClient() + PlaylistHistoryClient() + OfflineManagerClient() LoggerClient() Tagged() ComposableArchitecture() @@ -1228,7 +1262,7 @@ struct ContentCore: _Feature { // Discover.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1247,13 +1281,57 @@ struct Discover: _Feature { ViewComponents() ComposableArchitecture() NukeUI() + OfflineManagerClient() + FileClient() + } +} +// +// DownloadQueue.swift +// +// +// Created by MochiTeam on 16.05.2024. +// + +import Foundation + +struct DownloadQueue: _Feature { + var dependencies: any Dependencies { + Architecture() + FileClient() + ViewComponents() + ComposableArchitecture() + OfflineManagerClient() + Styling() + } +} +// +// Library.swift +// +// +// Created by MochiTeam on 09.04.2024. +// + +import Foundation + +struct Library: _Feature { + var dependencies: any Dependencies { + Architecture() + FileClient() + ViewComponents() + ComposableArchitecture() + OfflineManagerClient() + Styling() + PlaylistDetails() + DownloadQueue() + NukeUI() + SharedModels() } } // // MochiApp.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // @@ -1265,6 +1343,8 @@ struct MochiApp: _Feature { var dependencies: any Dependencies { Architecture() Discover() + Library() + DownloadQueue() Repos() Settings() SharedModels() @@ -1280,7 +1360,7 @@ struct MochiApp: _Feature { // ModuleLists.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1300,7 +1380,7 @@ struct ModuleLists: _Feature { // PlaylistDetails.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1313,6 +1393,7 @@ struct PlaylistDetails: _Feature { LoggerClient() ModuleClient() RepoClient() + OfflineManagerClient() PlaylistHistoryClient() Styling() SharedModels() @@ -1325,7 +1406,7 @@ struct PlaylistDetails: _Feature { // Repos.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1348,7 +1429,7 @@ struct Repos: _Feature { // Search.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1373,7 +1454,7 @@ struct Search: _Feature { // Settings.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1387,6 +1468,7 @@ struct Settings: _Feature { SharedModels() Styling() ViewComponents() + PlaylistHistoryClient() UserSettingsClient() ComposableArchitecture() NukeUI() @@ -1396,7 +1478,7 @@ struct Settings: _Feature { // VideoPlayer.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1418,7 +1500,7 @@ struct VideoPlayer: _Feature { // _Feature.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1437,7 +1519,7 @@ extension _Feature { // CoreDBMacros.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // @@ -1451,7 +1533,7 @@ struct CoreDBMacros: _Macro { // _Macro.swift // // -// Created by ErrorErrorError on 10/27/23. +// Created by MochiTeam on 10/27/23. // // @@ -1470,7 +1552,7 @@ extension _Macro { // MochiPlatforms.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // @@ -1486,7 +1568,7 @@ struct MochiPlatforms: PlatformSet { // Architecture.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1502,7 +1584,7 @@ struct Architecture: _Shared { // CoreDB.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // @@ -1530,7 +1612,7 @@ extension CoreDB: Testable { // FoundationHelpers.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1539,7 +1621,7 @@ struct FoundationHelpers: _Shared {} // JSValueCoder.swift // // -// Created by ErrorErrorError on 11/6/23. +// Created by MochiTeam on 11/6/23. // // @@ -1564,7 +1646,7 @@ extension JSValueCoder: Testable { // SharedModels.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1583,7 +1665,7 @@ struct SharedModels: _Shared { // Styling.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1602,7 +1684,7 @@ struct Styling: _Shared { // ViewComponents.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1619,7 +1701,7 @@ struct ViewComponents: _Shared { // _Shared.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -1638,7 +1720,7 @@ extension _Shared { // Index.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // @@ -1651,6 +1733,8 @@ let package = Package { ModuleLists() PlaylistDetails() Discover() + Library() + DownloadQueue() Repos() Search() Settings() diff --git a/Package/Sources/Clients/AnalyticsClient.swift b/Package/Sources/Clients/AnalyticsClient.swift index 255b907..57b34dc 100644 --- a/Package/Sources/Clients/AnalyticsClient.swift +++ b/Package/Sources/Clients/AnalyticsClient.swift @@ -2,7 +2,7 @@ // AnalyticsClient.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // diff --git a/Package/Sources/Clients/BuildClient.swift b/Package/Sources/Clients/BuildClient.swift index 57c5e26..736cdc7 100644 --- a/Package/Sources/Clients/BuildClient.swift +++ b/Package/Sources/Clients/BuildClient.swift @@ -2,7 +2,7 @@ // BuildClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Clients/ClipboardClient.swift b/Package/Sources/Clients/ClipboardClient.swift index 4217b3f..a30ad55 100644 --- a/Package/Sources/Clients/ClipboardClient.swift +++ b/Package/Sources/Clients/ClipboardClient.swift @@ -2,7 +2,7 @@ // ClipboardClient.swift // // -// Created by ErrorErrorError on 12/15/23. +// Created by MochiTeam on 12/15/23. // // diff --git a/Package/Sources/Clients/DatabaseClient.swift b/Package/Sources/Clients/DatabaseClient.swift index 2f14ffc..2689f0b 100644 --- a/Package/Sources/Clients/DatabaseClient.swift +++ b/Package/Sources/Clients/DatabaseClient.swift @@ -2,7 +2,7 @@ // DatabaseClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Clients/DeviceClient.swift b/Package/Sources/Clients/DeviceClient.swift index 2e5fb6b..d683623 100644 --- a/Package/Sources/Clients/DeviceClient.swift +++ b/Package/Sources/Clients/DeviceClient.swift @@ -2,7 +2,7 @@ // DeviceClient.swift // // -// Created by ErrorErrorError on 11/29/23. +// Created by MochiTeam on 11/29/23. // // diff --git a/Package/Sources/Clients/FileClient.swift b/Package/Sources/Clients/FileClient.swift index 94015e2..514768b 100644 --- a/Package/Sources/Clients/FileClient.swift +++ b/Package/Sources/Clients/FileClient.swift @@ -2,12 +2,13 @@ // FileClient.swift // // -// Created by ErrorErrorError on 10/6/23. +// Created by MochiTeam on 10/6/23. // // struct FileClient: _Client { var dependencies: any Dependencies { ComposableArchitecture() + SharedModels() } } diff --git a/Package/Sources/Clients/LocalizableClient.swift b/Package/Sources/Clients/LocalizableClient.swift index 9d21838..8514e0e 100644 --- a/Package/Sources/Clients/LocalizableClient.swift +++ b/Package/Sources/Clients/LocalizableClient.swift @@ -2,7 +2,7 @@ // LocalizableClient.swift // // -// Created by ErrorErrorError on 12/1/23. +// Created by MochiTeam on 12/1/23. // // diff --git a/Package/Sources/Clients/LoggerClient.swift b/Package/Sources/Clients/LoggerClient.swift index 29c121c..a556245 100644 --- a/Package/Sources/Clients/LoggerClient.swift +++ b/Package/Sources/Clients/LoggerClient.swift @@ -2,7 +2,7 @@ // LoggerClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Clients/ModuleClient.swift b/Package/Sources/Clients/ModuleClient.swift index 57ff9ab..064b27d 100644 --- a/Package/Sources/Clients/ModuleClient.swift +++ b/Package/Sources/Clients/ModuleClient.swift @@ -2,7 +2,7 @@ // ModuleClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Clients/OfflineManagerClient.swift b/Package/Sources/Clients/OfflineManagerClient.swift new file mode 100644 index 0000000..82c48ec --- /dev/null +++ b/Package/Sources/Clients/OfflineManagerClient.swift @@ -0,0 +1,17 @@ +// +// OfflineManagerClient.swift +// +// +// Created by MochiTeam on 06.04.2024. +// + +import Foundation + +struct OfflineManagerClient: _Client { + var dependencies: any Dependencies { + FileClient() + SharedModels() + ComposableArchitecture() + FlyingFox() + } +} diff --git a/Package/Sources/Clients/PlayerClient.swift b/Package/Sources/Clients/PlayerClient.swift index cb3de95..a0903ff 100644 --- a/Package/Sources/Clients/PlayerClient.swift +++ b/Package/Sources/Clients/PlayerClient.swift @@ -2,7 +2,7 @@ // PlayerClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Clients/PlaylistHistoryClient.swift b/Package/Sources/Clients/PlaylistHistoryClient.swift index b6f0b5e..6ef2e42 100644 --- a/Package/Sources/Clients/PlaylistHistoryClient.swift +++ b/Package/Sources/Clients/PlaylistHistoryClient.swift @@ -2,7 +2,7 @@ // PlaylistHistoryClient.swift // // -// Created by DeNeRr on 29.01.2024. +// Created by MochiTeam on 29.01.2024. // import Foundation diff --git a/Package/Sources/Clients/RepoClient.swift b/Package/Sources/Clients/RepoClient.swift index 6a97f80..972d288 100644 --- a/Package/Sources/Clients/RepoClient.swift +++ b/Package/Sources/Clients/RepoClient.swift @@ -2,7 +2,7 @@ // RepoClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Clients/UserDefaultsClient.swift b/Package/Sources/Clients/UserDefaultsClient.swift index af4a0ee..80bb57d 100644 --- a/Package/Sources/Clients/UserDefaultsClient.swift +++ b/Package/Sources/Clients/UserDefaultsClient.swift @@ -2,7 +2,7 @@ // UserDefaultsClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Clients/UserSettingsClient.swift b/Package/Sources/Clients/UserSettingsClient.swift index fcd4b52..3164e98 100644 --- a/Package/Sources/Clients/UserSettingsClient.swift +++ b/Package/Sources/Clients/UserSettingsClient.swift @@ -2,7 +2,7 @@ // UserSettingsClient.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Clients/_Client.swift b/Package/Sources/Clients/_Client.swift index 43b2d7e..4a3256f 100644 --- a/Package/Sources/Clients/_Client.swift +++ b/Package/Sources/Clients/_Client.swift @@ -2,7 +2,7 @@ // _Client.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Dependencies/ComposableArchitecture.swift b/Package/Sources/Dependencies/ComposableArchitecture.swift index c8f4861..70f8227 100644 --- a/Package/Sources/Dependencies/ComposableArchitecture.swift +++ b/Package/Sources/Dependencies/ComposableArchitecture.swift @@ -2,7 +2,7 @@ // ComposableArchitecture.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // diff --git a/Package/Sources/Dependencies/CustomDump.swift b/Package/Sources/Dependencies/CustomDump.swift index b6ef44f..40a76b2 100644 --- a/Package/Sources/Dependencies/CustomDump.swift +++ b/Package/Sources/Dependencies/CustomDump.swift @@ -2,7 +2,7 @@ // CustomDump.swift // // -// Created by ErrorErrorError on 1/1/24. +// Created by MochiTeam on 1/1/24. // // diff --git a/Package/Sources/Dependencies/FluidGradient.swift b/Package/Sources/Dependencies/FluidGradient.swift index 78f32ec..bdeef7b 100644 --- a/Package/Sources/Dependencies/FluidGradient.swift +++ b/Package/Sources/Dependencies/FluidGradient.swift @@ -2,7 +2,7 @@ // FluidGradient.swift // // -// Created by ErrorErrorError on 10/11/23. +// Created by MochiTeam on 10/11/23. // // diff --git a/Package/Sources/Dependencies/FlyingFox.swift b/Package/Sources/Dependencies/FlyingFox.swift new file mode 100644 index 0000000..26bba75 --- /dev/null +++ b/Package/Sources/Dependencies/FlyingFox.swift @@ -0,0 +1,14 @@ +// +// FlyingFox.swift +// +// +// Created by MochiTeam on 09.05.2024. +// + +import Foundation + +struct FlyingFox: PackageDependency { + var dependency: Package.Dependency { + .package(url: "https://github.com/swhitty/FlyingFox.git", .upToNextMajor(from: "0.14.0")) + } +} diff --git a/Package/Sources/Dependencies/Nuke.swift b/Package/Sources/Dependencies/Nuke.swift index 0c1d9b0..0ba1a68 100644 --- a/Package/Sources/Dependencies/Nuke.swift +++ b/Package/Sources/Dependencies/Nuke.swift @@ -2,7 +2,7 @@ // Nuke.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // diff --git a/Package/Sources/Dependencies/Parsing.swift b/Package/Sources/Dependencies/Parsing.swift index 71203f1..fef5ec9 100644 --- a/Package/Sources/Dependencies/Parsing.swift +++ b/Package/Sources/Dependencies/Parsing.swift @@ -2,7 +2,7 @@ // Parsing.swift // // -// Created by ErrorErrorError on 12/17/23. +// Created by MochiTeam on 12/17/23. // // diff --git a/Package/Sources/Dependencies/Semaphore.swift b/Package/Sources/Dependencies/Semaphore.swift index 3f2d66a..7c2f45f 100644 --- a/Package/Sources/Dependencies/Semaphore.swift +++ b/Package/Sources/Dependencies/Semaphore.swift @@ -2,7 +2,7 @@ // Semaphore.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // diff --git a/Package/Sources/Dependencies/Semver.swift b/Package/Sources/Dependencies/Semver.swift index 5b100b0..e837e33 100644 --- a/Package/Sources/Dependencies/Semver.swift +++ b/Package/Sources/Dependencies/Semver.swift @@ -2,7 +2,7 @@ // Semver.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // diff --git a/Package/Sources/Dependencies/SwiftLog.swift b/Package/Sources/Dependencies/SwiftLog.swift index 7045e92..64719d0 100644 --- a/Package/Sources/Dependencies/SwiftLog.swift +++ b/Package/Sources/Dependencies/SwiftLog.swift @@ -2,7 +2,7 @@ // SwiftLog.swift // // -// Created by ErrorErrorError on 11/9/23. +// Created by MochiTeam on 11/9/23. // // diff --git a/Package/Sources/Dependencies/SwiftSoup.swift b/Package/Sources/Dependencies/SwiftSoup.swift index a351f96..daaa7ba 100644 --- a/Package/Sources/Dependencies/SwiftSoup.swift +++ b/Package/Sources/Dependencies/SwiftSoup.swift @@ -2,7 +2,7 @@ // SwiftSoup.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // diff --git a/Package/Sources/Dependencies/SwiftSyntax.swift b/Package/Sources/Dependencies/SwiftSyntax.swift index a228801..4abcb84 100644 --- a/Package/Sources/Dependencies/SwiftSyntax.swift +++ b/Package/Sources/Dependencies/SwiftSyntax.swift @@ -2,7 +2,7 @@ // SwiftSyntax.swift // // -// Created by ErrorErrorError on 10/11/23. +// Created by MochiTeam on 10/11/23. // // diff --git a/Package/Sources/Dependencies/SwiftUIBackports.swift b/Package/Sources/Dependencies/SwiftUIBackports.swift index a370293..39a7fd3 100644 --- a/Package/Sources/Dependencies/SwiftUIBackports.swift +++ b/Package/Sources/Dependencies/SwiftUIBackports.swift @@ -2,7 +2,7 @@ // SwiftUIBackports.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // diff --git a/Package/Sources/Dependencies/Tagged.swift b/Package/Sources/Dependencies/Tagged.swift index 234113d..cf91250 100644 --- a/Package/Sources/Dependencies/Tagged.swift +++ b/Package/Sources/Dependencies/Tagged.swift @@ -2,7 +2,7 @@ // Tagged.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Dependencies/XMLCoder.swift b/Package/Sources/Dependencies/XMLCoder.swift index abb2888..e42271d 100644 --- a/Package/Sources/Dependencies/XMLCoder.swift +++ b/Package/Sources/Dependencies/XMLCoder.swift @@ -2,7 +2,7 @@ // XMLCoder.swift // // -// Created by ErrorErrorError on 12/27/23. +// Created by MochiTeam on 12/27/23. // // diff --git a/Package/Sources/Features/ContentCore.swift b/Package/Sources/Features/ContentCore.swift index 2a8495e..c11d13f 100644 --- a/Package/Sources/Features/ContentCore.swift +++ b/Package/Sources/Features/ContentCore.swift @@ -2,7 +2,7 @@ // ContentCore.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -13,6 +13,8 @@ struct ContentCore: _Feature { Architecture() FoundationHelpers() ModuleClient() + PlaylistHistoryClient() + OfflineManagerClient() LoggerClient() Tagged() ComposableArchitecture() diff --git a/Package/Sources/Features/Discover.swift b/Package/Sources/Features/Discover.swift index 4d3ba1b..be7ad82 100644 --- a/Package/Sources/Features/Discover.swift +++ b/Package/Sources/Features/Discover.swift @@ -2,7 +2,7 @@ // Discover.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -21,5 +21,7 @@ struct Discover: _Feature { ViewComponents() ComposableArchitecture() NukeUI() + OfflineManagerClient() + FileClient() } } diff --git a/Package/Sources/Features/DownloadQueue.swift b/Package/Sources/Features/DownloadQueue.swift new file mode 100644 index 0000000..cf7745d --- /dev/null +++ b/Package/Sources/Features/DownloadQueue.swift @@ -0,0 +1,19 @@ +// +// DownloadQueue.swift +// +// +// Created by MochiTeam on 16.05.2024. +// + +import Foundation + +struct DownloadQueue: _Feature { + var dependencies: any Dependencies { + Architecture() + FileClient() + ViewComponents() + ComposableArchitecture() + OfflineManagerClient() + Styling() + } +} diff --git a/Package/Sources/Features/Library.swift b/Package/Sources/Features/Library.swift new file mode 100644 index 0000000..f0faf38 --- /dev/null +++ b/Package/Sources/Features/Library.swift @@ -0,0 +1,23 @@ +// +// Library.swift +// +// +// Created by MochiTeam on 09.04.2024. +// + +import Foundation + +struct Library: _Feature { + var dependencies: any Dependencies { + Architecture() + FileClient() + ViewComponents() + ComposableArchitecture() + OfflineManagerClient() + Styling() + PlaylistDetails() + DownloadQueue() + NukeUI() + SharedModels() + } +} diff --git a/Package/Sources/Features/MochiApp.swift b/Package/Sources/Features/MochiApp.swift index 964b5c9..b417bca 100644 --- a/Package/Sources/Features/MochiApp.swift +++ b/Package/Sources/Features/MochiApp.swift @@ -2,7 +2,7 @@ // MochiApp.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // @@ -14,6 +14,8 @@ struct MochiApp: _Feature { var dependencies: any Dependencies { Architecture() Discover() + Library() + DownloadQueue() Repos() Settings() SharedModels() diff --git a/Package/Sources/Features/ModuleLists.swift b/Package/Sources/Features/ModuleLists.swift index 2327b76..e54e08b 100644 --- a/Package/Sources/Features/ModuleLists.swift +++ b/Package/Sources/Features/ModuleLists.swift @@ -2,7 +2,7 @@ // ModuleLists.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Features/PlaylistDetails.swift b/Package/Sources/Features/PlaylistDetails.swift index c733c9d..5196374 100644 --- a/Package/Sources/Features/PlaylistDetails.swift +++ b/Package/Sources/Features/PlaylistDetails.swift @@ -2,7 +2,7 @@ // PlaylistDetails.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -15,6 +15,7 @@ struct PlaylistDetails: _Feature { LoggerClient() ModuleClient() RepoClient() + OfflineManagerClient() PlaylistHistoryClient() Styling() SharedModels() diff --git a/Package/Sources/Features/Repos.swift b/Package/Sources/Features/Repos.swift index a436061..145d5ef 100644 --- a/Package/Sources/Features/Repos.swift +++ b/Package/Sources/Features/Repos.swift @@ -2,7 +2,7 @@ // Repos.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Features/Search.swift b/Package/Sources/Features/Search.swift index 4e980f7..bfae522 100644 --- a/Package/Sources/Features/Search.swift +++ b/Package/Sources/Features/Search.swift @@ -2,7 +2,7 @@ // Search.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Features/Settings.swift b/Package/Sources/Features/Settings.swift index 000ed64..288b91a 100644 --- a/Package/Sources/Features/Settings.swift +++ b/Package/Sources/Features/Settings.swift @@ -2,7 +2,7 @@ // Settings.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // @@ -16,6 +16,7 @@ struct Settings: _Feature { SharedModels() Styling() ViewComponents() + PlaylistHistoryClient() UserSettingsClient() ComposableArchitecture() NukeUI() diff --git a/Package/Sources/Features/VideoPlayer.swift b/Package/Sources/Features/VideoPlayer.swift index b862fa1..0fd14c6 100644 --- a/Package/Sources/Features/VideoPlayer.swift +++ b/Package/Sources/Features/VideoPlayer.swift @@ -2,7 +2,7 @@ // VideoPlayer.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Features/_Feature.swift b/Package/Sources/Features/_Feature.swift index 8988146..c6bbbf7 100644 --- a/Package/Sources/Features/_Feature.swift +++ b/Package/Sources/Features/_Feature.swift @@ -2,7 +2,7 @@ // _Feature.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Index.swift b/Package/Sources/Index.swift index 83c0522..da15f29 100644 --- a/Package/Sources/Index.swift +++ b/Package/Sources/Index.swift @@ -2,7 +2,7 @@ // Index.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // @@ -15,6 +15,8 @@ let package = Package { ModuleLists() PlaylistDetails() Discover() + Library() + DownloadQueue() Repos() Search() Settings() diff --git a/Package/Sources/Macros/CoreDBMacros.swift b/Package/Sources/Macros/CoreDBMacros.swift index 04bb554..36b69f3 100644 --- a/Package/Sources/Macros/CoreDBMacros.swift +++ b/Package/Sources/Macros/CoreDBMacros.swift @@ -2,7 +2,7 @@ // CoreDBMacros.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // diff --git a/Package/Sources/Macros/_Macro.swift b/Package/Sources/Macros/_Macro.swift index a510cd4..e00ed78 100644 --- a/Package/Sources/Macros/_Macro.swift +++ b/Package/Sources/Macros/_Macro.swift @@ -2,7 +2,7 @@ // _Macro.swift // // -// Created by ErrorErrorError on 10/27/23. +// Created by MochiTeam on 10/27/23. // // diff --git a/Package/Sources/Platforms/MochiPlatforms.swift b/Package/Sources/Platforms/MochiPlatforms.swift index d3fa63e..7458126 100644 --- a/Package/Sources/Platforms/MochiPlatforms.swift +++ b/Package/Sources/Platforms/MochiPlatforms.swift @@ -2,7 +2,7 @@ // MochiPlatforms.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // diff --git a/Package/Sources/Shared/Architecture.swift b/Package/Sources/Shared/Architecture.swift index 9af7860..c1a2ebf 100644 --- a/Package/Sources/Shared/Architecture.swift +++ b/Package/Sources/Shared/Architecture.swift @@ -2,7 +2,7 @@ // Architecture.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Shared/CoreDB.swift b/Package/Sources/Shared/CoreDB.swift index 8b0297e..2813c2b 100644 --- a/Package/Sources/Shared/CoreDB.swift +++ b/Package/Sources/Shared/CoreDB.swift @@ -2,7 +2,7 @@ // CoreDB.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // diff --git a/Package/Sources/Shared/FoundationHelpers.swift b/Package/Sources/Shared/FoundationHelpers.swift index 2ca879a..815af3c 100644 --- a/Package/Sources/Shared/FoundationHelpers.swift +++ b/Package/Sources/Shared/FoundationHelpers.swift @@ -2,7 +2,7 @@ // FoundationHelpers.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Shared/JSValueCoder.swift b/Package/Sources/Shared/JSValueCoder.swift index f7d42d1..d5b5d66 100644 --- a/Package/Sources/Shared/JSValueCoder.swift +++ b/Package/Sources/Shared/JSValueCoder.swift @@ -2,7 +2,7 @@ // JSValueCoder.swift // // -// Created by ErrorErrorError on 11/6/23. +// Created by MochiTeam on 11/6/23. // // diff --git a/Package/Sources/Shared/SharedModels.swift b/Package/Sources/Shared/SharedModels.swift index 4599c13..c3f0057 100644 --- a/Package/Sources/Shared/SharedModels.swift +++ b/Package/Sources/Shared/SharedModels.swift @@ -2,7 +2,7 @@ // SharedModels.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Shared/Styling.swift b/Package/Sources/Shared/Styling.swift index b0c22b8..c2d105c 100644 --- a/Package/Sources/Shared/Styling.swift +++ b/Package/Sources/Shared/Styling.swift @@ -2,7 +2,7 @@ // Styling.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Shared/ViewComponents.swift b/Package/Sources/Shared/ViewComponents.swift index e72364a..ef26205 100644 --- a/Package/Sources/Shared/ViewComponents.swift +++ b/Package/Sources/Shared/ViewComponents.swift @@ -2,7 +2,7 @@ // ViewComponents.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Sources/Shared/_Shared.swift b/Package/Sources/Shared/_Shared.swift index dab1af1..65c2249 100644 --- a/Package/Sources/Shared/_Shared.swift +++ b/Package/Sources/Shared/_Shared.swift @@ -2,7 +2,7 @@ // _Shared.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Support/CSettingsBuilder.swift b/Package/Support/CSettingsBuilder.swift index f992f92..eaafebd 100644 --- a/Package/Support/CSettingsBuilder.swift +++ b/Package/Support/CSettingsBuilder.swift @@ -2,7 +2,7 @@ // CSettingsBuilder.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Package/Support/Macro.swift b/Package/Support/Macro.swift index 37e6669..13d76f4 100644 --- a/Package/Support/Macro.swift +++ b/Package/Support/Macro.swift @@ -2,7 +2,7 @@ // Macro.swift // // -// Created by ErrorErrorError on 10/11/23. +// Created by MochiTeam on 10/11/23. // // diff --git a/Package/Support/Testable.swift b/Package/Support/Testable.swift index 468c0a6..508e812 100644 --- a/Package/Support/Testable.swift +++ b/Package/Support/Testable.swift @@ -2,7 +2,7 @@ // Testable.swift // // -// Created by ErrorErrorError on 10/13/23. +// Created by MochiTeam on 10/13/23. // // diff --git a/Package/Support/_Path.swift b/Package/Support/_Path.swift index 628e737..4ccde4e 100644 --- a/Package/Support/_Path.swift +++ b/Package/Support/_Path.swift @@ -2,7 +2,7 @@ // _Path.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/README.md b/README.md index 0b8643f..3df4c83 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This application is currently a work-in-progress, so expect bugs and things brea ## Features - [X] Ad-Free, forever - [X] Source-based modular system (powered by JavaScriptCore) -- [ ] Offline downloads and local data support +- [X] Offline downloads and local data support - [ ] iCloud sync support - [ ] Tracker support - [ ] Integrated with Apple ecosystem @@ -22,7 +22,7 @@ This application is currently a work-in-progress, so expect bugs and things brea *TBA (using TestFlight)* ## Module development -*TBA* +https://mochisite.verce.app ## Contribution Any contribution is greatly appreciated. This application's structure is based on [swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture). diff --git a/Sources/Clients/AnalyticsClient/Client.swift b/Sources/Clients/AnalyticsClient/Client.swift index 05c0525..ed2fd56 100644 --- a/Sources/Clients/AnalyticsClient/Client.swift +++ b/Sources/Clients/AnalyticsClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created ErrorErrorError on 5/19/23. +// Created MochiTeam on 5/19/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/AnalyticsClient/Live.swift b/Sources/Clients/AnalyticsClient/Live.swift index 1a1b8ce..a2cd5c8 100644 --- a/Sources/Clients/AnalyticsClient/Live.swift +++ b/Sources/Clients/AnalyticsClient/Live.swift @@ -2,7 +2,7 @@ // Live.swift // // -// Created ErrorErrorError on 5/19/23. +// Created MochiTeam on 5/19/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/AnalyticsClient/Models.swift b/Sources/Clients/AnalyticsClient/Models.swift index 3250dd4..14e03e3 100644 --- a/Sources/Clients/AnalyticsClient/Models.swift +++ b/Sources/Clients/AnalyticsClient/Models.swift @@ -2,7 +2,7 @@ // Models.swift // // -// Created by ErrorErrorError on 5/19/23. +// Created by MochiTeam on 5/19/23. // // diff --git a/Sources/Clients/AnalyticsClient/Reducer.swift b/Sources/Clients/AnalyticsClient/Reducer.swift index a5358df..6e9e92f 100644 --- a/Sources/Clients/AnalyticsClient/Reducer.swift +++ b/Sources/Clients/AnalyticsClient/Reducer.swift @@ -2,7 +2,7 @@ // Reducer.swift // // -// Created by ErrorErrorError on 5/19/23. +// Created by MochiTeam on 5/19/23. // // diff --git a/Sources/Clients/BuildClient/Client.swift b/Sources/Clients/BuildClient/Client.swift index a940b76..421901e 100644 --- a/Sources/Clients/BuildClient/Client.swift +++ b/Sources/Clients/BuildClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created by ErrorErrorError on 7/28/23. +// Created by MochiTeam on 7/28/23. // // diff --git a/Sources/Clients/BuildClient/Model.swift b/Sources/Clients/BuildClient/Model.swift index 0308ea6..d51a742 100644 --- a/Sources/Clients/BuildClient/Model.swift +++ b/Sources/Clients/BuildClient/Model.swift @@ -2,7 +2,7 @@ // Model.swift // // -// Created by ErrorErrorError on 11/27/23. +// Created by MochiTeam on 11/27/23. // // diff --git a/Sources/Clients/ClipboardClient/Client.swift b/Sources/Clients/ClipboardClient/Client.swift index a70a181..aead34d 100644 --- a/Sources/Clients/ClipboardClient/Client.swift +++ b/Sources/Clients/ClipboardClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created ErrorErrorError on 12/15/23. +// Created MochiTeam on 12/15/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/ClipboardClient/Live.swift b/Sources/Clients/ClipboardClient/Live.swift index 25c57ba..f78ff45 100644 --- a/Sources/Clients/ClipboardClient/Live.swift +++ b/Sources/Clients/ClipboardClient/Live.swift @@ -2,7 +2,7 @@ // Live.swift // // -// Created ErrorErrorError on 12/15/23. +// Created MochiTeam on 12/15/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/DatabaseClient/Client.swift b/Sources/Clients/DatabaseClient/Client.swift index 0328959..1cbe9f6 100644 --- a/Sources/Clients/DatabaseClient/Client.swift +++ b/Sources/Clients/DatabaseClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created ErrorErrorError on 4/8/23. +// Created MochiTeam on 4/8/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/DatabaseClient/Exports.swift b/Sources/Clients/DatabaseClient/Exports.swift index fa8458f..1b85d9d 100644 --- a/Sources/Clients/DatabaseClient/Exports.swift +++ b/Sources/Clients/DatabaseClient/Exports.swift @@ -2,7 +2,7 @@ // Exports.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // diff --git a/Sources/Clients/DatabaseClient/Live.swift b/Sources/Clients/DatabaseClient/Live.swift index 582b760..6688bf9 100644 --- a/Sources/Clients/DatabaseClient/Live.swift +++ b/Sources/Clients/DatabaseClient/Live.swift @@ -2,7 +2,7 @@ // Live.swift // // -// Created ErrorErrorError on 4/8/23. +// Created MochiTeam on 4/8/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/DatabaseClient/MochiSchema.swift b/Sources/Clients/DatabaseClient/MochiSchema.swift index fc9b5db..7bd3f07 100644 --- a/Sources/Clients/DatabaseClient/MochiSchema.swift +++ b/Sources/Clients/DatabaseClient/MochiSchema.swift @@ -2,7 +2,7 @@ // MochiSchema.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // diff --git a/Sources/Clients/DatabaseClient/Models/Entry.swift b/Sources/Clients/DatabaseClient/Models/Entry.swift index d0ca125..9880903 100644 --- a/Sources/Clients/DatabaseClient/Models/Entry.swift +++ b/Sources/Clients/DatabaseClient/Models/Entry.swift @@ -2,7 +2,7 @@ // Entry.swift // // -// Created by ErrorErrorError on 1/1/24. +// Created by MochiTeam on 1/1/24. // // diff --git a/Sources/Clients/DatabaseClient/Models/EntryItem.swift b/Sources/Clients/DatabaseClient/Models/EntryItem.swift index 70729c1..27ee430 100644 --- a/Sources/Clients/DatabaseClient/Models/EntryItem.swift +++ b/Sources/Clients/DatabaseClient/Models/EntryItem.swift @@ -2,7 +2,7 @@ // EntryItem.swift // // -// Created by ErrorErrorError on 1/2/24. +// Created by MochiTeam on 1/2/24. // // diff --git a/Sources/Clients/DatabaseClient/Models/Extensions/Module+.swift b/Sources/Clients/DatabaseClient/Models/Extensions/Module+.swift index 9415844..b1266be 100644 --- a/Sources/Clients/DatabaseClient/Models/Extensions/Module+.swift +++ b/Sources/Clients/DatabaseClient/Models/Extensions/Module+.swift @@ -2,7 +2,7 @@ // Module+.swift // // -// Created by ErrorErrorError on 11/12/23. +// Created by MochiTeam on 11/12/23. // // diff --git a/Sources/Clients/DatabaseClient/Models/Extensions/PlaylistHistory+.swift b/Sources/Clients/DatabaseClient/Models/Extensions/PlaylistHistory+.swift index 27ba6b4..08c2254 100644 --- a/Sources/Clients/DatabaseClient/Models/Extensions/PlaylistHistory+.swift +++ b/Sources/Clients/DatabaseClient/Models/Extensions/PlaylistHistory+.swift @@ -2,7 +2,7 @@ // PlaylistHistory+.swift // // -// Created by DeNeRr on 31.01.2024. +// Created by MochiTeam on 31.01.2024. // import Foundation diff --git a/Sources/Clients/DatabaseClient/Models/Extensions/Repo+.swift b/Sources/Clients/DatabaseClient/Models/Extensions/Repo+.swift index 43efc3e..1566e85 100644 --- a/Sources/Clients/DatabaseClient/Models/Extensions/Repo+.swift +++ b/Sources/Clients/DatabaseClient/Models/Extensions/Repo+.swift @@ -2,7 +2,7 @@ // Repo+.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // diff --git a/Sources/Clients/DatabaseClient/Models/Library.swift b/Sources/Clients/DatabaseClient/Models/Library.swift deleted file mode 100644 index 50f9b64..0000000 --- a/Sources/Clients/DatabaseClient/Models/Library.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Library.swift -// -// -// Created by ErrorErrorError on 1/1/24. -// -// - -import CoreDB -import Foundation - -@Entity -struct Collection { - var title = "" - var entries = [Entry]() -} diff --git a/Sources/Clients/DatabaseClient/Models/Module.swift b/Sources/Clients/DatabaseClient/Models/Module.swift index 2eaddc2..7844d79 100644 --- a/Sources/Clients/DatabaseClient/Models/Module.swift +++ b/Sources/Clients/DatabaseClient/Models/Module.swift @@ -2,7 +2,7 @@ // Module.swift // // -// Created by ErrorErrorError on 5/17/23. +// Created by MochiTeam on 5/17/23. // // diff --git a/Sources/Clients/DatabaseClient/Models/PlaylistHistory.swift b/Sources/Clients/DatabaseClient/Models/PlaylistHistory.swift index 4c1d75a..48cd81a 100644 --- a/Sources/Clients/DatabaseClient/Models/PlaylistHistory.swift +++ b/Sources/Clients/DatabaseClient/Models/PlaylistHistory.swift @@ -2,7 +2,7 @@ // PlaylistHistory.swift // // -// Created by DeNeRr on 27.01.2024. +// Created by MochiTeam on 27.01.2024. // import CoreDB diff --git a/Sources/Clients/DatabaseClient/Models/Repo.swift b/Sources/Clients/DatabaseClient/Models/Repo.swift index 4c3c7f2..5f414bc 100644 --- a/Sources/Clients/DatabaseClient/Models/Repo.swift +++ b/Sources/Clients/DatabaseClient/Models/Repo.swift @@ -2,7 +2,7 @@ // Repo.swift // // -// Created by ErrorErrorError on 4/10/23. +// Created by MochiTeam on 4/10/23. // // diff --git a/Sources/Clients/DeviceClient/Client.swift b/Sources/Clients/DeviceClient/Client.swift index 46e20bf..25f1b37 100644 --- a/Sources/Clients/DeviceClient/Client.swift +++ b/Sources/Clients/DeviceClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created by ErrorErrorError on 11/29/23. +// Created by MochiTeam on 11/29/23. // // diff --git a/Sources/Clients/FileClient/Client+.swift b/Sources/Clients/FileClient/Client+.swift index 67bb530..083b138 100644 --- a/Sources/Clients/FileClient/Client+.swift +++ b/Sources/Clients/FileClient/Client+.swift @@ -2,11 +2,12 @@ // Client+.swift // // -// Created by ErrorErrorError on 11/12/23. +// Created by MochiTeam on 11/12/23. // // import Foundation +import SharedModels extension FileClient { public func createModuleDirectory(_ url: URL) throws { @@ -22,10 +23,135 @@ extension FileClient { .reposDir() .appendingPathComponent(url.absoluteString) } + + private func createDirectory(_ root: String, _ directory: String) throws -> URL { + var folderPath = try self.url(.documentDirectory, .userDomainMask, nil, true) + .LibraryDir() + if (!fileExists(folderPath.path)) { + try create(folderPath) + } + folderPath = folderPath.appendingPathComponent(root) + if (!fileExists(folderPath.path)) { + try create(folderPath) + } + folderPath = folderPath.appendingPathComponent(directory) + if (!fileExists(folderPath.path)) { + try create(folderPath) + } + return folderPath + } + + public func shouldCreateLibraryDirectory(_ root: LibraryDirectory, _ directory: String, _ metadata: T) throws { + let folderPath = try createDirectory(root.rawValue, directory) + let metadataPath = folderPath.appendingPathComponent("metadata.json") + // if !fileExists(metadataPath.path) { + try JSONEncoder().encode(metadata).write(to: metadataPath) + // } + } + public func shouldCreateLibraryDirectory(_ root: LibraryDirectory, _ directory: String) throws { + let _ = try createDirectory(root.rawValue, directory) + } + + public func initializeLibrary() throws { + var folderPath = try self.url(.documentDirectory, .userDomainMask, nil, true) + .LibraryDir() + if (!fileExists(folderPath.path)) { + try create(folderPath) + } + if (!fileExists(folderPath.appendingPathComponent(LibraryDirectory.playlistCache.rawValue).path)) { + try create(folderPath.appendingPathComponent(LibraryDirectory.playlistCache.rawValue)) + } + if (!fileExists(folderPath.appendingPathComponent(LibraryDirectory.downloaded.rawValue).path)) { + try create(folderPath.appendingPathComponent(LibraryDirectory.downloaded.rawValue)) + } + } + + public func retrieveLibraryDirectory() throws -> URL { + return try self.url(.documentDirectory, .userDomainMask, nil, false) + .LibraryDir() + } + public func retrieveLibraryDirectory(root: LibraryDirectory, playlist: String? = nil, episode: String? = nil) throws -> URL { + var url = try self.url(.documentDirectory, .userDomainMask, nil, false) + .LibraryDir() + .appendingPathComponent(root.rawValue) + if let playlist = playlist { + url = url.appendingPathComponent(playlist.sanitized) + } + if let episode = episode { + url = url.appendingPathComponent(episode.sanitized) + } + return url + } + + public func removePlaylistFromLibrary(_ root: LibraryDirectory, _ playlist: String, _ episode: String? = nil) throws { + var url = try self.url(.documentDirectory, .userDomainMask, nil, false) + .LibraryDir() + .appendingPathComponent(root.rawValue) + .appendingPathComponent(playlist.sanitized) + + if let episode = episode { + url = url.appendingPathComponent(episode.sanitized) + } + + if (fileExists(url.path)) { + try remove(url) + } + } + + public func getLibraryPlaylistImage(playlist: String) -> URL? { + return try? self.url(.documentDirectory, .userDomainMask, nil, false) + .LibraryDir() + .appendingPathComponent(LibraryDirectory.playlistCache.rawValue) + .appendingPathComponent(playlist.sanitized) + .appendingPathComponent("posterImage.jpeg") + } + + public func libraryEpisodeExists(folder: String, file: String) -> Bool { + guard let url = try? self.url(.documentDirectory, .userDomainMask, nil, false) + .LibraryDir() + .appendingPathComponent(LibraryDirectory.downloaded.rawValue) + .appendingPathComponent(folder.sanitized) + .appendingPathComponent(file.sanitized) + .appendingPathComponent("data") + .appendingPathExtension("movpkg") else { + return false + } + return fileExists(url.path) + } + + public func retrieveLibraryMetadata(root: LibraryDirectory, playlist: String, episode: String? = nil) throws -> Data? { + var url = try self.url(.documentDirectory, .userDomainMask, nil, false) + .LibraryDir() + .appendingPathComponent(root.rawValue) + .appendingPathComponent(playlist.sanitized) + if let episode = episode { + url = url.appendingPathComponent(episode.sanitized) + } + return FileManager.default.contents(atPath: url.appendingPathComponent("metadata.json").relativePath) + } + public func retrieveLibraryMetadata(root: LibraryDirectory, encodedPlaylist: String, episode: String? = nil) throws -> Data? { + var url = try self.url(.documentDirectory, .userDomainMask, nil, false) + .LibraryDir() + .appendingPathComponent(root.rawValue) + .appendingPathComponent(encodedPlaylist) + if let episode = episode { + url = url.appendingPathComponent(episode.sanitized) + } + return FileManager.default.contents(atPath: url.appendingPathComponent("metadata.json").relativePath) + } } extension URL { fileprivate func reposDir() -> URL { appendingPathComponent("Repos", isDirectory: true) } + fileprivate func LibraryDir() -> URL { + appendingPathComponent("Library", isDirectory: true) + } +} + +extension String { + var sanitized: String { + replacingOccurrences(of: "/", with: "\\") + } } diff --git a/Sources/Clients/FileClient/Client.swift b/Sources/Clients/FileClient/Client.swift index 725b6e6..e187e78 100644 --- a/Sources/Clients/FileClient/Client.swift +++ b/Sources/Clients/FileClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created by ErrorErrorError on 10/6/23. +// Created by MochiTeam on 10/6/23. // // @@ -21,6 +21,7 @@ public struct FileClient { public let fileExists: @Sendable (_ path: String) -> Bool public let create: @Sendable (_ url: URL) throws -> Void public let remove: @Sendable (_ url: URL) throws -> Void + public let observeDirectory: @Sendable (_ url: URL) throws -> AsyncStream<[String]> } // MARK: TestDependencyKey @@ -30,7 +31,8 @@ extension FileClient: TestDependencyKey { url: unimplemented(".url"), fileExists: unimplemented(".remove"), create: unimplemented(".create"), - remove: unimplemented(".remove") + remove: unimplemented(".remove"), + observeDirectory: unimplemented(".observeDirectory") ) } diff --git a/Sources/Clients/FileClient/Live.swift b/Sources/Clients/FileClient/Live.swift index 4363e23..ffebd08 100644 --- a/Sources/Clients/FileClient/Live.swift +++ b/Sources/Clients/FileClient/Live.swift @@ -2,16 +2,21 @@ // Live.swift // // -// Created by ErrorErrorError on 10/6/23. +// Created by MochiTeam on 10/6/23. // // import ComposableArchitecture import Foundation +import CoreData // MARK: - FileClient + DependencyKey extension FileClient: DependencyKey { + public enum Error: Swift.Error { + case FileNotFound + } + public static var liveValue: FileClient = Self { searchPathDir, mask, url, create in try FileManager.default.url( for: searchPathDir, @@ -25,6 +30,26 @@ extension FileClient: DependencyKey { try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) } remove: { url in try FileManager.default.removeItem(at: url) + } observeDirectory: { url in + let monitoredDirectoryFileDescriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY) + if monitoredDirectoryFileDescriptor == -1 { + throw Error.FileNotFound + } + let directoryMonitorQueue = DispatchQueue(label: "directorymonitor", attributes: .concurrent) + let directoryMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredDirectoryFileDescriptor, eventMask: DispatchSource.FileSystemEvent.write, queue: directoryMonitorQueue) as? DispatchSource + return .init { continuation in + let values = try? FileManager.default.contentsOfDirectory(atPath: url.path) + continuation.yield(values ?? []) + directoryMonitorSource?.setEventHandler { + let values = try? FileManager.default.contentsOfDirectory(atPath: url.path) + continuation.yield(values ?? []) + } + directoryMonitorSource?.resume() + + continuation.onTermination = { _ in + directoryMonitorSource?.cancel() + } + } } } diff --git a/Sources/Clients/LocalizableClient/Client.swift b/Sources/Clients/LocalizableClient/Client.swift index deada98..96d24e3 100644 --- a/Sources/Clients/LocalizableClient/Client.swift +++ b/Sources/Clients/LocalizableClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created by ErrorErrorError on 12/1/23. +// Created by MochiTeam on 12/1/23. // // diff --git a/Sources/Clients/LocalizableClient/Localizable.swift b/Sources/Clients/LocalizableClient/Localizable.swift index b5cd4b0..0bd5825 100644 --- a/Sources/Clients/LocalizableClient/Localizable.swift +++ b/Sources/Clients/LocalizableClient/Localizable.swift @@ -2,7 +2,7 @@ // Localizable.swift // // -// Created by ErrorErrorError on 11/22/23. +// Created by MochiTeam on 11/22/23. // // diff --git a/Sources/Clients/LoggerClient/Client.swift b/Sources/Clients/LoggerClient/Client.swift index ccd1a23..abdd0ab 100644 --- a/Sources/Clients/LoggerClient/Client.swift +++ b/Sources/Clients/LoggerClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created ErrorErrorError on 5/30/23. +// Created MochiTeam on 5/30/23. // Copyright © 2023. All rights reserved. // @@ -13,7 +13,7 @@ import Logging import XCTestDynamicOverlay // Global App Logger -public let logger = Logger(label: "dev.errorerrorerror.mochi.app") { label in +public let logger = Logger(label: "dev.MochiTeam.mochi.app") { label in MultiplexLogHandler([ StreamLogHandler.standardOutput(label: label), ConsumableLogsHandler() diff --git a/Sources/Clients/LoggerClient/Models.swift b/Sources/Clients/LoggerClient/Models.swift index 342a5f2..5a8273d 100644 --- a/Sources/Clients/LoggerClient/Models.swift +++ b/Sources/Clients/LoggerClient/Models.swift @@ -2,7 +2,7 @@ // Models.swift // // -// Created by ErrorErrorError on 11/29/23. +// Created by MochiTeam on 11/29/23. // // diff --git a/Sources/Clients/ModuleClient/Client.swift b/Sources/Clients/ModuleClient/Client.swift index 2aadc5b..f84e81f 100644 --- a/Sources/Clients/ModuleClient/Client.swift +++ b/Sources/Clients/ModuleClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created ErrorErrorError on 4/10/23. +// Created MochiTeam on 4/10/23. // Copyright © 2023. All rights reserved. // @@ -16,7 +16,7 @@ import XCTestDynamicOverlay public struct ModuleClient: Sendable { public var initialize: @Sendable () async throws -> Void - var getModule: @Sendable (_ repoModuleId: RepoModuleID) async throws -> Self.Instance + public var getModule: @Sendable (_ repoModuleId: RepoModuleID) async throws -> Self.Instance public var removeCachedModule: @Sendable (_ repoModuleId: RepoModuleID) async throws -> Void public var removeCachedModules: @Sendable (_ repoID: Repo.ID) async throws -> Void } diff --git a/Sources/Clients/ModuleClient/Extensions/JSContext+.swift b/Sources/Clients/ModuleClient/Extensions/JSContext+.swift index 27a3684..72b60b1 100644 --- a/Sources/Clients/ModuleClient/Extensions/JSContext+.swift +++ b/Sources/Clients/ModuleClient/Extensions/JSContext+.swift @@ -2,7 +2,7 @@ // JSContext+.swift // // -// Created by ErrorErrorError on 11/17/23. +// Created by MochiTeam on 11/17/23. // // diff --git a/Sources/Clients/ModuleClient/Extensions/JSValue+.swift b/Sources/Clients/ModuleClient/Extensions/JSValue+.swift index f52abea..0c107fa 100644 --- a/Sources/Clients/ModuleClient/Extensions/JSValue+.swift +++ b/Sources/Clients/ModuleClient/Extensions/JSValue+.swift @@ -2,7 +2,7 @@ // JSValue+.swift // // -// Created by ErrorErrorError on 11/17/23. +// Created by MochiTeam on 11/17/23. // // diff --git a/Sources/Clients/ModuleClient/Instance.swift b/Sources/Clients/ModuleClient/Instance.swift index 58fe6b4..3b38f6f 100644 --- a/Sources/Clients/ModuleClient/Instance.swift +++ b/Sources/Clients/ModuleClient/Instance.swift @@ -2,7 +2,7 @@ // Instance.swift // // -// Created by ErrorErrorError on 10/28/23. +// Created by MochiTeam on 10/28/23. // // diff --git a/Sources/Clients/ModuleClient/JS+Bindings/JSContext+Console.swift b/Sources/Clients/ModuleClient/JS+Bindings/JSContext+Console.swift index 8e9b62e..ddd5ff2 100644 --- a/Sources/Clients/ModuleClient/JS+Bindings/JSContext+Console.swift +++ b/Sources/Clients/ModuleClient/JS+Bindings/JSContext+Console.swift @@ -2,7 +2,7 @@ // JSContext+Console.swift // // -// Created by ErrorErrorError on 11/17/23. +// Created by MochiTeam on 11/17/23. // // diff --git a/Sources/Clients/ModuleClient/JS+Bindings/JSContext+JSRuntime.swift b/Sources/Clients/ModuleClient/JS+Bindings/JSContext+JSRuntime.swift index 659c5ef..6a95046 100644 --- a/Sources/Clients/ModuleClient/JS+Bindings/JSContext+JSRuntime.swift +++ b/Sources/Clients/ModuleClient/JS+Bindings/JSContext+JSRuntime.swift @@ -2,7 +2,7 @@ // JSContext+JSRuntime.swift // // -// Created by ErrorErrorError on 11/4/23. +// Created by MochiTeam on 11/4/23. // // diff --git a/Sources/Clients/ModuleClient/JS+Bindings/JSContext+Request.swift b/Sources/Clients/ModuleClient/JS+Bindings/JSContext+Request.swift index 3058f97..9145e48 100644 --- a/Sources/Clients/ModuleClient/JS+Bindings/JSContext+Request.swift +++ b/Sources/Clients/ModuleClient/JS+Bindings/JSContext+Request.swift @@ -2,7 +2,7 @@ // JSContext+Request.swift // // -// Created by ErrorErrorError on 11/17/23. +// Created by MochiTeam on 11/17/23. // // diff --git a/Sources/Clients/ModuleClient/JS+Bindings/JSRuntime.swift b/Sources/Clients/ModuleClient/JS+Bindings/JSRuntime.swift index 1d32d7d..a7eee3e 100644 --- a/Sources/Clients/ModuleClient/JS+Bindings/JSRuntime.swift +++ b/Sources/Clients/ModuleClient/JS+Bindings/JSRuntime.swift @@ -2,7 +2,7 @@ // JSRuntime.swift // // -// Created by ErrorErrorError on 11/4/23. +// Created by MochiTeam on 11/4/23. // // diff --git a/Sources/Clients/ModuleClient/Live.swift b/Sources/Clients/ModuleClient/Live.swift index f24e58c..0e047ac 100644 --- a/Sources/Clients/ModuleClient/Live.swift +++ b/Sources/Clients/ModuleClient/Live.swift @@ -2,7 +2,7 @@ // Live.swift // // -// Created ErrorErrorError on 6/3/23. +// Created MochiTeam on 6/3/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/ModuleClient/Logger.swift b/Sources/Clients/ModuleClient/Logger.swift index 0722f61..413067b 100644 --- a/Sources/Clients/ModuleClient/Logger.swift +++ b/Sources/Clients/ModuleClient/Logger.swift @@ -2,7 +2,7 @@ // Logger.swift // // -// Created by ErrorErrorError on 11/29/23. +// Created by MochiTeam on 11/29/23. // // diff --git a/Sources/Clients/OfflineManagerClient/Client.swift b/Sources/Clients/OfflineManagerClient/Client.swift new file mode 100644 index 0000000..748f934 --- /dev/null +++ b/Sources/Clients/OfflineManagerClient/Client.swift @@ -0,0 +1,45 @@ +// +// Client.swift +// +// +// Created by MochiTeam on 06.04.2024. +// + +import FileClient +import Dependencies +@_exported +import Foundation +import SharedModels +import Tagged +import XCTestDynamicOverlay + +// MARK: - OfflineManagerClient + +public struct OfflineManagerClient { + public var download: @Sendable (DownloadAsset) async throws -> Void + public var cache: @Sendable (CacheAsset) async throws -> Void + public var remove: @Sendable (RemoveType, String, String?) async throws -> Void + public var togglePause: @Sendable (Int) async throws -> Void + public var cancel: @Sendable (Int) async throws -> Void + public var observeDownloading: @Sendable () -> AsyncStream<[DownloadingItem]> +} + +// MARK: TestDependencyKey + +extension OfflineManagerClient: TestDependencyKey { + public static let testValue = Self( + download: unimplemented("\(Self.self).download"), + cache: unimplemented("\(Self.self).cache"), + remove: unimplemented("\(Self.self).remove"), + togglePause: unimplemented("\(Self.self).togglePause"), + cancel: unimplemented("\(Self.self).cancel"), + observeDownloading: unimplemented("\(Self.self).observeDownloading") + ) +} + +extension DependencyValues { + public var offlineManagerClient: OfflineManagerClient { + get { self[OfflineManagerClient.self] } + set { self[OfflineManagerClient.self] = newValue } + } +} diff --git a/Sources/Clients/OfflineManagerClient/Live.swift b/Sources/Clients/OfflineManagerClient/Live.swift new file mode 100644 index 0000000..4ea46d3 --- /dev/null +++ b/Sources/Clients/OfflineManagerClient/Live.swift @@ -0,0 +1,458 @@ +// +// Live.swift +// +// +// Created by MochiTeam on 06.04.2024. +// + +import Dependencies +import Foundation +import FileClient +import AVFoundation +import UIKit +import SharedModels +import DatabaseClient +import FlyingFox +import OrderedCollections +import LoggerClient + +// MARK: - OfflineManagerClient + DependencyKey + +extension OfflineManagerClient: DependencyKey { + @Dependency(\.fileClient) private static var fileClient + private static let downloadManager = OfflineDownloadManager() + + public static let liveValue = Self( + download: { asset in + try? await downloadManager.setupAssetDownload(asset) + }, + cache: { asset in + let libraryFileUrl = try fileClient.retrieveLibraryDirectory(root: .playlistCache) + let playlist = asset.playlist + let playlistId = playlist.id.rawValue.replacingOccurrences(of: "/", with: "\\") + let imageUrl = libraryFileUrl.appendingPathComponent(playlistId).appendingPathComponent("posterImage.jpeg") + try? fileClient.shouldCreateLibraryDirectory(.playlistCache, playlistId, PlaylistCache( + playlist: playlist, + groups: asset.groups, + details: asset.details, + repoModuleId: .init(repoId: asset.repoModuleId.repoId, moduleId: asset.repoModuleId.moduleId) + )) + if let image = asset.playlist.posterImage ?? asset.playlist.bannerImage, !image.isFileURL { + let (data, _) = try await URLSession.shared.data(from: image) + + if let imageData = UIImage(data: data)?.jpegData(compressionQuality: 1) { + try imageData.write(to: imageUrl) + } + } + }, + remove: { type, playlist, episode in + switch type { + case .all: + try fileClient.removePlaylistFromLibrary(.downloaded, playlist, episode) + try fileClient.removePlaylistFromLibrary(.playlistCache, playlist, episode) + break + case .cache: + try fileClient.removePlaylistFromLibrary(.playlistCache, playlist, episode) + break + case .download: + try fileClient.removePlaylistFromLibrary(.downloaded, playlist, episode) + break + } + }, + togglePause: { taskId in + downloadManager.togglePauseDownload(taskId) + }, + cancel: { taskId in + downloadManager.cancelDownload(taskId) + }, + observeDownloading: { + .init { continuation in + let cancellable = Task.detached { + var values = downloadManager.downloadingItems.compactMap { + DownloadingItem(id: $0.metadata.link.url, percentComplete: $0.percentage, image: $0.playlist.posterImage ?? $0.playlist.bannerImage ?? URL(string: "")!, playlistName: $0.playlist.title ?? "", title: $0.episode.title ?? "Unknown Title", epNumber: $0.episode.number, taskId: $0.taskId, status: $0.status) + } + continuation.yield(values) + + let notifications = NotificationCenter.default.notifications( + named: .AssetDownloadTaskChanged + ) + for await notification in notifications { + switch notification.userInfo!["type"] as! Notification.Name { + case .AssetDownloadProgress: + let taskId = notification.userInfo?["taskId"] as! Int + let percent = notification.userInfo?["percent"] as! Double + if let idx = values.firstIndex(where: { $0.taskId == taskId }) { + values[idx].percentComplete = percent + } + break + case .AssetDownloadStateChanged: + let taskId = notification.userInfo?["taskId"] as! Int + let status = notification.userInfo?["status"] as! StatusType + if let idx = values.firstIndex(where: { $0.taskId == taskId }) { + values[idx].status = status + } + break + default: + break + } + + continuation.yield(values) + } + } + continuation.onTermination = { _ in + cancellable.cancel() + } + } + } + ) +} + +// MARK: - OfflineDownloadManager + +private class OfflineDownloadManager: NSObject { + private var config: URLSessionConfiguration! + private var downloadSession: AVAssetDownloadURLSession! + public var downloadingItems: [OfflineManagerClient.DownloadingAsset] = [] + private let server = HTTPServer(port: 64390) + + @Dependency(\.fileClient) var fileClient + + override init() { + super.init() + Task { + try await server.start() + } + config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background") + downloadSession = AVAssetDownloadURLSession(configuration: config, assetDownloadDelegate: self, delegateQueue: OperationQueue.main) + } + + public func setupAssetDownload(_ asset: OfflineManagerClient.DownloadAsset) async throws { + await initializeRoutes() + try await server.waitUntilListening() + let options = ["AVURLAssetHTTPHeaderFieldsKey": asset.headers] + let libraryFileUrl = try fileClient.retrieveLibraryDirectory(root: .playlistCache) + let playlist = asset.playlist + let avAsset = AVURLAsset(url: URL(string: "http://localhost:64390/download.m3u?url=\(asset.episodeMetadata.link.url.absoluteString.replacingOccurrences(of: "&", with: ">>"))\(!asset.episodeMetadata.subtitles.isEmpty ? "&subs=\(String(data: try JSONEncoder().encode(asset.episodeMetadata.subtitles), encoding: .utf8)!)" : "")")!, options: options) + let preferredMediaSelection = try await avAsset.load(.preferredMediaSelection) + print(asset.episodeMetadata.link.url.absoluteString) + guard let downloadTask = downloadSession.aggregateAssetDownloadTask(with: avAsset, + mediaSelections: [preferredMediaSelection], + assetTitle: asset.playlist.title ?? "Unknown Title", + assetArtworkData: nil, + options: nil) else { + throw OfflineManagerClient.Error.failedToCreateDownloadTask + } + downloadingItems.append(.init(url: asset.episodeMetadata.link.url, playlist: playlist, episode: asset.episode, metadata: asset.episodeMetadata, taskId: downloadTask.taskIdentifier, status: .downloading)) + + let playlistId = playlist.id.rawValue.replacingOccurrences(of: "/", with: "\\") + let imageUrl = libraryFileUrl.appendingPathComponent(playlistId).appendingPathComponent("posterImage.jpeg") + try? fileClient.shouldCreateLibraryDirectory(.playlistCache, playlistId, PlaylistCache( + playlist: playlist, + groups: asset.groups, + details: asset.details, + repoModuleId: .init(repoId: asset.repoModuleId.repoId, moduleId: asset.repoModuleId.moduleId) + )) + + let image = asset.playlist.posterImage ?? asset.playlist.bannerImage ?? URL(string: "")! + let (data, _) = try await URLSession.shared.data(from: image) + + if let imageData = UIImage(data: data)?.jpegData(compressionQuality: 1) { + try imageData.write(to: imageUrl) + } + + downloadTask.resume() + + NotificationCenter.default.post(name: .AssetDownloadTaskChanged, object: nil, userInfo: ["type": Notification.Name.AssetDownloadStateChanged, "taskId": downloadTask.taskIdentifier, "status": OfflineManagerClient.StatusType.downloading]) + } + + func togglePauseDownload(_ taskId: Int) { + downloadSession.getAllTasks { tasksArray in + if let task = tasksArray.first(where: { $0.taskIdentifier == taskId }), let idx = self.downloadingItems.firstIndex(where: { $0.taskId == taskId }) { + if (task.state == .suspended) { + task.resume() + self.downloadingItems[idx].status = .downloading + NotificationCenter.default.post(name: .AssetDownloadTaskChanged, object: nil, userInfo: ["type": Notification.Name.AssetDownloadStateChanged, "taskId": taskId, "status": OfflineManagerClient.StatusType.downloading]) + } else if (task.state == .running) { + task.suspend() + self.downloadingItems[idx].status = .suspended + NotificationCenter.default.post(name: .AssetDownloadTaskChanged, object: nil, userInfo: ["type": Notification.Name.AssetDownloadStateChanged, "taskId": taskId, "status": OfflineManagerClient.StatusType.suspended]) + } + } + } + } + + func cancelDownload(_ taskId: Int) { + downloadSession.getAllTasks { taskArray in + taskArray.first(where: { $0.taskIdentifier == taskId })?.cancel() + if let idx = self.downloadingItems.firstIndex(where: { $0.taskId == taskId }) { + if let location = self.downloadingItems[idx].location { + try? self.fileClient.remove(location); + } + self.downloadingItems.remove(at: idx) + } + NotificationCenter.default.post(name: .AssetDownloadTaskChanged, object: nil, userInfo: ["type": Notification.Name.AssetDownloadStateChanged, "taskId": taskId, "status": OfflineManagerClient.StatusType.cancelled]) + } + } + + func restorePendingDownloads() { + downloadSession.getAllTasks { tasksArray in + for task in tasksArray { + guard let downloadTask = task as? AVAssetDownloadTask else { break } + + let _ = downloadTask.urlAsset + downloadTask.resume() + } + } + } + + public func deleteOfflineAsset() { + do { + let userDefaults = UserDefaults.standard + if let assetPath = userDefaults.value(forKey: "assetPath") as? String { + let baseURL = URL(fileURLWithPath: NSHomeDirectory()) + let assetURL = baseURL.appendingPathComponent(assetPath) + try FileManager.default.removeItem(at: assetURL) + userDefaults.removeObject(forKey: "assetPath") + } + } catch { + print("An error occured deleting offline asset: \(error)") + } + } + +} + +extension OfflineDownloadManager { + private func initializeRoutes() async { + await server.appendRoute("GET /download.m3u", handler: { req in + let hlsSubtitleGroupID = "mochi-sub" + + func getHighestResolutionUrlFromMultivariant(_ m3u8String: String) -> String { + let lines = m3u8String.split(separator: "\n", omittingEmptySubsequences: false).map { String($0) } + var highestResIdx = 0 + var highestRes = 0 + + let regex = try! NSRegularExpression(pattern: "BANDWIDTH=(\\d+)") + + + for (i, line) in lines.enumerated() { + let range = NSRange(location: 0, length: line.utf16.count) + if let result = regex.firstMatch(in: line, range: range) { + let newRes = Int(line[Range(result.range, in: line)!].split(separator: "=")[1])! + if (newRes > highestRes) { + highestRes = newRes + highestResIdx = i + } + } + } + + return lines[highestResIdx + 1] + } + + func convertMainPlaylistToMultivariant(_ url: String, _ subtitles: [Playlist.EpisodeServer.Subtitle]) -> String { + // Build a multivariant playlist out of a single main playlist + let subtitlesMediaStrings = subtitles.enumerated() + .map(makeSubtitleTypes) + + return """ + #EXTM3U + \(subtitlesMediaStrings.joined(separator: "\n")) + #EXT-X-STREAM-INF:BANDWIDTH=640000\(!subtitles.isEmpty ? ",SUBTITLES=\"\(hlsSubtitleGroupID)\"": "") + \(url) + """ + } + + func makeSubtitleTypes(_ idx: Int, _ subtitle: Playlist.EpisodeServer.Subtitle) -> String { + "#EXT-X-MEDIA:" + ( + [ + "TYPE": "SUBTITLES", + "GROUP-ID": "\"\(hlsSubtitleGroupID)\"", + "NAME": "\"\(subtitle.name)\"", + "CHARACTERISTICS": "\"public.accessibility.transcribes-spoken-dialog\"", + "DEFAULT": subtitle.default ? "YES" : "NO", + "AUTOSELECT": subtitle.autoselect ? "YES" : "NO", + "FORCED": "NO", + "URI": "\"http://localhost:64390/subs.m3u8?url=\(subtitle.url.absoluteString)\"", + "LANGUAGE": "\"\(subtitle.name)\"" + ] as OrderedDictionary + ) + .map { "\($0.key)=\($0.value)" } + .joined(separator: ",") + } + + var m3u8: String + var urlString = req.query["url"]!.replacingOccurrences(of: ">>", with: "&") + var rq = URLRequest(url: URL(string: urlString)!) + var headers = req.headers + headers.removeValue(forKey: .host) + headers.forEach { (key, value) in + rq.addValue(value, forHTTPHeaderField: key.rawValue) + } + let (data, _) = try await URLSession.shared.data(for: rq) + guard let string = String(data: data, encoding: .utf8) else { + throw OfflineManagerClient.Error.failedToGenerateHLS + } + + if string.contains("#EXT-X-STREAM-INF") { + urlString = getHighestResolutionUrlFromMultivariant(string) + } + + var subs: [Playlist.EpisodeServer.Subtitle] = [] + if let subString = req.query["subs"] { + subs = try JSONDecoder().decode([Playlist.EpisodeServer.Subtitle].self, from: subString.data(using: .utf8)!) + } + m3u8 = convertMainPlaylistToMultivariant(urlString, subs) + + var path = req.path + path.remove(at: req.path.startIndex) + return HTTPResponse(statusCode: .ok, headers: [.contentType: "application/vnd.apple.mpegurl"], body: m3u8.data(using: .utf8)!) + }) + + await server.appendRoute("GET /subs.m3u8", handler: { req in + func setupSubM3U8(_ url: URL) async throws -> String { + var rq = URLRequest(url: URL(string: url.absoluteString)!) + var headers = req.headers + headers.removeValue(forKey: .host) + headers.forEach { (key, value) in + rq.addValue(value, forHTTPHeaderField: key.rawValue) + } + let (data, _) = try await URLSession.shared.data(for: rq) + let vttString = String(data: data , encoding: .utf8)! + + let lastTimeStampString = ( + try? NSRegularExpression(pattern: "(?:(\\d+):)?(\\d+):([\\d\\.]+)") + .matches( + in: vttString, + range: .init(location: 0, length: vttString.utf16.count) + ) + .last + .flatMap { Range($0.range, in: vttString) } + .flatMap { String(vttString[$0]) } + ) ?? "0.000" + + let duration = lastTimeStampString.components(separatedBy: ":").reversed() + .compactMap { Double($0) } + .enumerated() + .map { pow(60.0, Double($0.offset)) * $0.element } + .reduce(0, +) + + let m3u8Subtitle = """ + #EXTM3U + #EXT-X-VERSION:3 + #EXT-X-MEDIA-SEQUENCE:1 + #EXT-X-PLAYLIST-TYPE:VOD + #EXT-X-ALLOW-CACHE:NO + #EXT-X-TARGETDURATION:\(Int(duration)) + #EXTINF:\(String(format: "%.3f", duration)), no desc + \(url.absoluteString) + #EXT-X-ENDLIST + """ + + return m3u8Subtitle + } + + let idx = URL(string: req.query.first!.value)! + let m3u8 = try await setupSubM3U8(idx) + var headers = req.headers + headers.updateValue("application/vnd.apple.mpegurl", forKey: .contentType) + return HTTPResponse(statusCode: .ok, headers: headers, body: m3u8.data(using: .utf8)!) + }) + } +} + +extension OfflineDownloadManager: AVAssetDownloadDelegate { + func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, + didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], + timeRangeExpectedToLoad: CMTimeRange, for mediaSelection: AVMediaSelection) { + let percentComplete = loadedTimeRanges.reduce(0) { (rc, value) -> Double in + let loadedTimeRange: CMTimeRange = value.timeRangeValue + return rc + Double((loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds)) + } + guard let idx = downloadingItems.firstIndex(where: { + let components = NSURLComponents(url: aggregateAssetDownloadTask.urlAsset.url, resolvingAgainstBaseURL: true) + return $0.metadata.link.url.absoluteString == components?.queryItems?.first(where: { $0.name == "url" })?.value + }) else { + return + } + downloadingItems[idx].percentage = percentComplete +// debugPrint(percentComplete) + let params: [String : Any] = ["type": Notification.Name.AssetDownloadProgress, "taskId": aggregateAssetDownloadTask.taskIdentifier, "percent": percentComplete] + NotificationCenter.default.post(name: .AssetDownloadTaskChanged, object: nil, userInfo: params) + } + + func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, willDownloadTo location: URL) { + guard let idx = downloadingItems.firstIndex(where: { + let components = NSURLComponents(url: aggregateAssetDownloadTask.urlAsset.url, resolvingAgainstBaseURL: true) + return $0.metadata.link.url.absoluteString == components?.queryItems?.first(where: { $0.name == "url" })?.value + }) else { + return + } + downloadingItems[idx].location = location + } + + func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, didCompleteFor mediaSelection: AVMediaSelection) { + if let downloadedAsset = downloadingItems.first(where: { + let components = NSURLComponents(url: aggregateAssetDownloadTask.urlAsset.url, resolvingAgainstBaseURL: true) + return $0.metadata.link.url.absoluteString == components?.queryItems?.first(where: { $0.name == "url" })?.value + }) { + do { + try saveVideo(asset: downloadedAsset, location: downloadedAsset.location!) + } catch { + debugPrint(error) + } + } + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + debugPrint("Task completed: \(task), error: \(String(describing: error))") + + guard let task = task as? AVAggregateAssetDownloadTask else { return } + if let idx = downloadingItems.firstIndex(where: { + let components = NSURLComponents(url: task.urlAsset.url, resolvingAgainstBaseURL: true) + return $0.url.absoluteString == components?.queryItems?.first(where: { $0.name == "url" })?.value + }) { + guard error == nil else { + downloadingItems[idx].status = .error + NotificationCenter.default.post(name: .AssetDownloadTaskChanged, object: nil, userInfo: ["type": Notification.Name.AssetDownloadStateChanged, "taskId": downloadingItems[idx].taskId, "status": OfflineManagerClient.StatusType.error]) + logger.error("\(error)") + return + } + downloadingItems[idx].status = .finished + NotificationCenter.default.post(name: .AssetDownloadTaskChanged, object: nil, userInfo: ["type": Notification.Name.AssetDownloadStateChanged, "taskId": downloadingItems[idx].taskId, "status": OfflineManagerClient.StatusType.finished]) + } + } +} + +extension OfflineDownloadManager { + private func saveVideo(asset: OfflineManagerClient.DownloadingAsset, location: URL) throws { + let outputURL = try fileClient.retrieveLibraryDirectory(root: .downloaded, playlist: asset.playlist.id.rawValue, episode: asset.episode.id.rawValue) + debugPrint("File saved to: \(outputURL)") + if (FileManager.default.fileExists(atPath: outputURL.path)) { + try FileManager.default.removeItem(at: outputURL.appendingPathComponent("data").appendingPathExtension("movpkg")) + } + try fileClient.shouldCreateLibraryDirectory( + .downloaded, + outputURL.pathComponents.suffix(2).joined(separator: "/"), + EpisodeMetadata( + link: asset.metadata.link, + source: Playlist.EpisodeSource(id: asset.metadata.source.id, displayName: asset.metadata.source.displayName, description: asset.metadata.source.description, servers: [asset.metadata.server]), + subtitles: asset.metadata.subtitles, + server: Playlist.EpisodeServer(id: asset.metadata.server.id, displayName: asset.metadata.server.displayName, description: asset.metadata.server.description), + skipTimes: asset.metadata.skipTimes + ) + ) + try FileManager.default.moveItem(at: location, to: outputURL.appendingPathComponent("data").appendingPathExtension("movpkg")) + } +} + +extension Notification.Name { + /// Notification for when download progress has changed. + static let AssetDownloadProgress = Notification.Name(rawValue: "AssetDownloadProgressNotification") + + /// Notification for when the download state of an Asset has changed. + static let AssetDownloadStateChanged = Notification.Name(rawValue: "AssetDownloadStateChangedNotification") + + static let AssetDownloadTaskChanged = Notification.Name(rawValue: "AssetDownloadTaskChanged") + + /// Notification for when AssetPersistenceManager has completely restored its state. + static let AssetPersistenceManagerDidRestoreState = Notification.Name(rawValue: "AssetPersistenceManagerDidRestoreStateNotification") +} diff --git a/Sources/Clients/OfflineManagerClient/Models.swift b/Sources/Clients/OfflineManagerClient/Models.swift new file mode 100644 index 0000000..50da23d --- /dev/null +++ b/Sources/Clients/OfflineManagerClient/Models.swift @@ -0,0 +1,109 @@ +// +// Models.swift +// +// +// Created by MochiTeam on 06.04.2024. +// + +import Foundation +import SharedModels +import Tagged + +extension OfflineManagerClient { + public enum Error: Swift.Error, Equatable, Sendable { + case failedToGetPlaylistId + case failedToCreateDownloadTask + case failedToGenerateHLS + } + + public enum RemoveType { + case cache + case download + case all + } + + public struct DownloadAsset: Equatable, Sendable { + public let episodeMetadata: EpisodeMetadata + public let episode: Playlist.Item + public let headers: [String: String] + public let groups: [Playlist.Group]? + public let playlist: Playlist + public let details: Playlist.Details? + public let repoModuleId: RepoModuleID + + public init(episodeMetadata: EpisodeMetadata, headers: [String: String], episode: Playlist.Item, groups: [Playlist.Group]?, playlist: Playlist, details: Playlist.Details?, repoModuleId: RepoModuleID) { + self.episodeMetadata = episodeMetadata + self.headers = headers + self.episode = episode + self.groups = groups + self.playlist = playlist + self.details = details + self.repoModuleId = repoModuleId + } + } + + public struct CacheAsset: Equatable, Sendable { + public let groups: [Playlist.Group]? + public let playlist: Playlist + public let details: Playlist.Details? + public let repoModuleId: RepoModuleID + + public init(groups: [Playlist.Group]?, playlist: Playlist, details: Playlist.Details?, repoModuleId: RepoModuleID) { + self.playlist = playlist + self.details = details + self.groups = groups + self.repoModuleId = repoModuleId + } + } + + public struct DownloadingItem: Identifiable, Sendable, Equatable, Hashable { + public let id: URL + public var percentComplete: Double + public let image: URL + public let playlistName: String + public let title: String + public let epNumber: Double + public let taskId: Int + public var status: StatusType + + public init(id: URL, percentComplete: Double, image: URL, playlistName: String, title: String, epNumber: Double, taskId: Int, status: StatusType) { + self.id = id + self.percentComplete = percentComplete + self.image = image + self.playlistName = playlistName + self.title = title + self.epNumber = epNumber + self.taskId = taskId + self.status = status + } + } + + public enum StatusType: Sendable { + case downloading + case suspended + case finished + case cancelled + case error + } + + public struct DownloadingAsset: Hashable { + public let url: URL + public let playlist: Playlist + public let episode: Playlist.Item + public let metadata: EpisodeMetadata + public var location: URL? + public var percentage: Double = 0 + public var taskId: Int + public var status: StatusType + + public init(url: URL, playlist: Playlist, episode: Playlist.Item, metadata: EpisodeMetadata, location: URL? = nil, taskId: Int, status: StatusType) { + self.url = url + self.playlist = playlist + self.episode = episode + self.metadata = metadata + self.location = location + self.taskId = taskId + self.status = status + } + } +} diff --git a/Sources/Clients/PlayerClient/Client.swift b/Sources/Clients/PlayerClient/Client.swift index b84e191..45e4a0e 100644 --- a/Sources/Clients/PlayerClient/Client.swift +++ b/Sources/Clients/PlayerClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created ErrorErrorError on 5/26/23. +// Created MochiTeam on 5/26/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/PlayerClient/Extension/AVMediaSelectionGroup+Struct.swift b/Sources/Clients/PlayerClient/Extension/AVMediaSelectionGroup+Struct.swift index 53f94c2..94e13db 100644 --- a/Sources/Clients/PlayerClient/Extension/AVMediaSelectionGroup+Struct.swift +++ b/Sources/Clients/PlayerClient/Extension/AVMediaSelectionGroup+Struct.swift @@ -2,7 +2,7 @@ // AVMediaSelectionGroup+Struct.swift // // -// Created by ErrorErrorError on 7/16/23. +// Created by MochiTeam on 7/16/23. // // diff --git a/Sources/Clients/PlayerClient/Extension/AVPlayer+.swift b/Sources/Clients/PlayerClient/Extension/AVPlayer+.swift index 43fa308..5e0770f 100644 --- a/Sources/Clients/PlayerClient/Extension/AVPlayer+.swift +++ b/Sources/Clients/PlayerClient/Extension/AVPlayer+.swift @@ -2,7 +2,7 @@ // AVPlayer+.swift // // -// Created by ErrorErrorError on 6/10/23. +// Created by MochiTeam on 6/10/23. // // diff --git a/Sources/Clients/PlayerClient/Internal/PlayerItem+DASH.swift b/Sources/Clients/PlayerClient/Internal/PlayerItem+DASH.swift index cdbfb35..d8d3ee9 100644 --- a/Sources/Clients/PlayerClient/Internal/PlayerItem+DASH.swift +++ b/Sources/Clients/PlayerClient/Internal/PlayerItem+DASH.swift @@ -2,7 +2,7 @@ // PlayerItem+DASH.swift // // -// Created by ErrorErrorError on 12/27/23. +// Created by MochiTeam on 12/27/23. // // diff --git a/Sources/Clients/PlayerClient/Internal/PlayerItem+HLS.swift b/Sources/Clients/PlayerClient/Internal/PlayerItem+HLS.swift index d6382c1..f17a857 100644 --- a/Sources/Clients/PlayerClient/Internal/PlayerItem+HLS.swift +++ b/Sources/Clients/PlayerClient/Internal/PlayerItem+HLS.swift @@ -2,7 +2,7 @@ // PlayerItem+HLS.swift // // -// Created by ErrorErrorError on 6/18/23. +// Created by MochiTeam on 6/18/23. // // // Source: https://github.com/jbweimar/external-webvtt-example/blob/master/External%20WebVTT%20Example/CustomResourceLoaderDelegate.swift diff --git a/Sources/Clients/PlayerClient/Internal/PlayerItem.swift b/Sources/Clients/PlayerClient/Internal/PlayerItem.swift index 686fc39..16927a6 100644 --- a/Sources/Clients/PlayerClient/Internal/PlayerItem.swift +++ b/Sources/Clients/PlayerClient/Internal/PlayerItem.swift @@ -2,7 +2,7 @@ // PlayerItem.swift // // -// Created by ErrorErrorError on 6/18/23. +// Created by MochiTeam on 6/18/23. // // @@ -40,7 +40,7 @@ final class PlayerItem: AVPlayerItem { self.resourceQueue = DispatchQueue(label: "playeritem-\(payload.link.absoluteString)", qos: .utility) let headers = payload.headers - if payload.subtitles.isEmpty { + if payload.subtitles.isEmpty || payload.link.isFileURL { self.payload = .init(modifiedLink: payload.link, payload: payload) } else { self.payload = .init(modifiedLink: payload.link.change(scheme: Self.hlsCommonScheme), payload: payload) diff --git a/Sources/Clients/PlayerClient/Live.swift b/Sources/Clients/PlayerClient/Live.swift index 557231d..a19d25d 100644 --- a/Sources/Clients/PlayerClient/Live.swift +++ b/Sources/Clients/PlayerClient/Live.swift @@ -2,7 +2,7 @@ // Live.swift // // -// Created ErrorErrorError on 5/26/23. +// Created MochiTeam on 5/26/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/PlayerClient/Models.swift b/Sources/Clients/PlayerClient/Models.swift index ba4faba..804420f 100644 --- a/Sources/Clients/PlayerClient/Models.swift +++ b/Sources/Clients/PlayerClient/Models.swift @@ -2,7 +2,7 @@ // Models.swift // // -// Created by ErrorErrorError on 5/26/23. +// Created by MochiTeam on 5/26/23. // // diff --git a/Sources/Clients/PlayerClient/Views/PlayerRoutePickerView.swift b/Sources/Clients/PlayerClient/Views/PlayerRoutePickerView.swift index 326c573..8b7f5e1 100644 --- a/Sources/Clients/PlayerClient/Views/PlayerRoutePickerView.swift +++ b/Sources/Clients/PlayerClient/Views/PlayerRoutePickerView.swift @@ -2,7 +2,7 @@ // PlayerRoutePickerView.swift // // -// Created by ErrorErrorError on 6/17/23. +// Created by MochiTeam on 6/17/23. // // diff --git a/Sources/Clients/PlayerClient/Views/PlayerView.swift b/Sources/Clients/PlayerClient/Views/PlayerView.swift index fc4942f..03c3de3 100644 --- a/Sources/Clients/PlayerClient/Views/PlayerView.swift +++ b/Sources/Clients/PlayerClient/Views/PlayerView.swift @@ -2,7 +2,7 @@ // PlayerView.swift // // -// Created by ErrorErrorError on 5/31/23. +// Created by MochiTeam on 5/31/23. // // diff --git a/Sources/Clients/PlaylistHistoryClient/Client.swift b/Sources/Clients/PlaylistHistoryClient/Client.swift index 8c077cf..8899632 100644 --- a/Sources/Clients/PlaylistHistoryClient/Client.swift +++ b/Sources/Clients/PlaylistHistoryClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created by DeNeRr on 28.01.2024. +// Created by MochiTeam on 28.01.2024. // import DatabaseClient @@ -19,6 +19,8 @@ public struct PlaylistHistoryClient: Sendable { public var updateEpId: @Sendable (EpIdPayload) async throws -> Void public var fetch: @Sendable (RMP) async throws -> PlaylistHistory public var fetchForModule: @Sendable (String, String) async throws -> [PlaylistHistory] + public var observeAll: @Sendable () -> AsyncStream<[PlaylistHistory]> + public var observeRepoModule: @Sendable (String, String) -> AsyncStream<[PlaylistHistory]> public var updateTimestamp: @Sendable (RMP, Double) async throws -> Void public var updateDateWatched: @Sendable (RMP) async throws -> Void public var observe: @Sendable (RMP) -> AsyncStream<[PlaylistHistory]> @@ -33,6 +35,8 @@ extension PlaylistHistoryClient: TestDependencyKey { updateEpId: unimplemented("\(Self.self).updateEpId"), fetch: unimplemented("\(Self.self).fetch"), fetchForModule: unimplemented("\(Self.self).fetchForModule"), + observeAll: unimplemented("\(Self.self).observeAll"), + observeRepoModule: unimplemented("\(Self.self).fetchForModule"), updateTimestamp: unimplemented("\(Self.self).updateTimestamp"), updateDateWatched: unimplemented("\(Self.self).updateDateWatched"), observe: unimplemented("\(Self.self).observe"), diff --git a/Sources/Clients/PlaylistHistoryClient/Live.swift b/Sources/Clients/PlaylistHistoryClient/Live.swift index 692c85a..4545852 100644 --- a/Sources/Clients/PlaylistHistoryClient/Live.swift +++ b/Sources/Clients/PlaylistHistoryClient/Live.swift @@ -2,7 +2,7 @@ // Live.swift // // -// Created by DeNeRr on 28.01.2024. +// Created by MochiTeam on 28.01.2024. // import DatabaseClient @@ -19,7 +19,7 @@ extension PlaylistHistoryClient: DependencyKey { updateEpId: { payload in if var playlist = try? await databaseClient .fetch(.all.where(\PlaylistHistory.repoId == payload.rmp.repoId).where(\PlaylistHistory.moduleId == payload.rmp.moduleId).where(\PlaylistHistory.playlistID == payload.rmp.playlistId)).first { - playlist.epId = payload.episode.id.rawValue + playlist.epId = payload.episode.id playlist.dateWatched = Date.now playlist.epName = payload.episode.title playlist.groupId = payload.groupId @@ -30,7 +30,7 @@ extension PlaylistHistoryClient: DependencyKey { } else { _ = try await databaseClient.insert(PlaylistHistory( playlistID: payload.rmp.playlistId, - epId: payload.episode.id.rawValue, + epId: payload.episode.id, playlistName: payload.playlistName, moduleId: payload.rmp.moduleId, repoId: payload.rmp.repoId, @@ -54,6 +54,12 @@ extension PlaylistHistoryClient: DependencyKey { return history?.sorted(by: { $0.dateWatched > $1.dateWatched }) ?? [] }, + observeAll: { + databaseClient.observe(.all.where(\PlaylistHistory.moduleId != nil)) + }, + observeRepoModule: { repoId, moduleId in + databaseClient.observe(.all.where(\PlaylistHistory.repoId == repoId).where(\PlaylistHistory.moduleId == moduleId)) + }, updateTimestamp: { rmp, timestamp in if var playlist = try? await databaseClient .fetch(.all.where(\PlaylistHistory.repoId == rmp.repoId).where(\PlaylistHistory.moduleId == rmp.moduleId).where(\PlaylistHistory.playlistID == rmp.playlistId)).first { diff --git a/Sources/Clients/PlaylistHistoryClient/Models.swift b/Sources/Clients/PlaylistHistoryClient/Models.swift index 4ba7e36..fd7e885 100644 --- a/Sources/Clients/PlaylistHistoryClient/Models.swift +++ b/Sources/Clients/PlaylistHistoryClient/Models.swift @@ -2,11 +2,10 @@ // Models.swift // // -// Created by DeNeRr on 29.01.2024. +// Created by MochiTeam on 29.01.2024. // import Foundation -import SharedModels extension PlaylistHistoryClient { public enum Error: Swift.Error, Equatable, Sendable { @@ -24,16 +23,28 @@ extension PlaylistHistoryClient { self.playlistId = playlistId } } + + public struct Episode: Equatable, Sendable { + let id: String + let title: String + let thumbnail: URL? + + public init(id: String, title: String, thumbnail: URL?) { + self.id = id + self.title = title + self.thumbnail = thumbnail + } + } public struct EpIdPayload: Equatable, Sendable { public let rmp: RMP - public let episode: Playlist.Item + public let episode: Episode public let playlistName: String? public let pageId: String public let groupId: String public let variantId: String - public init(rmp: RMP, episode: Playlist.Item, playlistName: String?, pageId: String, groupId: String, variantId: String) { + public init(rmp: RMP, episode: Episode, playlistName: String?, pageId: String, groupId: String, variantId: String) { self.rmp = rmp self.episode = episode self.playlistName = playlistName diff --git a/Sources/Clients/RepoClient/Client.swift b/Sources/Clients/RepoClient/Client.swift index 648a910..7dd2a7a 100644 --- a/Sources/Clients/RepoClient/Client.swift +++ b/Sources/Clients/RepoClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created ErrorErrorError on 4/8/23. +// Created MochiTeam on 4/8/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/RepoClient/Live.swift b/Sources/Clients/RepoClient/Live.swift index c632c4a..070cfba 100644 --- a/Sources/Clients/RepoClient/Live.swift +++ b/Sources/Clients/RepoClient/Live.swift @@ -2,7 +2,7 @@ // Live.swift // // -// Created ErrorErrorError on 4/8/23. +// Created MochiTeam on 4/8/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/RepoClient/Models.swift b/Sources/Clients/RepoClient/Models.swift index 6df2cf9..ce1d5dd 100644 --- a/Sources/Clients/RepoClient/Models.swift +++ b/Sources/Clients/RepoClient/Models.swift @@ -2,7 +2,7 @@ // Models.swift // // -// Created by ErrorErrorError on 4/8/23. +// Created by MochiTeam on 4/8/23. // // diff --git a/Sources/Clients/UserDefaultsClient/Client.swift b/Sources/Clients/UserDefaultsClient/Client.swift index 54c34e5..0c69539 100644 --- a/Sources/Clients/UserDefaultsClient/Client.swift +++ b/Sources/Clients/UserDefaultsClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created ErrorErrorError on 4/6/23. +// Created MochiTeam on 4/6/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/UserDefaultsClient/Live.swift b/Sources/Clients/UserDefaultsClient/Live.swift index 6c9523c..cc20dad 100644 --- a/Sources/Clients/UserDefaultsClient/Live.swift +++ b/Sources/Clients/UserDefaultsClient/Live.swift @@ -2,7 +2,7 @@ // Live.swift // // -// Created ErrorErrorError on 4/6/23. +// Created MochiTeam on 4/6/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/UserSettingsClient/AppIcon.swift b/Sources/Clients/UserSettingsClient/AppIcon.swift index c5300ac..46440e6 100644 --- a/Sources/Clients/UserSettingsClient/AppIcon.swift +++ b/Sources/Clients/UserSettingsClient/AppIcon.swift @@ -2,7 +2,7 @@ // AppIcon.swift // // -// Created by ErrorErrorError on 10/11/23. +// Created by MochiTeam on 10/11/23. // // diff --git a/Sources/Clients/UserSettingsClient/Client.swift b/Sources/Clients/UserSettingsClient/Client.swift index 51c4a98..fb144a6 100644 --- a/Sources/Clients/UserSettingsClient/Client.swift +++ b/Sources/Clients/UserSettingsClient/Client.swift @@ -2,7 +2,7 @@ // Client.swift // // -// Created ErrorErrorError on 4/8/23. +// Created MochiTeam on 4/8/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Clients/UserSettingsClient/Live.swift b/Sources/Clients/UserSettingsClient/Live.swift index e31a9a5..1de64ea 100644 --- a/Sources/Clients/UserSettingsClient/Live.swift +++ b/Sources/Clients/UserSettingsClient/Live.swift @@ -2,7 +2,7 @@ // Live.swift // // -// Created ErrorErrorError on 4/8/23. +// Created MochiTeam on 4/8/23. // Copyright © 2023. All rights reserved. // @@ -12,7 +12,11 @@ import Foundation extension UserSettingsClient: DependencyKey { public static let liveValue: Self = { - let userSettings = LockIsolated(UserSettings()) + let userSettings = LockIsolated(UserSettings( + developerModeEnabled: UserDefaults.standard.bool(forKey: "userSettings.developerModeEnabled"), + fastForwardAmount: UserDefaults.standard.value(forKey: "userSettings.fastForwardAmount") as? Double, + fastBackwardAmount: UserDefaults.standard.value(forKey: "userSettings.fastBackwardAmount") as? Double + )) let subject = PassthroughSubject() return Self { @@ -22,6 +26,9 @@ extension UserSettingsClient: DependencyKey { state = newValue subject.send(newValue) print("Save settings") + UserDefaults.standard.setValue(newValue.fastForwardAmount, forKey: "userSettings.fastForwardAmount") + UserDefaults.standard.setValue(newValue.fastBackwardAmount, forKey: "userSettings.fastBackwardAmount") + UserDefaults.standard.setValue(newValue.developerModeEnabled, forKey: "userSettings.developerModeEnabled") } } save: { // TODO: Save UserSettingsClient diff --git a/Sources/Clients/UserSettingsClient/Theme.swift b/Sources/Clients/UserSettingsClient/Theme.swift index 2126f56..98bd9d0 100644 --- a/Sources/Clients/UserSettingsClient/Theme.swift +++ b/Sources/Clients/UserSettingsClient/Theme.swift @@ -2,7 +2,7 @@ // Theme.swift // // -// Created by ErrorErrorError on 10/11/23. +// Created by MochiTeam on 10/11/23. // // @@ -104,6 +104,7 @@ public enum Theme: Codable, Sendable, Hashable, Identifiable, CaseIterable { extension Theme { public static let pastelGreen = Color(hue: 138 / 360, saturation: 0.33, brightness: 0.63) + public static let pastelRed = Color(hue: 2 / 360, saturation: 0.42, brightness: 0.96) public static let pastelBlue = Color(hue: 178 / 360, saturation: 0.39, brightness: 0.7) public static let pastelOrange = Color(hue: 27 / 360, saturation: 0.41, brightness: 0.69) } diff --git a/Sources/Clients/UserSettingsClient/UserSettings.swift b/Sources/Clients/UserSettingsClient/UserSettings.swift index 339bdc1..f9ea531 100644 --- a/Sources/Clients/UserSettingsClient/UserSettings.swift +++ b/Sources/Clients/UserSettingsClient/UserSettings.swift @@ -2,22 +2,28 @@ // UserSettings.swift // // -// Created by ErrorErrorError on 5/19/23. +// Created by MochiTeam on 5/19/23. // // public struct UserSettings: Sendable, Equatable, Codable { public var theme: Theme + public var fastForwardAmount: Double + public var fastBackwardAmount: Double public var appIcon: AppIcon public var developerModeEnabled: Bool public init( - theme: Theme = .automatic, + theme: Theme? = .automatic, appIcon: AppIcon = .default, - developerModeEnabled: Bool = false + developerModeEnabled: Bool = false, + fastForwardAmount: Double? = 15, + fastBackwardAmount: Double? = 5 ) { - self.theme = theme + self.theme = theme ?? .automatic self.appIcon = appIcon self.developerModeEnabled = developerModeEnabled + self.fastForwardAmount = fastForwardAmount ?? 15 + self.fastBackwardAmount = fastBackwardAmount ?? 5 } } diff --git a/Sources/Features/App/AppDelegateFeature.swift b/Sources/Features/App/AppDelegateFeature.swift index 6cca364..7c9c1cc 100644 --- a/Sources/Features/App/AppDelegateFeature.swift +++ b/Sources/Features/App/AppDelegateFeature.swift @@ -2,7 +2,7 @@ // AppDelegateFeature.swift // // -// Created by ErrorErrorError on 5/19/23. +// Created by MochiTeam on 5/19/23. // // diff --git a/Sources/Features/App/AppFeature+Reducer.swift b/Sources/Features/App/AppFeature+Reducer.swift index 8a15219..023faa4 100644 --- a/Sources/Features/App/AppFeature+Reducer.swift +++ b/Sources/Features/App/AppFeature+Reducer.swift @@ -2,7 +2,7 @@ // AppFeature+Reducer.swift // // -// Created by ErrorErrorError on 4/6/23. +// Created by MochiTeam on 4/6/23. // // @@ -14,6 +14,7 @@ import Discover import Foundation import ModuleLists import Repos +import Library import Settings import VideoPlayer @@ -40,6 +41,8 @@ extension AppFeature: Reducer { case let .view(.didSelectTab(tab)): if state.selected == tab { switch tab { + case .library: + break case .discover: state.discover.path.removeAll() case .repos: @@ -54,6 +57,31 @@ extension AppFeature: Reducer { case .internal(.appDelegate): break + case let .internal(.library(.delegate(.playbackVideoItem(_, repoModuleId, playlist, group, variant, paging, itemId)))): + let effect = state.videoPlayer?.clearForNewPlaylistIfNeeded( + repoModuleId: repoModuleId, + playlist: playlist, + groupId: group, + variantId: variant, + pageId: paging, + episodeId: itemId + ) + .map { Action.internal(.videoPlayer(.presented($0))) } + + if let effect { + return effect + } else { + state.videoPlayer = .init( + repoModuleId: repoModuleId, + playlist: playlist, + group: group, + variant: variant, + page: paging, + episodeId: itemId, + prefersOffline: true + ) + } + case let .internal(.discover(.delegate(.playbackVideoItem(_, repoModuleId, playlist, group, variant, paging, itemId)))): let effect = state.videoPlayer?.clearForNewPlaylistIfNeeded( repoModuleId: repoModuleId, @@ -81,6 +109,9 @@ extension AppFeature: Reducer { case .internal(.discover): break + case .internal(.library): + break + case .internal(.repos): break @@ -106,6 +137,10 @@ extension AppFeature: Reducer { DiscoverFeature() } + Scope(state: \.library, action: \.internal.library) { + LibraryFeature() + } + Scope(state: \.repos, action: \.internal.repos) { ReposFeature() } diff --git a/Sources/Features/App/AppFeature.swift b/Sources/Features/App/AppFeature.swift index b459a9b..764f586 100644 --- a/Sources/Features/App/AppFeature.swift +++ b/Sources/Features/App/AppFeature.swift @@ -2,13 +2,14 @@ // AppFeature.swift // // -// Created by ErrorErrorError on 4/6/23. +// Created by MochiTeam on 4/6/23. // // import Architecture import DatabaseClient import Discover +import Library import Foundation import ModuleLists import Repos @@ -23,6 +24,7 @@ public struct AppFeature: Feature { public struct State: FeatureState { public var appDelegate = AppDelegateFeature.State() public var discover = DiscoverFeature.State() + public var library = LibraryFeature.State() public var repos = ReposFeature.State() public var settings = SettingsFeature.State() @@ -34,16 +36,19 @@ public struct AppFeature: Feature { discover: DiscoverFeature.State = .init(), repos: ReposFeature.State = .init(), settings: SettingsFeature.State = .init(), - selected: AppFeature.State.Tab = Tab.discover + selected: AppFeature.State.Tab = Tab.discover, + library: LibraryFeature.State = .init() ) { self.discover = discover self.repos = repos self.settings = settings self.selected = selected + self.library = library } public enum Tab: String, CaseIterable, Sendable, Localizable, Hashable { case discover = "Discover" + case library = "Library" case repos = "Repos" case settings = "Settings" @@ -51,6 +56,8 @@ public struct AppFeature: Feature { switch self { case .discover: "doc.text.image" + case .library: + "rectangle.stack" case .repos: "globe" case .settings: @@ -58,21 +65,12 @@ public struct AppFeature: Feature { } } - var selected: String { - switch self { - case .discover: - "doc.text.image.fill" - case .repos: - image - case .settings: - "gearshape.fill" - } - } - var colorAccent: Color { switch self { case .discover: Theme.pastelGreen + case .library: + Theme.pastelRed case .repos: Theme.pastelBlue case .settings: @@ -98,6 +96,7 @@ public struct AppFeature: Feature { public enum InternalAction: SendableAction { case appDelegate(AppDelegateFeature.Action) case discover(DiscoverFeature.Action) + case library(LibraryFeature.Action) case repos(ReposFeature.Action) case settings(SettingsFeature.Action) case videoPlayer(PresentationAction) diff --git a/Sources/Features/App/Exported.swift b/Sources/Features/App/Exported.swift index f80894e..f2e1085 100644 --- a/Sources/Features/App/Exported.swift +++ b/Sources/Features/App/Exported.swift @@ -2,7 +2,7 @@ // Exported.swift // // -// Created by ErrorErrorError on 11/23/23. +// Created by MochiTeam on 11/23/23. // // diff --git a/Sources/Features/App/iOS/AppFeatureView+iOS.swift b/Sources/Features/App/iOS/AppFeatureView+iOS.swift index 5e927e5..33e5d31 100644 --- a/Sources/Features/App/iOS/AppFeatureView+iOS.swift +++ b/Sources/Features/App/iOS/AppFeatureView+iOS.swift @@ -2,7 +2,7 @@ // AppFeatureView+iOS.swift // // -// Created by ErrorErrorError on 11/23/23. +// Created by MochiTeam on 11/23/23. // // @@ -13,6 +13,7 @@ import Foundation import FoundationHelpers import ModuleLists import Repos +import Library import Settings import Styling import SwiftUI @@ -48,6 +49,14 @@ extension AppFeature.View: View { ) ) .accentColor(nil) + case .library: + LibraryFeature.View( + store: store.scope( + state: \.library, + action: \.internal.library + ) + ) + .accentColor(nil) case .settings: SettingsFeature.View( store: store.scope( @@ -59,7 +68,7 @@ extension AppFeature.View: View { } } .tabItem { - Label(tab.localized, systemImage: viewStore.state == tab ? tab.selected : tab.image) + Label(tab.localized, systemImage: tab.image) } .tag(tab) } diff --git a/Sources/Features/App/macOS/AppFeatureView+macOS.swift b/Sources/Features/App/macOS/AppFeatureView+macOS.swift index 7fd0882..05acdcf 100644 --- a/Sources/Features/App/macOS/AppFeatureView+macOS.swift +++ b/Sources/Features/App/macOS/AppFeatureView+macOS.swift @@ -2,7 +2,7 @@ // AppFeatureView+macOS.swift // // -// Created by ErrorErrorError on 11/23/23. +// Created by MochiTeam on 11/23/23. // // @@ -38,6 +38,14 @@ extension AppFeature.View: View { action: \.internal.discover ) ) + case .library: + LibraryFeature.View( + store: store.scope( + state: \.library, + action: \.internal.library + ) + ) + .accentColor(nil) case .repos: ReposFeature.View( store: store.scope( diff --git a/Sources/Features/ContentCore/ContentCore+View.swift b/Sources/Features/ContentCore/ContentCore+View.swift index 3eaff03..42c3903 100644 --- a/Sources/Features/ContentCore/ContentCore+View.swift +++ b/Sources/Features/ContentCore/ContentCore+View.swift @@ -2,7 +2,7 @@ // ContentCore+View.swift // // -// Created by ErrorErrorError on 7/13/23. +// Created by MochiTeam on 7/13/23. // // @@ -22,7 +22,7 @@ extension ContentCore { @ObservedObject private var viewStore: ViewStoreOf private let contentType: Playlist.PlaylistType - + @MainActor public init( store: StoreOf, @@ -204,23 +204,78 @@ extension ContentCore { ScrollView(.horizontal, showsIndicators: false) { HStack(alignment: .top, spacing: 12) { ForEach(items.value ?? Self.placeholderItems, id: \.number) { item in + let isDownloaded = viewStore.downloadedEpisodes.contains(item.id.rawValue.replacingOccurrences(of: "/", with: "\\")) VStack(alignment: .leading, spacing: 0) { - FillAspectImage(url: item.thumbnail ?? viewStore.playlist.posterImage) - .aspectRatio(16 / 9, contentMode: .fit) - .cornerRadius(12) + ZStack { + FillAspectImage(url: item.thumbnail ?? viewStore.playlist.posterImage) + .aspectRatio(16 / 9, contentMode: .fit) + .cornerRadius(12) + + VStack { + HStack { + Spacer() + if isDownloaded == true { + Image(systemName: "checkmark") + .font(.system(size: 20, weight: .black)) + .foregroundColor(.white) + .padding(7) + .background(.green) + .clipShape(Circle()) + .padding(.top, 7.5) + .padding(.trailing, 7.5) + + } + else if isDownloaded == false{ + Button(action: { + store.send(.didTapDownloadPlaylist(item)) + + }) { + Image(systemName: "arrow.down") + .font(.system(size: 20, weight: .black)) + .foregroundColor(.black) + .padding(7) + .background(.white) + .clipShape(Circle()) + } + .padding(.top, 7.5) + .padding(.trailing, 7.5) + + } + } + Spacer() + } + } + .contextMenu { + if isDownloaded { + Button(role: .destructive) { + store.send(.didTapRemoveDownloadedPlaylist(item)) + } label: { + Label("Remove episode", systemImage: "trash") + } + .buttonStyle(.plain) + } + } Spacer() .frame(height: 8) - - Text(String(format: contentType.itemTypeWithNumber, item.number.withoutTrailingZeroes)) - .font(.footnote.weight(.semibold)) - .foregroundColor(.init(white: 0.4)) - + + HStack { + Text(String(format: contentType.itemTypeWithNumber, item.number.withoutTrailingZeroes)) + .font(.footnote.weight(.semibold)) + .foregroundColor(.init(white: 0.4)) + if isDownloaded { + Image(systemName: "cloud.fill") + .foregroundColor(.init(white: 0.4)) + } + } + Spacer() .frame(height: 4) - + Text(item.title ?? String(format: contentType.itemTypeWithNumber, item.number.withoutTrailingZeroes)) .font(.body.weight(.semibold)) + .foregroundStyle(Color.primary) + .multilineTextAlignment(.leading) } .frame(width: 228) .contentShape(Rectangle()) @@ -269,6 +324,110 @@ extension ContentCore { .animation(.easeInOut, value: _selectedGroupId) .animation(.easeInOut, value: _selectedVariantId) .animation(.easeInOut, value: _selectedPagingId) + .sheet( + store: store.scope( + state: \.$downloadSelection, + action: \.downloadSelection + ), + state: /DownloadSelection.State.selection, + action: DownloadSelection.Action.selection + ) { store in + VStack { + Capsule() + .frame(width: 48, height: 4) + .foregroundColor(.gray.opacity(0.26)) + .padding(.top, 8) + + WithViewStore(store, observe: \.`self`) { viewStore in + List { + LoadableView(loadable: viewStore.state.sources) { sources in + Section("Sources") { + ForEach(sources) { source in + Button { + store.send(.selectSource(source)) + } label: { + HStack(alignment: .center) { + Text(source.displayName) + Spacer() + if (viewStore.state.selectedSource?.id == source.id) { Image(systemName: "checkmark").foregroundColor(.blue) } + } + } + .foregroundColor(Color.primary) + } + } + + if let selectedSource = viewStore.state.selectedSource { + Section("Servers") { + ForEach(sources.first(where: { $0.id.rawValue == selectedSource.id.rawValue })?.servers ?? []) { server in + Button { + store.send(.selectServer(server)) + } label: { + HStack(alignment: .center) { + Text(server.displayName) + Spacer() + if (viewStore.state.selectedServer?.id == server.id) { + if case .loading = viewStore.state.serverResponse { + ProgressView().progressViewStyle(CircularProgressViewStyle(tint: Color.blue)) + } else { + Image(systemName: "checkmark").foregroundColor(.blue) + } + } + } + } + .foregroundColor(Color.primary) + } + } + } + } + + LoadableView(loadable: viewStore.serverResponse) { serverResponse in + Section("Quality") { + ForEach(serverResponse.links) { link in + Button { + store.send(.selectQuality(link)) + } label: { + HStack(alignment: .center) { + Text(link.quality.description) + Spacer() + if (viewStore.state.selectedQuality?.id == link.id) { Image(systemName: "checkmark").foregroundColor(.blue) } + } + } + .foregroundColor(Color.primary) + } + } + Section("Subtitles") { + ForEach(serverResponse.subtitles) { subtitle in + Button { + store.send(.selectSubtitle(subtitle)) + } label: { + HStack(alignment: .center) { + Text(subtitle.name) + Spacer() + if (viewStore.state.selectedSubtitle?.id == subtitle.id) { Image(systemName: "checkmark").foregroundColor(.blue) } + } + } + .foregroundColor(Color.primary) + } + } + if let selectedQuality = viewStore.state.selectedQuality { + Button { + store.send(.download(viewStore.selectedSource!, viewStore.selectedServer!, selectedQuality, viewStore.selectedSubtitle != nil ? [viewStore.selectedSubtitle!] : [], serverResponse.skipTimes, viewStore.state.episode, serverResponse.headers)) + } label: { + Text("Download") + } + .frame(maxWidth: .infinity, alignment: .center) + } + } + } + .animation(.easeInOut, value: viewStore.state.selectedSource) + .animation(.easeInOut, value: viewStore.serverResponse.value) + .animation(.easeInOut, value: viewStore.state.selectedQuality) + } + } + .onAppear { + store.send(.didAppear) + } + } } .onChange(of: _selectedGroupId) { _ in _selectedVariantId = nil @@ -277,6 +436,9 @@ extension ContentCore { .onChange(of: _selectedVariantId) { _ in _selectedPagingId = nil } + .onAppear { + store.send(.didAppear) + } } } } @@ -380,15 +542,3 @@ extension Playlist.PlaylistType { } // MARK: - ContentListingView_Previews - -#Preview { - ContentCore.View( - store: .init( - initialState: .init( - repoModuleId: Repo().id(.init("")), - playlist: .empty - ), - reducer: { EmptyReducer() } - ) - ) -} diff --git a/Sources/Features/ContentCore/ContentCore.swift b/Sources/Features/ContentCore/ContentCore.swift index 3be5186..e2536d4 100644 --- a/Sources/Features/ContentCore/ContentCore.swift +++ b/Sources/Features/ContentCore/ContentCore.swift @@ -2,7 +2,7 @@ // ContentCore.swift // // -// Created by ErrorErrorError on 7/2/23. +// Created by MochiTeam on 7/2/23. // // @@ -16,6 +16,7 @@ import OrderedCollections import PlaylistHistoryClient import SharedModels import Tagged +import FileClient // MARK: - Cancellable @@ -30,24 +31,34 @@ public struct ContentCore: Reducer { public var repoModuleId: RepoModuleID public var playlist: Playlist public var groups: Loadable<[Playlist.Group]> + public var cachedGroups: [Playlist.Group]? public var playlistHistory: Loadable + + @PresentationState public var downloadSelection: DownloadSelection.State? + + public var downloadedEpisodes: [String] = [] public init( repoModuleId: RepoModuleID, playlist: Playlist, groups: Loadable<[Playlist.Group]> = .pending, - playlistHistory: Loadable = .pending + cachedGroups: [Playlist.Group]? = nil, + playlistHistory: Loadable = .pending, + downloadSelection: DownloadSelection.State? = nil ) { self.repoModuleId = repoModuleId self.playlist = playlist self.groups = groups + self.cachedGroups = cachedGroups self.playlistHistory = playlistHistory + self.downloadSelection = downloadSelection } } @CasePathable @dynamicMemberLookup public enum Action: SendableAction { + case didAppear case update(option: Playlist.ItemsRequestOptions?, Loadable) case didRequestLoadingPendingContent(Playlist.ItemsRequestOptions?) case didTapContent(Playlist.ItemsRequestOptions) @@ -59,6 +70,12 @@ public struct ContentCore: Reducer { id: Playlist.Item.ID, shouldReset: Bool = false ) + case observeDirectory(URL, Bool) + case didTapDownloadPlaylist(Playlist.Item) + case didTapRemoveDownloadedPlaylist(Playlist.Item) + case setDownloadedEpisodes([String]) + case downloadSelection(PresentationAction) + case updateCache([Playlist.Group]) } public enum Error: Swift.Error, Equatable, Sendable { @@ -70,9 +87,25 @@ public struct ContentCore: Reducer { public var body: some ReducerOf { Reduce { state, action in switch action { + case .didAppear: + let playlist = state.playlist + return .run { send in + @Dependency(\.fileClient) var fileClient + let playlistDir = try fileClient.retrieveLibraryDirectory(root: .downloaded, playlist: playlist.id.rawValue) + if fileClient.fileExists(playlistDir.path) { + await send(.observeDirectory(playlistDir, true)) + } else { + await send(.observeDirectory(playlistDir.deletingLastPathComponent(), false)) + } + + } + case let .didTapContent(option): return state.fetchContent(option) - + + case let .didTapDownloadPlaylist(episode): + state.downloadSelection = .selection(.init(repoModuleId: state.repoModuleId, playlistId: state.playlist.id, episode: episode)) + case let .didTapPlaylistItem(groupId, variantId, pageId, itemId, shouldReset): @Dependency(\.playlistHistoryClient) var playlistHistoryClient let playlist = state.playlist @@ -80,9 +113,9 @@ public struct ContentCore: Reducer { let item = state.item(groupId: groupId, variantId: variantId, pageId: pageId, itemId: itemId).value return .run { _ in if let item { - try? await playlistHistoryClient.updateEpId(.init( + try await playlistHistoryClient.updateEpId(.init( rmp: .init(repoId: repoModuleId.repoId.absoluteString, moduleId: repoModuleId.moduleId.rawValue, playlistId: playlist.id.rawValue), - episode: item, + episode: .init(id: item.id.rawValue, title: item.title ?? "Unknown", thumbnail: item.thumbnail ?? playlist.posterImage ?? playlist.bannerImage), playlistName: playlist.title, pageId: pageId.rawValue, groupId: groupId.rawValue, @@ -93,6 +126,32 @@ public struct ContentCore: Reducer { } } } + + case let .didTapRemoveDownloadedPlaylist(episode): + @Dependency(\.fileClient) var fileClient + let playlist = state.playlist + return .run { _ in + try fileClient.removePlaylistFromLibrary(.downloaded, playlist.id.rawValue, episode.id.rawValue) + } + + case let .observeDirectory(directory, isPlaylistDirectory): + @Dependency(\.fileClient) var fileClient + let playlistId = state.playlist.id.rawValue + return .run { send in + for await contents in try fileClient.observeDirectory(directory) { + if (isPlaylistDirectory) { + await send(.setDownloadedEpisodes(contents)) + } else { + if let directory = try? fileClient.retrieveLibraryDirectory(root: .downloaded, playlist: playlistId), FileManager.default.fileExists(atPath: directory.path) { + await send(.observeDirectory(directory, true)) + } + } + + } + } + + case let .setDownloadedEpisodes(episodes): + state.downloadedEpisodes = episodes case let .playlistHistoryResponse(response): state.playlistHistory = response @@ -102,9 +161,18 @@ public struct ContentCore: Reducer { case let .update(option, response): state.update(option, response) + + case .updateCache: + break + + case .downloadSelection: + break } return .none } + .ifLet(\.$downloadSelection, action: \.downloadSelection) { + DownloadSelection() + } } } @@ -146,14 +214,18 @@ extension ContentCore.State { } update(option, .loading) - + let cachedGroups = cachedGroups return .run { send in try await withTaskCancellation(id: Cancellable.fetchContent, cancelInFlight: true) { - let value = try await moduleClient.withModule(id: repoModuleId) { module in - try await module.playlistEpisodes(playlistId, option) + let module = try await moduleClient.getModule(repoModuleId) + do { + let newGroups = try await module.playlistEpisodes(playlistId, option) + await send(.updateCache(newGroups)) + await send(.update(option: option, .loaded(newGroups))) + } catch let error { + await send(.update(option: option, cachedGroups != nil ? .loaded(cachedGroups!) : .failed(error))) } - await send(.update(option: option, .loaded(value))) for await playlistHistoryItems in playlistHistoryClient.observe(.init(repoId: repoModuleId.repoId.absoluteString, moduleId: repoModuleId.moduleId.rawValue, playlistId: playlistId.rawValue)) { if let playlistHistory = playlistHistoryItems.first { await send(.playlistHistoryResponse(.loaded(playlistHistory))) diff --git a/Sources/Features/ContentCore/DownloadSelection.swift b/Sources/Features/ContentCore/DownloadSelection.swift new file mode 100644 index 0000000..298fd13 --- /dev/null +++ b/Sources/Features/ContentCore/DownloadSelection.swift @@ -0,0 +1,145 @@ +// +// DownloadSection.swift +// +// +// DownloadSelection by MochiTeam on 15.04.2024. +// + +import Foundation +import OfflineManagerClient +import ComposableArchitecture +import SharedModels + +private enum Cancellable: Hashable, CaseIterable { + case fetchingSources + case fetchingServer +} + + +public struct DownloadSelection: Reducer { + public enum State: Equatable, Sendable { + case selection(Selection.State) + } + + public enum Action: Equatable, Sendable { + case selection(Selection.Action) + } + + public var body: some ReducerOf { + Scope(state: /State.selection, action: /Action.selection) { + Selection() + } + } + + public struct Selection: Reducer { + public struct State: Equatable, Sendable { + public let repoModuleId: RepoModuleID + public let playlistId: Playlist.ID + public let episode: Playlist.Item + + public var sources: Loadable<[Playlist.EpisodeSource]> + public var serverResponse: Loadable + + public var selectedSource: Playlist.EpisodeSource? = nil + public var selectedServer: Playlist.EpisodeServer? = nil + public var selectedQuality: Playlist.EpisodeServer.Link? = nil + public var selectedSubtitle: Playlist.EpisodeServer.Subtitle? = nil + + public init(repoModuleId: RepoModuleID, playlistId: Playlist.ID, episode: Playlist.Item, sources: Loadable<[Playlist.EpisodeSource]> = .pending, serverResponse: Loadable = .pending) { + self.repoModuleId = repoModuleId + self.playlistId = playlistId + self.episode = episode + self.sources = sources + self.serverResponse = serverResponse + } + } + + public enum Action: Equatable, Sendable { + case didAppear + case sourcesResponse(Loadable<[Playlist.EpisodeSource]>) + case selectSource(Playlist.EpisodeSource) + case selectServer(Playlist.EpisodeServer) + case selectQuality(Playlist.EpisodeServer.Link) + case selectSubtitle(Playlist.EpisodeServer.Subtitle) + case serverResponse(Loadable) + case download(Playlist.EpisodeSource, Playlist.EpisodeServer, Playlist.EpisodeServer.Link, [Playlist.EpisodeServer.Subtitle], [Playlist.EpisodeServer.SkipTime], Playlist.Item, [String: String]) + } + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case .didAppear: + @Dependency(\.moduleClient) var moduleClient + let episode = state.episode + let playlistId = state.playlistId + let repoModuleId = state.repoModuleId + return .run { send in + try await withTaskCancellation(id: Cancellable.fetchingSources, cancelInFlight: true) { + let value = try await moduleClient.withModule(id: repoModuleId) { module in + try await module.playlistEpisodeSources( + .init( + playlistId: playlistId, + episodeId: episode.id + ) + ) + } + await send(.sourcesResponse(.loaded(value))) + } + } + case let .sourcesResponse(sources): + state.sources = sources + + case let .serverResponse(serverResponse): + state.serverResponse = serverResponse + + case let .selectSource(source): + state.selectedSource = source + state.serverResponse = .pending + state.selectedQuality = nil + state.selectedSubtitle = nil + + case let .selectQuality(quality): + state.selectedQuality = quality + + case let .selectSubtitle(subtitle): + state.selectedSubtitle = subtitle + + case let .selectServer(server): + state.serverResponse = .loading + state.selectedQuality = nil + state.selectedSubtitle = nil + guard let source = state.selectedSource else { + return .none + } + let episode = state.episode + let playlistId = state.playlistId + let repoModuleId = state.repoModuleId + @Dependency(\.moduleClient) var moduleClient + state.selectedServer = server + return .run { send in + try await withTaskCancellation(id: Cancellable.fetchingServer, cancelInFlight: true) { + let value = try await moduleClient.withModule(id: repoModuleId) { module in + try await module.playlistEpisodeServer( + .init( + playlistId: playlistId, + episodeId: episode.id, + sourceId: source.id, + serverId: server.id + ) + ) + } + await send(.serverResponse(.loaded(value))) + } + } + + case .download: + @Dependency(\.dismiss) var dismiss + return .run { + await dismiss() + } + } + return .none + } + } + } +} diff --git a/Sources/Features/Discover/DiscoverFeature+Reducer.swift b/Sources/Features/Discover/DiscoverFeature+Reducer.swift index f4712d3..3454ee1 100644 --- a/Sources/Features/Discover/DiscoverFeature+Reducer.swift +++ b/Sources/Features/Discover/DiscoverFeature+Reducer.swift @@ -2,7 +2,7 @@ // DiscoverFeature+Reducer.swift // // -// Created by ErrorErrorError on 4/5/23. +// Created by MochiTeam on 4/5/23. // // @@ -35,7 +35,7 @@ extension DiscoverFeature { } guard let moduleId = UserDefaults.standard.string(forKey: "LastSelectedModuleId"), let repoId = UserDefaults.standard.url(forKey: "LastSelectedRepoId") else { - state.section = .home() + state.section = .home(.init(listings: .pending)) break } return .run { send in @@ -50,11 +50,12 @@ extension DiscoverFeature { case let .view(.didTapContinueWatching(item)): let blankUrl = URL(string: "_blank")! return .run { send in - try? await moduleClient.withModule(id: .init(repoId: Repo.ID(URL(string: item.repoId)!), moduleId: Module.ID(item.moduleId))) { module in + await send(.internal(.setPlaylistLoading(item.playlistID))) + try await moduleClient.withModule(id: .init(repoId: Repo.ID(URL(string: item.repoId)!), moduleId: Module.ID(item.moduleId))) { module in let options = Playlist.ItemsRequestOptions.page(.init(item.groupId), .init(item.variantId), .init(item.pageId)) - let eps = try? await module.playlistEpisodes(Playlist.ID(rawValue: item.playlistID), options) + let eps = try await module.playlistEpisodes(Playlist.ID(rawValue: item.playlistID), options) let playlist = Playlist(id: Playlist.ID(rawValue: item.playlistID), title: item.playlistName, posterImage: nil, bannerImage: nil, url: blankUrl, status: .unknown, type: .video) @@ -62,7 +63,7 @@ extension DiscoverFeature { await send( .delegate( .playbackVideoItem( - eps ?? [], + eps, repoModuleId: .init(repoId: Repo.ID(URL(string: item.repoId)!), moduleId: Module.ID(item.moduleId)), playlist: playlist, group: .init(item.groupId), @@ -72,7 +73,11 @@ extension DiscoverFeature { ) ) ) + await send(.internal(.setPlaylistLoading(nil))) } + } catch: { error, send in + logger.error("failed to play last watched episode: \(error)") + await send(.internal(.setPlaylistLoading(nil))) } case let .view(.didTapRemovePlaylistHistory(repoId, moduleId, playlistId)): @@ -111,12 +116,38 @@ extension DiscoverFeature { } state.path.append(.viewMoreListing(.init(repoModuleId: id, listing: listing))) + + case .view(.onLastWatchedAppear): + return .run { send in + await send(.internal(.fetchLastWatchedListing)) + } + + case .view(.didHomeAppear): + return .run { send in + try await Task.sleep(nanoseconds: 50_000_000) + let items = try await databaseClient.fetch(Request.all).flatMap { $0.modules.map { $0.manifest } } + + for await history in playlistHistoryClient.observeAll() { + let grouped = Dictionary(grouping: history.sorted(by: { $0.dateWatched > $1.dateWatched }), by: { $0.moduleId }).sorted(by: { $0.key < $1.key }) + let listings = grouped.filter { (key, value) in items.contains(where: { $0.id.rawValue == key } ) }.map { (key, value) in + let manifest = items[id: Tagged(rawValue: key)] + return DiscoverFeature.Section.HistoryListings(id: Tagged(rawValue: key), history: value, title: manifest?.name, icon: manifest?.icon) + } + await send(.internal(.setHomeListings(.loaded(listings)))) + } + } + + case let .internal(.setHomeListings(listings)): + if var homeState = state.section.home { + homeState.listings = listings + state.section = .home(homeState) + } case let .internal(.selectedModule(selection)): if let selection { state.section = .module(.init(module: selection, listings: .pending)) } else { - state.section = .home() + state.section = .home(.init(listings: .pending)) } return state.fetchLatestListings(selection) @@ -154,15 +185,14 @@ extension DiscoverFeature { ) ) - case .internal(.onLastWatchedAppear): + case .internal(.fetchLastWatchedListing): guard let repoModule = state.section.module?.module.id else { break } return .run { send in - if let history = try? await playlistHistoryClient.fetchForModule(repoModule.repoId.absoluteString, repoModule.moduleId.rawValue) { - await send(.internal(.updateLastWatched(history))) - } + let history = try await playlistHistoryClient.fetchForModule(repoModule.repoId.absoluteString, repoModule.moduleId.rawValue) + await send(.internal(.updateLastWatched(history))) } case let .internal(.removeLastWatchedPlaylist(playlistId)): @@ -176,6 +206,9 @@ extension DiscoverFeature { case let .internal(.showCaptcha(html, hostname)): state.solveCaptcha = .solveCaptcha(.init(html: html, hostname: hostname)) + + case let .internal(.setPlaylistLoading(loadingState)): + state.playlistLoading = loadingState case .internal(.solveCaptcha): break @@ -184,7 +217,7 @@ extension DiscoverFeature { break case .delegate(.playbackDismissed): - return .send(.internal(.onLastWatchedAppear)) + return .send(.internal(.fetchLastWatchedListing)) case .delegate: break @@ -208,7 +241,7 @@ extension DiscoverFeature.State { @Dependency(\.moduleClient) var moduleClient guard let selectedModule else { - section = .home(.init()) + section = .home(.init(listings: .loading)) return .none } @@ -222,7 +255,7 @@ extension DiscoverFeature.State { try await module.discoverListings() } - await send(.internal(.onLastWatchedAppear)) + await send(.internal(.fetchLastWatchedListing)) await send(.internal(.loadedListings(id, .loaded(value)))) } diff --git a/Sources/Features/Discover/DiscoverFeature+View.swift b/Sources/Features/Discover/DiscoverFeature+View.swift index 5b0d510..e1f59a6 100644 --- a/Sources/Features/Discover/DiscoverFeature+View.swift +++ b/Sources/Features/Discover/DiscoverFeature+View.swift @@ -2,7 +2,7 @@ // DiscoverFeature+View.swift // // -// Created by ErrorErrorError on 4/5/23. +// Created by MochiTeam on 4/5/23. // // @@ -33,13 +33,8 @@ extension DiscoverFeature.View: View { switch viewStore.state { case .empty: VStack {} - case .home: - // TODO: Create home listing - VStack { - Spacer() - Text("Coming soon!") - Spacer() - } + case let .home(homeState): + buildHomeView(homeState: homeState) case let .module(moduleState): buildModuleView(moduleState: moduleState) } @@ -164,6 +159,110 @@ extension DiscoverFeature.View: View { } } +extension DiscoverFeature.View { + @MainActor + func buildHomeView(homeState: DiscoverFeature.Section.HomeState) -> some View { + LoadableView(loadable: homeState.listings) { listings in + ScrollView(.vertical, showsIndicators: false) { + ForEach(listings, id: \.id) { listing in + Group { + if listing.history.isEmpty { + VStack(spacing: 12) { + Spacer() + Text(localizable: "Listings Empty") + .font(.title2.weight(.medium)) + Text(localizable: "There are no listings for this module") + Spacer() + } + .foregroundColor(.gray) + } else { + VStack(spacing: 24) { + Spacer() + .frame(height: 0) + .fixedSize(horizontal: false, vertical: true) + lastWatchedListing(listing) + } + } + } + .transition(.opacity) + } + } + } failedView: { _ in + VStack(spacing: 12) { + Spacer() + + Text(localizable: "Module Error") + .font(.title2.weight(.medium)) + Text(String(localizable: "There was an error retrieving content")) + Button { + store.send(.view(.didTapRetryLoadingModule)) + } label: { + Text(localizable: "Retry") + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background { + RoundedRectangle(cornerRadius: 4, style: .continuous) + .fill(Color.gray.opacity(0.25)) + } + } + .buttonStyle(.plain) + + Spacer() + } + .transition(.opacity) + } waitingView: { + let placeholders: [Playlist] = (0..<10).map { .placeholder($0) } + + buildListingsView( + [ + .init( + id: "0", + title: "Continue Watching", + type: .lastWatched, + paging: .init( + id: "demo-1", + items: placeholders + ) + ), + .init( + id: "1", + title: "Continue Watching", + type: .lastWatched, + paging: .init( + id: "demo-1", + items: placeholders + ) + ), + .init( + id: "2", + title: "Continue Watching", + type: .lastWatched, + paging: .init( + id: "demo-1", + items: placeholders + ) + ), + .init( + id: "3", + title: "Continue Watching", + type: .lastWatched, + paging: .init( + id: "demo-1", + items: placeholders + ) + ) + ] + ) + .shimmering() + .disabled(true) + .transition(.opacity) + .onAppear { + store.send(.view(.didHomeAppear)) + } + } + } +} + extension DiscoverFeature.View { @MainActor func buildModuleView(moduleState: DiscoverFeature.Section.ModuleListingState) -> some View { @@ -250,7 +349,6 @@ extension DiscoverFeature.View { ] ) .shimmering() - .disabled(true) .transition(.opacity) } } @@ -273,7 +371,7 @@ extension DiscoverFeature.View { case .featured: featuredListing(listing) case .lastWatched: - lastWatchedListing() + lastWatchedListing(nil) } } } @@ -283,12 +381,39 @@ extension DiscoverFeature.View { extension DiscoverFeature.View { @MainActor - func lastWatchedListing() -> some View { + func lastWatchedListing(_ listing: DiscoverFeature.Section.HistoryListings?) -> some View { LazyVStack(alignment: .leading) { HStack { - Text("Last Watched") - .font(.title3.weight(.semibold)) - + if let listing = listing { + HStack { + LazyImage(url: URL(string: listing.icon ?? "")) { state in + if let image = state.image { + image + .resizable() + .scaledToFit() + .frame(width: 44, height: 44) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } else { + EmptyView() + } + } + .transition(.opacity) + + VStack(alignment: .leading) { + Text("Continue Watching") + .font(.title3.weight(.semibold)) + if let title = listing.title { + Text(title) + .foregroundStyle(.secondary) + .font(.subheadline) + } + } + } + } else { + Text("Continue Watching") + .font(.title3.weight(.semibold)) + } + Spacer() // if listing.paging.nextPage != nil { @@ -308,45 +433,55 @@ extension DiscoverFeature.View { ScrollView(.horizontal, showsIndicators: false) { HStack(alignment: .top, spacing: 12) { WithViewStore(store, observe: \.`self`) { viewStore in - ForEach(viewStore.lastWatched ?? [], id: \.self) { item in + ForEach(listing?.history ?? viewStore.lastWatched ?? [], id: \.self) { item in VStack(alignment: .leading, spacing: 8) { - ZStack(alignment: .bottom) { - FillAspectImage(url: item.thumbnail ?? URL(string: "")) - .aspectRatio(16 / 10, contentMode: .fit) - .overlay { - LinearGradient( - gradient: .init( - colors: [ - .black.opacity(0), - .black.opacity(0.8) - ], - easing: .easeIn - ), - startPoint: .top, - endPoint: .bottom - ) - } - - VStack(alignment: .leading, spacing: 5) { - Text(item.playlistName ?? "No Title") - .lineLimit(3) - .font(.subheadline.weight(.medium)) - .multilineTextAlignment(.leading) - .fixedSize(horizontal: false, vertical: true) - .foregroundColor(.white) - .padding(.horizontal) - - GeometryReader { proxy in - Color(.white) - .opacity(0.8) - .frame(maxWidth: proxy.size.width * item.timestamp) + ZStack { + ZStack(alignment: .bottom) { + FillAspectImage(url: item.thumbnail ?? URL(string: "")) + .aspectRatio(16 / 10, contentMode: .fit) + .overlay { + LinearGradient( + gradient: .init( + colors: [ + .black.opacity(0), + .black.opacity(0.8) + ], + easing: .easeIn + ), + startPoint: .top, + endPoint: .bottom + ) + } + + VStack(alignment: .leading, spacing: 5) { + Text(item.playlistName ?? "No Title") + .lineLimit(3) + .font(.subheadline.weight(.medium)) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(.white) + .padding(.horizontal) + + GeometryReader { proxy in + Color(.white) + .opacity(0.8) + .frame(maxWidth: proxy.size.width * item.timestamp) + } + .clipShape(Capsule(style: .continuous)) + .frame(maxWidth: .infinity) + .frame(height: 6) } - .clipShape(Capsule(style: .continuous)) - .frame(maxWidth: .infinity) - .frame(height: 6) + } + .blur(radius: viewStore.playlistLoading == item.playlistID ? 5 : 0) + .animation(.easeOut, value: viewStore.playlistLoading) + if (viewStore.playlistLoading == item.playlistID) { + ProgressView() + .controlSize(.large) + .tint(.white) + .frame(width: 50, height: 50) } } - .cornerRadius(12) + .clipShape(RoundedRectangle(cornerRadius: 12)) .contextMenu { Button(role: .destructive) { viewStore.send(.view(.didTapRemovePlaylistHistory(item.repoId, item.moduleId, item.playlistID))) @@ -365,9 +500,11 @@ extension DiscoverFeature.View { .frame(width: 248) .contentShape(Rectangle()) .onTapGesture { - store.send(.view(.didTapContinueWatching(item))) + if (viewStore.playlistLoading == nil) { + store.send(.view(.didTapContinueWatching(item))) + } } - .animation(.easeInOut, value: viewStore.lastWatched) + .animation(.easeInOut, value: listing?.history) } } } @@ -375,6 +512,9 @@ extension DiscoverFeature.View { } .frame(maxWidth: .infinity) } + .onAppear { + store.send(.view(.onLastWatchedAppear)) + } } @MainActor diff --git a/Sources/Features/Discover/DiscoverFeature.swift b/Sources/Features/Discover/DiscoverFeature.swift index a470c34..4a85023 100644 --- a/Sources/Features/Discover/DiscoverFeature.swift +++ b/Sources/Features/Discover/DiscoverFeature.swift @@ -2,7 +2,7 @@ // DiscoverFeature.swift // // -// Created by ErrorErrorError on 4/5/23. +// Created by MochiTeam on 4/5/23. // // @@ -20,6 +20,9 @@ import SharedModels import Styling import SwiftUI import ViewComponents +import Tagged +import OfflineManagerClient +import FileClient // MARK: - DiscoverFeature @@ -115,7 +118,7 @@ public struct DiscoverFeature: Feature { @CasePathable @dynamicMemberLookup public enum Section: Equatable, Sendable { - case home(HomeState = .init()) + case home(HomeState) case module(ModuleListingState) case empty @@ -140,9 +143,20 @@ public struct DiscoverFeature: Feature { moduleState.module.module.icon.flatMap { URL(string: $0) } } } + + public struct HistoryListings: Equatable, Sendable, Identifiable { + public let id: Module.ID + public let history: [PlaylistHistory] + public let title: String? + public let icon: String? + } public struct HomeState: Equatable, Sendable { - public init() {} + public var listings: Loadable<[HistoryListings]> + + init(listings: Loadable<[HistoryListings]>) { + self.listings = listings + } } public struct ModuleListingState: Equatable, Sendable { @@ -162,8 +176,9 @@ public struct DiscoverFeature: Feature { public struct State: FeatureState { public var section: Section public var path: StackState + public var playlistLoading = String?.none - @PresentationState public var lastWatched: [PlaylistHistory]? + public var lastWatched: [PlaylistHistory]? = [] @PresentationState public var moduleLists: ModuleListsFeature.State? @PresentationState public var solveCaptcha: DiscoverFeature.Captcha.State? @@ -171,14 +186,12 @@ public struct DiscoverFeature: Feature { section: DiscoverFeature.Section = .empty, path: StackState = .init(), moduleLists: ModuleListsFeature.State? = nil, - solveCaptcha: DiscoverFeature.Captcha.State? = nil, - lastWatched: [PlaylistHistory]? = [] + solveCaptcha: DiscoverFeature.Captcha.State? = nil ) { self.section = section self.path = path self.moduleLists = moduleLists self.solveCaptcha = solveCaptcha - self.lastWatched = lastWatched } } @@ -189,6 +202,7 @@ public struct DiscoverFeature: Feature { @dynamicMemberLookup public enum ViewAction: SendableAction { case didAppear + case didHomeAppear case didTapOpenModules case didTapContinueWatching(PlaylistHistory) case didTapRemovePlaylistHistory(String, String, String) @@ -196,6 +210,7 @@ public struct DiscoverFeature: Feature { case didTapSearchButton case didTapViewMoreListing(DiscoverListing.ID) case didTapRetryLoadingModule + case onLastWatchedAppear } @CasePathable @@ -223,7 +238,9 @@ public struct DiscoverFeature: Feature { case path(StackAction) case updateLastWatched([PlaylistHistory]) case removeLastWatchedPlaylist(String) - case onLastWatchedAppear + case setPlaylistLoading(String?) + case fetchLastWatchedListing + case setHomeListings(Loadable<[DiscoverFeature.Section.HistoryListings]>) } case view(ViewAction) @@ -234,7 +251,8 @@ public struct DiscoverFeature: Feature { @MainActor public struct View: FeatureView { public let store: StoreOf - + + @Dependency(\.fileClient) var fileClient @Dependency(\.localizableClient.localize) var localize @Environment(\.horizontalSizeClass) var horizontalSizeClass @@ -247,6 +265,8 @@ public struct DiscoverFeature: Feature { @Dependency(\.repoClient) var repoClient @Dependency(\.databaseClient) var databaseClient @Dependency(\.moduleClient) var moduleClient + @Dependency(\.fileClient) var fileClient + @Dependency(\.offlineManagerClient) var offlineManagerClient @Dependency(\.playlistHistoryClient) var playlistHistoryClient public init() {} diff --git a/Sources/Features/Discover/ViewMoreListing.swift b/Sources/Features/Discover/ViewMoreListing.swift index 5425ff5..631ae24 100644 --- a/Sources/Features/Discover/ViewMoreListing.swift +++ b/Sources/Features/Discover/ViewMoreListing.swift @@ -2,7 +2,7 @@ // ViewMoreListing.swift // // -// Created by ErrorErrorError on 12/13/23. +// Created by MochiTeam on 12/13/23. // // diff --git a/Sources/Features/Discover/WebView.swift b/Sources/Features/Discover/WebView.swift index bf076fb..a7b9cc4 100644 --- a/Sources/Features/Discover/WebView.swift +++ b/Sources/Features/Discover/WebView.swift @@ -2,7 +2,7 @@ // WebView.swift // // -// Created by DeNeRr on 22.02.2024. +// Created by MochiTeam on 22.02.2024. // import SwiftUI diff --git a/Sources/Features/DownloadQueue/DownloadQueueFeature+Reducer.swift b/Sources/Features/DownloadQueue/DownloadQueueFeature+Reducer.swift new file mode 100644 index 0000000..c9a1810 --- /dev/null +++ b/Sources/Features/DownloadQueue/DownloadQueueFeature+Reducer.swift @@ -0,0 +1,39 @@ +// +// DownloadQueueFeature+Reducer.swift +// +// +// Created by MochiTeam on 17.05.2024. +// + +import Architecture +import ComposableArchitecture +import Foundation + +extension DownloadQueueFeature: Reducer { + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case .view(.didAppear): + return .run { send in + for await items in offlineManagerClient.observeDownloading() { + await send(.internal(.updateDownloadingItems(items))) + } + } + + case let .view(.didTapCancelDownload(item)): + return .run { send in + try await offlineManagerClient.cancel(item.taskId) + } + + case let .view(.pause(item)): + return .run { send in + try await offlineManagerClient.togglePause(item.taskId) + } + + case let .internal(.updateDownloadingItems(items)): + state.downloadQueue = items + } + return .none + } + } +} diff --git a/Sources/Features/DownloadQueue/DownloadQueueFeature+View.swift b/Sources/Features/DownloadQueue/DownloadQueueFeature+View.swift new file mode 100644 index 0000000..ab289bf --- /dev/null +++ b/Sources/Features/DownloadQueue/DownloadQueueFeature+View.swift @@ -0,0 +1,126 @@ +// +// DownloadQueueFeature+View.swift +// +// +// Created by MochiTeam on 17.05.2024. +// + +import Foundation +import ComposableArchitecture +import SwiftUI +import ViewComponents +import Styling + +// MARK: - DownloadQueueFeature + View + +extension DownloadQueueFeature.View: View { + @MainActor public var body: some View { + WithViewStore(store, observe: \.downloadQueue) { viewStore in + ScrollView { + ForEach(viewStore.state, id: \.`self`) { item in + HStack(spacing: 6) { + FillAspectImage(url: item.image) + .aspectRatio(3 / 4, contentMode: .fit) + .cornerRadius(12) + .frame(height: 80) + + VStack(alignment: .leading, spacing: 6) { + Text("\(item.title)") + .lineLimit(3) + .font(.headline.weight(.medium)) + .multilineTextAlignment(.leading) + Text(item.playlistName) + .lineLimit(2) + .font(.caption.weight(.medium)) + .foregroundStyle(.secondary) + .multilineTextAlignment(.leading) + } + + Spacer() + switch item.status { + case .suspended: + CircularProgressView(progress: item.percentComplete, barStyle: .init(fill: Theme.pastelRed.opacity(0.4), width: 4, blurRadius: 0)) { + Image(systemName: "play.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .padding(6) + .foregroundStyle(Theme.pastelRed) + } + .onTapGesture { + viewStore.send(.pause(item)) + } + .frame(width: 30, height: 30) + .animation(.easeInOut, value: item.status) + case .finished: + Image(systemName: "checkmark.circle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 29, height: 29) + .foregroundStyle(Theme.pastelRed) + .animation(.easeInOut, value: item.status) + case .cancelled: + Image(systemName: "xmark.circle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 29, height: 29) + .foregroundStyle(Color.secondary.opacity(0.4)) + .animation(.easeInOut, value: item.status) + case .downloading: + CircularProgressView(progress: item.percentComplete, barStyle: .init(fill: Theme.pastelRed, width: 4, blurRadius: 0)) { + Image(systemName: "pause.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .padding(6) + .foregroundStyle(Theme.pastelRed) + } + .frame(width: 30, height: 30) + .contentShape(Rectangle()) + .onTapGesture { + viewStore.send(.pause(item)) + } + .animation(.easeInOut, value: item.status) + case .error: + Image(systemName: "exclamationmark.circle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 29, height: 29) + .foregroundStyle(Theme.pastelRed) + .animation(.easeInOut, value: item.status) + } + } + .contentShape(Rectangle()) + .contextMenu { + Button(role: .destructive) { + viewStore.send(.didTapCancelDownload(item)) + } label: { + Label("Cancel Download", systemImage: "xmark") + } + .buttonStyle(.plain) + } + } + } + .frame(maxWidth: .infinity) + .padding() + .navigationTitle("Download Queue") + .onAppear { + viewStore.send(.didAppear) + } + } + } +} + +import OfflineManagerClient +//#Preview { +// DownloadQueueFeature.View( +// store: .init( +// initialState: .init( +// downloadQueue: [ +// OfflineManagerClient.DownloadingItem(id: URL(string: "_blank")!, percentComplete: 0, image: URL(string: "https://fastly.picsum.photos/id/306/200/300.jpg?hmac=T-FQeWIc7YbLbcYdpyDGypNif0btJ8n5P4ozBJx8WgE")!, playlistName: "downloading", title: "Test 3", epNumber: 1, taskId: 0, status: .downloading), +// OfflineManagerClient.DownloadingItem(id: URL(string: "_blank")!, percentComplete: 1, image: URL(string: "https://fastly.picsum.photos/id/1006/200/300.jpg?hmac=8H_lylM_UA6ot7bOUTm-ZzZkGKHmdjC-QU4yB3Xo5aQ")!, playlistName: "finished", title: "Test 2", epNumber: 2, taskId: 1, status: .finished), +// OfflineManagerClient.DownloadingItem(id: URL(string: "_blank")!, percentComplete: 0.35, image: URL(string: "https://fastly.picsum.photos/id/978/200/300.jpg?hmac=sP2_huC-v5a6cNxpdmxp1FPInoDET7j7O3GoftdaEJk")!, playlistName: "suspended", title: "Test 1", epNumber: 3, taskId: 2, status: .suspended) +// ] +// ), +// reducer: { EmptyReducer() } +// ) +// ) +//} diff --git a/Sources/Features/DownloadQueue/DownloadQueueFeature.swift b/Sources/Features/DownloadQueue/DownloadQueueFeature.swift new file mode 100644 index 0000000..40844b3 --- /dev/null +++ b/Sources/Features/DownloadQueue/DownloadQueueFeature.swift @@ -0,0 +1,64 @@ +// +// DownloadQueueFeature.swift +// +// +// Created by MochiTeam on 16.05.2024. +// + +import Architecture +import ComposableArchitecture +import Foundation +import OfflineManagerClient + +// MARK: - DownloadQueueFeature + +public struct DownloadQueueFeature: Feature { + public struct State: FeatureState { + public var downloadQueue: [OfflineManagerClient.DownloadingItem] + + public init( + downloadQueue: [OfflineManagerClient.DownloadingItem] = [] + ) { + self.downloadQueue = downloadQueue + } + } + + @CasePathable + @dynamicMemberLookup + public enum Action: FeatureAction { + @CasePathable + @dynamicMemberLookup + public enum ViewAction: SendableAction { + case didAppear + case didTapCancelDownload(OfflineManagerClient.DownloadingItem) + case pause(OfflineManagerClient.DownloadingItem) + } + + @CasePathable + @dynamicMemberLookup + public enum DelegateAction: SendableAction {} + + @CasePathable + @dynamicMemberLookup + public enum InternalAction: SendableAction { + case updateDownloadingItems([OfflineManagerClient.DownloadingItem]) + } + + case view(ViewAction) + case delegate(DelegateAction) + case `internal`(InternalAction) + } + + @MainActor + public struct View: FeatureView { + public let store: StoreOf + @MainActor + public init(store: StoreOf) { + self.store = store + } + } + + @Dependency(\.offlineManagerClient) var offlineManagerClient + + public init() {} +} diff --git a/Sources/Features/LIbrary/LibraryFeature+Reducer.swift b/Sources/Features/LIbrary/LibraryFeature+Reducer.swift new file mode 100644 index 0000000..2963c16 --- /dev/null +++ b/Sources/Features/LIbrary/LibraryFeature+Reducer.swift @@ -0,0 +1,123 @@ +// +// LibraryFeature+Reducer.swift +// +// +// Created by MochiTeam on 09.04.2024. +// + +import Architecture +import ComposableArchitecture +import Foundation +import SharedModels +import OfflineManagerClient +import FileClient + +// MARK: - LibraryFeature + Reducer + +extension LibraryFeature: Reducer { + public var body: some ReducerOf { + Scope(state: \.self, action: \.view) { + BindingReducer() + } + + Reduce { state, action in + switch action { + case .view(.didAppear): + return .run { send in + try fileClient.initializeLibrary() + await send(.internal(.observeDirectory(try fileClient.retrieveLibraryDirectory(root: .playlistCache)))) + } + + case let .view(.didTapPlaylist(fileMetadata)): + state.path.append(.playlistDetails(.init( + content: .init( + repoModuleId: .init(repoId: .init(rawValue: fileMetadata.repoModuleId.repoId), moduleId: .init(rawValue: fileMetadata.repoModuleId.moduleId)), + playlist: fileMetadata.playlist, + cachedGroups: fileMetadata.groups + ), + details: fileMetadata.details != nil ? .loaded(fileMetadata.details!) : .pending))) + + case let .view(.didTapDownloadQueue): + state.path.append(.downloadQueue(.init())) + + case let .view(.didTapRemoveBookmark(cache)): + return .run { _ in + try await offlineManagerClient.remove(.cache, cache.playlist.id.rawValue, nil); + } + + case let .view(.didTapRemovePlaylist(cache)): + return .run { _ in + try await offlineManagerClient.remove(.all, cache.playlist.id.rawValue, nil); + } + + case .view(.didtapOpenLibraryCollectionSheet): + break + + case .view(.didTapShowDownloadedOnly): + let lastOfflineOnlyState = !state.showOfflineOnly + state.showOfflineOnly = !state.showOfflineOnly + return .run { send in + await send(.internal(.observeDirectory(try fileClient.retrieveLibraryDirectory(root: lastOfflineOnlyState ? .downloaded : .playlistCache)))) + } + + + case .view(.binding(\.$searchValue)): + if let playlists = state.playlists.value { + state.searchedPlaylists = playlists.filter { $0.playlist.title?.lowercased().contains(state.searchValue.lowercased()) ?? false } + } + + case .view(.binding): + break + + case .view: + break + + case let .internal(.path(.element(_, .playlistDetails(.delegate(.playbackVideoItem(items, id, playlist, group, variant, paging, itemId)))))): + return .run { send in + await send( + .delegate( + .playbackVideoItem( + items, + repoModuleId: id, + playlist: playlist, + group: group, + variant: variant, + paging: paging, + itemId: itemId + ) + ) + ) + } + + case let .internal(.observeDirectory(directory)): + return .run { send in + for try await playlistIds in try fileClient.observeDirectory(directory) { + let playlists = try playlistIds.flatMap { + if let json = try fileClient.retrieveLibraryMetadata(root: .playlistCache, encodedPlaylist: $0) { + var cache: PlaylistCache = try JSONDecoder().decode(PlaylistCache.self, from: json) + if let image = fileClient.getLibraryPlaylistImage(playlist: cache.playlist.id.rawValue) { + cache.playlist.posterImage = image + cache.playlist.bannerImage = image + } + return cache + } + return nil + } + await send(.internal(.playlistsDidLoad(playlists))) + } + } + + case let .internal(.playlistsDidLoad(playlists)): + state.playlists = .loaded(playlists.sorted(by: { $0.playlist.title ?? "" < $1.playlist.title ?? "" })) + case .internal: + break + case .delegate: + break + } + return .none + } + .forEach(\.path, action: \.internal.path) { + Path() + } + } +} diff --git a/Sources/Features/LIbrary/LibraryFeature+View.swift b/Sources/Features/LIbrary/LibraryFeature+View.swift new file mode 100644 index 0000000..853e6e5 --- /dev/null +++ b/Sources/Features/LIbrary/LibraryFeature+View.swift @@ -0,0 +1,159 @@ +// +// LibraryFeature+View.swift +// +// +// Created by MochiTeam on 09.04.2024. +// + +import Architecture +import ComposableArchitecture +import LocalizableClient +import Foundation +import SwiftUI +import ViewComponents +import Styling +import PlaylistDetails +import AVKit +import DownloadQueue + +// MARK: - LibraryFeature + View + +extension LibraryFeature.View: View { + @MainActor public var body: some View { + NavStack( + store.scope( + state: \.path, + action: \.internal.path + ) + ) { + WithViewStore(store, observe: \.`self`) { viewStore in + LoadableView(loadable: viewStore.state.playlists) { playlists in + ScrollView(.vertical) { + LazyVGrid( + columns: [.init(.adaptive(minimum: 120), alignment: .top)], + alignment: .leading + ) { + ForEach(viewStore.searchValue.isEmpty ? playlists : viewStore.searchedPlaylists, id: \.playlist.id) { item in + VStack(alignment: .leading) { + FillAspectImage(url: item.playlist.posterImage ?? item.playlist.bannerImage ?? URL(string: "")!) + .aspectRatio(3 / 4, contentMode: .fit) + .cornerRadius(12) + .contextMenu { + Button(role: .destructive) { + viewStore.send(.didTapRemoveBookmark(item)) + } label: { + Label("Remove Bookmark", systemImage: "bookmark.slash") + } + } + Text(item.playlist.title ?? "") + .font(.footnote) + } + .contentShape(Rectangle()) + .onTapGesture { + viewStore.send(.didTapPlaylist(item)) + } + } + } + .animation(.easeInOut, value: viewStore.searchedPlaylists) + .animation(.easeInOut, value: playlists) + .safeAreaInset(edge: .top) { + ScrollView(.horizontal, showsIndicators: false) { + WithViewStore(store, observe: \.showOfflineOnly) { viewStore in + Button { + viewStore.send(.didTapShowDownloadedOnly) + } label: { + Text("Downloaded") + .font(.footnote) + .foregroundStyle(viewStore.state ? Color.white : Theme.pastelRed) + .padding(8) + .background( + Capsule() + .style( + withStroke: Color.gray.opacity(0.2), + fill: viewStore.state ? Theme.pastelRed : buttonBackgroundColor + ) + ) + } + } + } + } + .padding(.horizontal) + } + .searchable(text: viewStore.$searchValue.removeDuplicates(), placement: .toolbar) + } + } + .toolbar { + ToolbarItem(placement: .navigation) { + Button { +// store.send(.view(.didtapOpenLibraryCollectionSheet)) + } label: { + HStack(alignment: .center, spacing: 8) { + Text(selectedDirectory ?? "Library") + +// Image(systemName: "chevron.down") +// .font(.caption.weight(.bold)) +// .foregroundColor(.gray) + } + #if os(iOS) + .font(.title.bold()) + #else + .font(.title3.bold()) + #endif + .contentShape(Rectangle()) + .scaleEffect(1.0) + .transition(.opacity) + } + #if os(macOS) + .buttonStyle(.bordered) + #else + .buttonStyle(.plain) + #endif + } + ToolbarItem(placement: .topBarTrailing) { + Button { + store.send(.view(.didTapDownloadQueue)) + } label: { + Image(systemName: "arrow.down.circle") + } + .foregroundColor(Theme.pastelRed) + } + } + .navigationTitle("") +#if os(iOS) + .navigationBarTitleDisplayMode(.inline) +#endif + } destination: { store in + SwitchStore(store) { state in + switch state { + case .playlistDetails: + CaseLet( + /LibraryFeature.Path.State.playlistDetails, + action: LibraryFeature.Path.Action.playlistDetails, + then: { store in PlaylistDetailsFeature.View(store: store) } + ) + case .downloadQueue: + CaseLet( + /LibraryFeature.Path.State.downloadQueue, + action: LibraryFeature.Path.Action.downloadQueue, + then: { store in DownloadQueueFeature.View(store: store) } + ) + } + } + } + .onAppear { + store.send(.view(.didAppear)) + } + } +} + +#Preview { + LibraryFeature.View( + store: .init( + initialState: .init( + path: .init(), + playlists: .loaded(.init()) + ), + reducer: { EmptyReducer() } + ) + ) +} diff --git a/Sources/Features/LIbrary/LibraryFeature.swift b/Sources/Features/LIbrary/LibraryFeature.swift new file mode 100644 index 0000000..3067d2b --- /dev/null +++ b/Sources/Features/LIbrary/LibraryFeature.swift @@ -0,0 +1,123 @@ +// +// LibraryFeature.swift +// +// +// Created by MochiTeam on 09.04.2024. +// + +import Architecture +import ComposableArchitecture +import Foundation +import FileClient +import SwiftUI +import ViewComponents +import PlaylistDetails +import SharedModels +import DownloadQueue + + +// MARK: - LibraryFeature + +public struct LibraryFeature: Feature { + public struct Path: Reducer { + @CasePathable + @dynamicMemberLookup + public enum State: Equatable, Sendable { + case playlistDetails(PlaylistDetailsFeature.State) + case downloadQueue(DownloadQueueFeature.State) + } + + @CasePathable + @dynamicMemberLookup + public enum Action: Equatable, Sendable { + case playlistDetails(PlaylistDetailsFeature.Action) + case downloadQueue(DownloadQueueFeature.Action) + } + + @ReducerBuilder public var body: some ReducerOf { + Scope(state: \.playlistDetails, action: \.playlistDetails) { + PlaylistDetailsFeature() + } + Scope(state: \.downloadQueue, action: \.downloadQueue) { + DownloadQueueFeature() + } + } + } + + public struct State: FeatureState { + public var path: StackState + public var playlists: Loadable<[PlaylistCache]> + + public var searchedPlaylists: [PlaylistCache] = [] + @BindingState public var searchValue: String = "" + public var showOfflineOnly: Bool = false + + public init(path: StackState = .init(), playlists: Loadable<[PlaylistCache]> = .pending) { + self.path = path + self.playlists = playlists + } + } + + @CasePathable + @dynamicMemberLookup + public enum Action: FeatureAction { + @CasePathable + @dynamicMemberLookup + public enum ViewAction: SendableAction, BindableAction { + case didAppear + case didTapPlaylist(PlaylistCache) + case didtapOpenLibraryCollectionSheet + case didTapRemoveBookmark(PlaylistCache) + case didTapRemovePlaylist(PlaylistCache) + case didTapShowDownloadedOnly + case didTapDownloadQueue + + case binding(BindingAction) + } + + @CasePathable + @dynamicMemberLookup + public enum DelegateAction: SendableAction { + case playbackVideoItem( + Playlist.ItemsResponse, + repoModuleId: RepoModuleID, + playlist: Playlist, + group: Playlist.Group.ID, + variant: Playlist.Group.Variant.ID, + paging: PagingID, + itemId: Playlist.Item.ID + ) + } + + @CasePathable + @dynamicMemberLookup + public enum InternalAction: SendableAction { + case path(StackAction) + case playlistsDidLoad([PlaylistCache]) + case observeDirectory(URL) + } + + case view(ViewAction) + case delegate(DelegateAction) + case `internal`(InternalAction) + } + + @MainActor + public struct View: FeatureView { + public let store: StoreOf + @Environment(\.colorScheme) var scheme + var buttonBackgroundColor: Color { scheme == .dark ? .init(white: 0.2) : .init(white: 0.94) } + + @SwiftUI.State public var selectedDirectory: String? + + @MainActor + public init(store: StoreOf) { + self.store = store + } + } + + @Dependency(\.fileClient) var fileClient + @Dependency(\.offlineManagerClient) var offlineManagerClient + + public init() {} +} diff --git a/Sources/Features/Library/LibraryFeature+Reducer.swift b/Sources/Features/Library/LibraryFeature+Reducer.swift deleted file mode 100644 index 9295f2f..0000000 --- a/Sources/Features/Library/LibraryFeature+Reducer.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// LibraryFeature+Reducer.swift -// -// -// Created ErrorErrorError on 1/2/24. -// Copyright © 2024. All rights reserved. -// - -import Architecture -import ComposableArchitecture - -extension LibraryFeature: Reducer { - public var body: some ReducerOf { - Reduce { _, action in - switch action { - case .view: - break - - case .internal: - break - - case .delegate: - break - } - return .none - } - } -} diff --git a/Sources/Features/Library/LibraryFeature+View.swift b/Sources/Features/Library/LibraryFeature+View.swift deleted file mode 100644 index b99cf81..0000000 --- a/Sources/Features/Library/LibraryFeature+View.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// LibraryFeature+View.swift -// -// -// Created ErrorErrorError on 1/2/24. -// Copyright © 2024. All rights reserved. -// - -import Architecture -import ComposableArchitecture -import SwiftUI - -// MARK: - LibraryFeature.View + View - -extension LibraryFeature.View: View { - @MainActor public var body: some View { - WithViewStore(store, observe: \.`self`) { viewStore in - Text("Hello, World!") - .onAppear { - viewStore.send(.didAppear) - } - } - } -} - -// MARK: - LibraryFeatureView_Previews - -struct LibraryFeatureView_Previews: PreviewProvider { - static var previews: some View { - LibraryFeature.View( - store: .init( - initialState: .init(), - reducer: { LibraryFeature() } - ) - ) - } -} diff --git a/Sources/Features/Library/LibraryFeature.swift b/Sources/Features/Library/LibraryFeature.swift deleted file mode 100644 index 313e7c7..0000000 --- a/Sources/Features/Library/LibraryFeature.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// LibraryFeature.swift -// -// -// Created ErrorErrorError on 1/2/24. -// Copyright © 2024. All rights reserved. -// - -import Architecture -import ComposableArchitecture - -public enum LibraryFeature: Feature { - public struct State: FeatureState { - // TODO: Set state - - public init() {} - } - - @CasePathable - @dynamicMemberLookup - public enum Action: FeatureAction { - public enum ViewAction: SendableAction {} - public enum DelegateAction: SendableAction {} - public enum InternalAction: SendableAction {} - - case view(ViewAction) - case delegate(DelegateAction) - case `internal`(InternalAction) - } - - @MainActor - public struct View: FeatureView { - public let store: StoreOf - - public nonisolated init(store: StoreOf) { - self.store = store - } - } - - public init() {} -} diff --git a/Sources/Features/ModuleLists/ModuleListsFeature+Reducer.swift b/Sources/Features/ModuleLists/ModuleListsFeature+Reducer.swift index c53e14a..375564f 100644 --- a/Sources/Features/ModuleLists/ModuleListsFeature+Reducer.swift +++ b/Sources/Features/ModuleLists/ModuleListsFeature+Reducer.swift @@ -2,7 +2,7 @@ // ModuleListsFeature+Reducer.swift // // -// Created ErrorErrorError on 4/23/23. +// Created MochiTeam on 4/23/23. // Copyright © 2023. All rights reserved. // @@ -12,8 +12,6 @@ import DatabaseClient import Foundation import RepoClient -let defaults = UserDefaults.standard - extension ModuleListsFeature { public var body: some ReducerOf { Reduce { state, action in @@ -29,13 +27,19 @@ extension ModuleListsFeature { return .run { await dismiss() } + + case .view(.didTapHome): + UserDefaults.standard.set(nil, forKey: "LastSelectedModuleId") + UserDefaults.standard.set(nil, forKey: "LastSelectedRepoId") + return .concatenate(.send(.delegate(.selectedModule(nil)))) + case let .view(.didSelectModule(repoId, moduleId)): guard let module = state.repos[id: repoId]?.modules[id: moduleId]?.manifest else { break } - defaults.set(moduleId.rawValue, forKey: "LastSelectedModuleId") - defaults.set(repoId.rawValue, forKey: "LastSelectedRepoId") + UserDefaults.standard.set(moduleId.rawValue, forKey: "LastSelectedModuleId") + UserDefaults.standard.set(repoId.rawValue, forKey: "LastSelectedRepoId") return .concatenate(.send(.delegate(.selectedModule(.init(repoId: repoId, module: module))))) case let .internal(.fetchRepos(.success(repos))): diff --git a/Sources/Features/ModuleLists/ModuleListsFeature+View.swift b/Sources/Features/ModuleLists/ModuleListsFeature+View.swift index 1464b08..787c499 100644 --- a/Sources/Features/ModuleLists/ModuleListsFeature+View.swift +++ b/Sources/Features/ModuleLists/ModuleListsFeature+View.swift @@ -2,7 +2,7 @@ // ModuleListsFeature+View.swift // // -// Created ErrorErrorError on 4/23/23. +// Created MochiTeam on 4/23/23. // Copyright © 2023. All rights reserved. // @@ -48,6 +48,12 @@ extension ModuleListsFeature.View: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal) + Button { + store.send(.view(.didTapHome)) + } label: { + Image(systemName: "house") + } + .buttonStyle(.materialToolbarItem) Button { store.send(.view(.didTapToDismiss)) } label: { @@ -200,7 +206,7 @@ import Styling remoteURL: .init(string: "/").unsafelyUnwrapped, manifest: .init( name: "Local Repo", - author: "errorerrorerror", + author: "MochiTeam", description: "This is a local repo" ), modules: [ diff --git a/Sources/Features/ModuleLists/ModuleListsFeature.swift b/Sources/Features/ModuleLists/ModuleListsFeature.swift index e089e3a..fc48c4e 100644 --- a/Sources/Features/ModuleLists/ModuleListsFeature.swift +++ b/Sources/Features/ModuleLists/ModuleListsFeature.swift @@ -2,7 +2,7 @@ // ModuleListsFeature.swift // // -// Created ErrorErrorError on 4/23/23. +// Created MochiTeam on 4/23/23. // Copyright © 2023. All rights reserved. // @@ -46,6 +46,7 @@ public struct ModuleListsFeature: Feature { public enum ViewAction: SendableAction { case onTask case didTapToDismiss + case didTapHome case didSelectModule(Repo.ID, Module.ID) } diff --git a/Sources/Features/PlaylistDetails/PlaylistDetailsFeature+Reducer.swift b/Sources/Features/PlaylistDetails/PlaylistDetailsFeature+Reducer.swift index d1d05fc..9e41e74 100644 --- a/Sources/Features/PlaylistDetails/PlaylistDetailsFeature+Reducer.swift +++ b/Sources/Features/PlaylistDetails/PlaylistDetailsFeature+Reducer.swift @@ -2,7 +2,7 @@ // PlaylistDetailsFeature+Reducer.swift // // -// Created ErrorErrorError on 5/19/23. +// Created MochiTeam on 5/19/23. // Copyright © 2023. All rights reserved. // @@ -37,7 +37,32 @@ extension PlaylistDetailsFeature { Reduce { state, action in switch action { case .view(.onTask): - return state.fetchPlaylistDetails() + @Dependency(\.fileClient) var fileClient + + let playlistId = state.playlist.id.rawValue + let cacheDirectory = try? fileClient.retrieveLibraryDirectory(root: .playlistCache) + let downloadsDirectory = try? fileClient.retrieveLibraryDirectory(root: .downloaded) + return .merge( + state.fetchPlaylistDetails(), + .run { send in + if let cacheDirectory = cacheDirectory, fileClient.fileExists(cacheDirectory.path) { + for await _ in try fileClient.observeDirectory(cacheDirectory) { + let dir = try fileClient.retrieveLibraryDirectory(root: .playlistCache, playlist: playlistId) + await send(.internal(.setBookmark(FileManager.default.fileExists(atPath: dir.path)))) + } + } + }, + .run { send in + if let downloadsDirectory = downloadsDirectory, fileClient.fileExists(downloadsDirectory.path) { + for await _ in try fileClient.observeDirectory(downloadsDirectory) { + if let dir = try? fileClient.retrieveLibraryDirectory(root: .downloaded, playlist: playlistId) { + let isEmpty = (try? FileManager.default.contentsOfDirectory(atPath: dir.path).isEmpty) ?? true + await send(.internal(.setHasDownloadedContent(!isEmpty))) + } + } + } + } + ) case .view(.didTappedBackButton): return .run { await self.dismiss() } @@ -52,6 +77,31 @@ extension PlaylistDetailsFeature { description: state.details.value?.synopsis ?? "No Description Available" ) ) + + case .view(.didTapAddToLibrary): + let playlist = state.playlist + if (state.isInLibrary) { + return .run { + try await offlineManagerClient.remove(.cache, playlist.id.rawValue, nil) + } + } + let repoModuleId = state.content.repoModuleId + let details = state.details.value + let groups = state.content.groups.value + return .run { + try await offlineManagerClient.cache(.init( + groups: groups, + playlist: playlist, + details: details, + repoModuleId: repoModuleId + )) + } + + case .view(.didTapRemoveDownloads): + let playlist = state.playlist + return .run { + try await offlineManagerClient.remove(.download, playlist.id.rawValue, nil) + } case .view(.binding): break @@ -61,6 +111,42 @@ extension PlaylistDetailsFeature { case let .internal(.playlistDetailsResponse(loadable)): state.details = loadable + break + + case let .internal(.content(.downloadSelection(.presented(.selection(.download(source, server, link, subtitles, skipTimes, episode, headers)))))): + let playlist = state.playlist + let details = state.details.value + let groups = state.content.groups.value + let repoModuleId = state.content.repoModuleId + return .run { send in + try await offlineManagerClient.download(.init( + episodeMetadata: .init(link: link, source: source, subtitles: subtitles, server: server, skipTimes: skipTimes), + headers: headers, + episode: episode, + groups: groups, + playlist: playlist, + details: details, + repoModuleId: repoModuleId + )) + } + + case let .internal(.setBookmark(bookmarked)): + state.isInLibrary = bookmarked + + case let .internal(.setHasDownloadedContent(isDownloaded)): + state.hasDownloadedContent = isDownloaded + + case let .internal(.content(.updateCache(newCache))): + if (!state.isInLibrary) { + break + } + let playlist = state.playlist + let details = state.details.value + let repoModuleId = state.content.repoModuleId + return .run { send in + try await offlineManagerClient.cache(.init(groups: newCache, playlist: playlist, details: details, repoModuleId: repoModuleId)) + } + case let .internal(.content(.didTapPlaylistItem(groupId, variantId, pageId, itemId, _))): guard state.content.groups.value != nil else { diff --git a/Sources/Features/PlaylistDetails/PlaylistDetailsFeature.swift b/Sources/Features/PlaylistDetails/PlaylistDetailsFeature.swift index 12ea655..5cb49dc 100644 --- a/Sources/Features/PlaylistDetails/PlaylistDetailsFeature.swift +++ b/Sources/Features/PlaylistDetails/PlaylistDetailsFeature.swift @@ -2,7 +2,7 @@ // PlaylistDetailsFeature.swift // // -// Created ErrorErrorError on 5/19/23. +// Created MochiTeam on 5/19/23. // Copyright © 2023. All rights reserved. // @@ -17,6 +17,7 @@ import SharedModels import Styling import SwiftUI import ViewComponents +import OfflineManagerClient public struct PlaylistDetailsFeature: Feature { public struct Destination: ComposableArchitecture.Reducer { @@ -58,6 +59,8 @@ public struct PlaylistDetailsFeature: Feature { public var content: ContentCore.State public var playlist: Playlist { content.playlist } public var details: Loadable + public var isInLibrary: Bool = false + public var hasDownloadedContent: Bool = false @PresentationState public var destination: Destination.State? @@ -68,14 +71,18 @@ public struct PlaylistDetailsFeature: Feature { } public var resumableState: Resumable { - // TODO: Show start based on last resumed or selected content? if playlist.status == .upcoming { return .upcoming } if let group = content.groups.value?.first(where: { $0.default ?? false }) ?? content.groups.value?.first, let variant = group.variants.value?.first { if let epId = playlistHistory.value?.epId { - if let page = variant.pagings.value?.first(where: { $0.items.value!.contains(where: { $0.id.rawValue == epId }) }), + if let page = variant.pagings.value?.first(where: { + if let output = $0.items.value { + return output.contains(where: { $0.id.rawValue == epId }) + } + return true + }), let item = page.items.value?.first(where: { $0.id.rawValue == epId }) { return .resume(group.id, variant.id, page.id, item.id, item.title ?? "", playlistHistory.value?.timestamp ?? 0.0) } @@ -159,6 +166,8 @@ public struct PlaylistDetailsFeature: Feature { case didTapToRetryDetails case didTapOnReadMore case binding(BindingAction) + case didTapAddToLibrary + case didTapRemoveDownloads } @CasePathable @@ -179,6 +188,8 @@ public struct PlaylistDetailsFeature: Feature { case playlistDetailsResponse(Loadable) case content(ContentCore.Action) case destination(PresentationAction) + case setBookmark(Bool) + case setHasDownloadedContent(Bool) } case view(ViewAction) @@ -200,6 +211,8 @@ public struct PlaylistDetailsFeature: Feature { } } + @Dependency(\.offlineManagerClient) var offlineManagerClient + @Dependency(\.fileClient) var fileClient @Dependency(\.moduleClient) var moduleClient @Dependency(\.databaseClient) var databaseClient @Dependency(\.repoClient) var repoClient diff --git a/Sources/Features/PlaylistDetails/iOS/PlaylistDetailsFeature+View+iOS.swift b/Sources/Features/PlaylistDetails/iOS/PlaylistDetailsFeature+View+iOS.swift index 88e6c0b..4425fb2 100644 --- a/Sources/Features/PlaylistDetails/iOS/PlaylistDetailsFeature+View+iOS.swift +++ b/Sources/Features/PlaylistDetails/iOS/PlaylistDetailsFeature+View+iOS.swift @@ -2,7 +2,7 @@ // PlaylistDetailsFeature+View+iOS.swift // // -// Created ErrorErrorError on 5/19/23. +// Created MochiTeam on 5/19/23. // Copyright © 2023. All rights reserved. // @@ -83,11 +83,15 @@ extension PlaylistDetailsFeature.View: View { .navigationBarTitle("", displayMode: .inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { - Button {} label: { - Image(systemName: "plus") + WithViewStore(store, observe: \.isInLibrary) { viewStore in + Button { + viewStore.send(.didTapAddToLibrary) + } label: { + Image(systemName: viewStore.state ? "bookmark.fill" : "plus") + } + .animation(.spring, value: viewStore.state) + .buttonStyle(.materialToolbarItem) } - .buttonStyle(.materialToolbarItem) - .disabled(true) } ToolbarItem(placement: .topBarTrailing) { @@ -100,6 +104,16 @@ extension PlaylistDetailsFeature.View: View { Text("Open Playlist URL") } } + WithViewStore(store, observe: \.hasDownloadedContent) { viewStore in + if (viewStore.state) { + Button(role: .destructive) { + viewStore.send(.didTapRemoveDownloads) + } label: { + Image(systemName: "trash.fill") + Text("Remove Downloaded Content") + } + } + } } label: { Image(systemName: "ellipsis") .materialToolbarItemStyle() diff --git a/Sources/Features/PlaylistDetails/macOS/PlaylistDetailsFeature+View+macOS.swift b/Sources/Features/PlaylistDetails/macOS/PlaylistDetailsFeature+View+macOS.swift index cec111f..1f3b625 100644 --- a/Sources/Features/PlaylistDetails/macOS/PlaylistDetailsFeature+View+macOS.swift +++ b/Sources/Features/PlaylistDetails/macOS/PlaylistDetailsFeature+View+macOS.swift @@ -2,7 +2,7 @@ // PlaylistDetailsFeature+View+macOS.swift // // -// Created by ErrorErrorError on 11/23/23. +// Created by MochiTeam on 11/23/23. // // diff --git a/Sources/Features/Repos/Components/RepoURLTextField+iOS.swift b/Sources/Features/Repos/Components/RepoURLTextField+iOS.swift index b4e7d8f..c005265 100644 --- a/Sources/Features/Repos/Components/RepoURLTextField+iOS.swift +++ b/Sources/Features/Repos/Components/RepoURLTextField+iOS.swift @@ -2,7 +2,7 @@ // RepoURLTextField+iOS.swift // // -// Created by ErrorErrorError on 12/15/23. +// Created by MochiTeam on 12/15/23. // // diff --git a/Sources/Features/Repos/Components/RepoURLTextField+macOS.swift b/Sources/Features/Repos/Components/RepoURLTextField+macOS.swift index 74ccac2..eee2da6 100644 --- a/Sources/Features/Repos/Components/RepoURLTextField+macOS.swift +++ b/Sources/Features/Repos/Components/RepoURLTextField+macOS.swift @@ -2,7 +2,7 @@ // RepoURLTextField+macOS.swift // // -// Created by ErrorErrorError on 12/15/23. +// Created by MochiTeam on 12/15/23. // // diff --git a/Sources/Features/Repos/RepoPackages/RepoPackagesFeature+Reducer.swift b/Sources/Features/Repos/RepoPackages/RepoPackagesFeature+Reducer.swift index 505a6f7..71d3748 100644 --- a/Sources/Features/Repos/RepoPackages/RepoPackagesFeature+Reducer.swift +++ b/Sources/Features/Repos/RepoPackages/RepoPackagesFeature+Reducer.swift @@ -2,7 +2,7 @@ // RepoPackagesFeature+Reducer.swift // // -// Created by ErrorErrorError on 8/16/23. +// Created by MochiTeam on 8/16/23. // // diff --git a/Sources/Features/Repos/RepoPackages/RepoPackagesFeature+View.swift b/Sources/Features/Repos/RepoPackages/RepoPackagesFeature+View.swift index 43e8540..c3ea8e5 100644 --- a/Sources/Features/Repos/RepoPackages/RepoPackagesFeature+View.swift +++ b/Sources/Features/Repos/RepoPackages/RepoPackagesFeature+View.swift @@ -2,7 +2,7 @@ // RepoPackagesFeature+View.swift // // -// Created ErrorErrorError on 5/4/23. +// Created MochiTeam on 5/4/23. // Copyright © 2023. All rights reserved. // @@ -357,7 +357,7 @@ extension StatusView { initialState: .init( repo: .init( remoteURL: .init(string: "/").unsafelyUnwrapped, - manifest: .init(name: "Repo 1", author: "errorerrorerror") + manifest: .init(name: "Repo 1", author: "MochiTeam") ) ), reducer: { EmptyReducer() } diff --git a/Sources/Features/Repos/RepoPackages/RepoPackagesFeature.swift b/Sources/Features/Repos/RepoPackages/RepoPackagesFeature.swift index ca1428d..8fa4a71 100644 --- a/Sources/Features/Repos/RepoPackages/RepoPackagesFeature.swift +++ b/Sources/Features/Repos/RepoPackages/RepoPackagesFeature.swift @@ -2,7 +2,7 @@ // RepoPackagesFeature.swift // // -// Created ErrorErrorError on 5/4/23. +// Created MochiTeam on 5/4/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Features/Repos/ReposFeature+Reducer.swift b/Sources/Features/Repos/ReposFeature+Reducer.swift index 81dd17a..0884b50 100644 --- a/Sources/Features/Repos/ReposFeature+Reducer.swift +++ b/Sources/Features/Repos/ReposFeature+Reducer.swift @@ -2,7 +2,7 @@ // ReposFeature+Reducer.swift // // -// Created ErrorErrorError on 4/18/23. +// Created MochiTeam on 4/18/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Features/Repos/ReposFeature+View.swift b/Sources/Features/Repos/ReposFeature+View.swift index b95b7fd..756b8d9 100644 --- a/Sources/Features/Repos/ReposFeature+View.swift +++ b/Sources/Features/Repos/ReposFeature+View.swift @@ -2,7 +2,7 @@ // ReposFeature+View.swift // // -// Created ErrorErrorError on 4/18/23. +// Created MochiTeam on 4/18/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Features/Repos/ReposFeature.swift b/Sources/Features/Repos/ReposFeature.swift index 3985c41..870bfa6 100644 --- a/Sources/Features/Repos/ReposFeature.swift +++ b/Sources/Features/Repos/ReposFeature.swift @@ -2,7 +2,7 @@ // ReposFeature.swift // // -// Created ErrorErrorError on 4/18/23. +// Created MochiTeam on 4/18/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Features/Search/SearchFeature+Reducer.swift b/Sources/Features/Search/SearchFeature+Reducer.swift index 9a88a18..0b41903 100644 --- a/Sources/Features/Search/SearchFeature+Reducer.swift +++ b/Sources/Features/Search/SearchFeature+Reducer.swift @@ -2,7 +2,7 @@ // SearchFeature+Reducer.swift // // -// Created ErrorErrorError on 4/18/23. +// Created MochiTeam on 4/18/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Features/Search/SearchFeature+View.swift b/Sources/Features/Search/SearchFeature+View.swift index a69b14d..fb75278 100644 --- a/Sources/Features/Search/SearchFeature+View.swift +++ b/Sources/Features/Search/SearchFeature+View.swift @@ -2,7 +2,7 @@ // SearchFeature+View.swift // // -// Created ErrorErrorError on 4/18/23. +// Created MochiTeam on 4/18/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Features/Search/SearchFeature.swift b/Sources/Features/Search/SearchFeature.swift index 7d948d5..49f90e0 100644 --- a/Sources/Features/Search/SearchFeature.swift +++ b/Sources/Features/Search/SearchFeature.swift @@ -2,7 +2,7 @@ // SearchFeature.swift // // -// Created ErrorErrorError on 4/18/23. +// Created MochiTeam on 4/18/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Features/Settings/Components/Logs.swift b/Sources/Features/Settings/Components/Logs.swift index 7bcffb3..15464d5 100644 --- a/Sources/Features/Settings/Components/Logs.swift +++ b/Sources/Features/Settings/Components/Logs.swift @@ -2,7 +2,7 @@ // Logs.swift // // -// Created by ErrorErrorError on 11/29/23. +// Created by MochiTeam on 11/29/23. // // @@ -155,6 +155,8 @@ extension Logs { public let store: StoreOf @Dependency(\.dateFormatter) var dateFormatter + + @SwiftUI.State var showCopyAlert = false @MainActor public init(store: StoreOf) { @@ -162,38 +164,52 @@ extension Logs { } @MainActor public var body: some SwiftUI.View { - ScrollView(.vertical) { - LazyVStack(spacing: 12) { - WithViewStore(store, observe: \.selected) { viewStore in - if viewStore.logsEmpty { - Text("No logs available.") - } else { - _VariadicView.Tree(Layout()) { - switch viewStore.state { - case let .system(events): - ForEach(events, id: \.timestamp) { event in - eventRow( - level: event.level.rawValue, - levelColor: event.level.color, - timeStamp: event.timestamp, - message: event.message - ) - } - case let .module(_, _, events): - ForEach(events, id: \.timestamp) { event in - eventRow( - level: event.level.rawValue, - levelColor: event.level.color, - timeStamp: event.timestamp, - message: event.body - ) + ZStack(alignment: .bottom) { + ScrollView(.vertical) { + LazyVStack(spacing: 12) { + WithViewStore(store, observe: \.selected) { viewStore in + if viewStore.logsEmpty { + Text("No logs available.") + } else { + _VariadicView.Tree(Layout()) { + switch viewStore.state { + case let .system(events): + ForEach(events, id: \.timestamp) { event in + eventRow( + level: event.level.rawValue, + levelColor: event.level.color, + timeStamp: event.timestamp, + message: event.message + ) + } + case let .module(_, _, events): + ForEach(events, id: \.timestamp) { event in + eventRow( + level: event.level.rawValue, + levelColor: event.level.color, + timeStamp: event.timestamp, + message: event.body + ) + } } } } } } + .padding() + } + if showCopyAlert { + VStack { + Text("Copied to clipboard!") + } + .transition(.opacity) + .padding() + .background(Theme.pastelOrange) + .foregroundColor(.white) + .clipShape(RoundedCorners(12)) + .shadow(radius: 10) + .offset(y: -20) } - .padding() } .moduleListsSheet( store.scope( @@ -300,6 +316,17 @@ extension Logs { Text(message) .font(.footnote) .frame(maxWidth: .infinity, alignment: .leading) + .onLongPressGesture { + UIPasteboard.general.setValue(message, forPasteboardType: "public.plain-text") + withAnimation { + showCopyAlert = true + } + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + withAnimation { + self.showCopyAlert = false + } + } + } } .frame(maxWidth: .infinity) } diff --git a/Sources/Features/Settings/Platforms/SettingsFeature+iOS.swift b/Sources/Features/Settings/Platforms/SettingsFeature+iOS.swift index f719d44..95fabbc 100644 --- a/Sources/Features/Settings/Platforms/SettingsFeature+iOS.swift +++ b/Sources/Features/Settings/Platforms/SettingsFeature+iOS.swift @@ -2,7 +2,7 @@ // SettingsFeature+iOS.swift // // -// Created by ErrorErrorError on 11/27/23. +// Created by MochiTeam on 11/27/23. // // @@ -44,9 +44,7 @@ private struct VersionView: View { Text( """ Design and developed by \ - [@errorerrorerror](https://errorerrorerror.dev) \ - & \ - [contributors](https://github.com/Mochi-Team/mochi/contributors) + [the community](https://mochisite.vercel.app) """ ) .multilineTextAlignment(.center) diff --git a/Sources/Features/Settings/Platforms/SettingsFeature+macOS.swift b/Sources/Features/Settings/Platforms/SettingsFeature+macOS.swift index 0607798..d3b974d 100644 --- a/Sources/Features/Settings/Platforms/SettingsFeature+macOS.swift +++ b/Sources/Features/Settings/Platforms/SettingsFeature+macOS.swift @@ -2,7 +2,7 @@ // SettingsFeature+macOS.swift // // -// Created by ErrorErrorError on 11/27/23. +// Created by MochiTeam on 11/27/23. // // diff --git a/Sources/Features/Settings/SettingsFeature+Reducer.swift b/Sources/Features/Settings/SettingsFeature+Reducer.swift index 57d76f5..09d36c6 100644 --- a/Sources/Features/Settings/SettingsFeature+Reducer.swift +++ b/Sources/Features/Settings/SettingsFeature+Reducer.swift @@ -2,7 +2,7 @@ // SettingsFeature+Reducer.swift // // -// Created ErrorErrorError on 4/8/23. +// Created MochiTeam on 4/8/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Features/Settings/SettingsFeature+View.swift b/Sources/Features/Settings/SettingsFeature+View.swift index b362939..7e69bd8 100644 --- a/Sources/Features/Settings/SettingsFeature+View.swift +++ b/Sources/Features/Settings/SettingsFeature+View.swift @@ -2,7 +2,7 @@ // SettingsFeature+View.swift // // -// Created ErrorErrorError on 4/8/23. +// Created MochiTeam on 4/8/23. // Copyright © 2023. All rights reserved. // @@ -46,16 +46,38 @@ struct GeneralView: View { @Environment(\.theme) var theme var body: some View { - EmptyView() -// SettingsGroup(title: showTitle ? SettingsFeature.Section.general.localized : "") { -// // TODO: Actually allow users to set which discover page to show on startup -// SettingRow(title: "Discover Page", accessory: { -// Toggle("", isOn: .constant(true)) -// .labelsHidden() -// .toggleStyle(.switch) -// .controlSize(.small) -// }) -// } + WithViewStore(store, observe: \.`self`) { viewStore in + SettingsGroup(title: showTitle ? SettingsFeature.Section.general.localized : "") { + SettingRow(title: "Fast Forward skip amount", accessory: { + HStack { + TextField("", value: viewStore.$userSettings.fastForwardAmount, format: .number) + Text("s").foregroundStyle(.secondary) + } + .frame(maxWidth: 50) + .padding(8) + .background(Color.secondarySystemBackground) + .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(.quaternary, lineWidth: 0.5) + ) + }) + SettingRow(title: "Fast Backward skip amount", accessory: { + HStack { + TextField("", value: viewStore.$userSettings.fastBackwardAmount, format: .number) + Text("s").foregroundStyle(.secondary) + } + .frame(maxWidth: 50) + .padding(8) + .background(Color.secondarySystemBackground) + .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(.quaternary, lineWidth: 0.5) + ) + }) + } + } } } diff --git a/Sources/Features/Settings/SettingsFeature.swift b/Sources/Features/Settings/SettingsFeature.swift index fd7bc0e..4052e81 100644 --- a/Sources/Features/Settings/SettingsFeature.swift +++ b/Sources/Features/Settings/SettingsFeature.swift @@ -2,7 +2,7 @@ // SettingsFeature.swift // // -// Created ErrorErrorError on 4/8/23. +// Created MochiTeam on 4/8/23. // Copyright © 2023. All rights reserved. // diff --git a/Sources/Features/VideoPlayer/Components/ProgressBar.swift b/Sources/Features/VideoPlayer/Components/ProgressBar.swift index 4c87aa2..1f7ff6e 100644 --- a/Sources/Features/VideoPlayer/Components/ProgressBar.swift +++ b/Sources/Features/VideoPlayer/Components/ProgressBar.swift @@ -2,7 +2,7 @@ // ProgressBar.swift // // -// Created by ErrorErrorError on 11/22/23. +// Created by MochiTeam on 11/22/23. // // @@ -59,8 +59,9 @@ struct ProgressBar: View { } return false } - + private static let defaultEmptyTime = "--:--" + private static let defaultLiveVideo = "LIVE" private static let defaultZeroTime = "00:00" init(store: Store) { @@ -116,27 +117,47 @@ struct ProgressBar: View { .frame(maxWidth: .infinity) .frame(height: 24) - Text("\(progressDisplayTime) / \(durationDisplayTime)") - .font(.caption.monospacedDigit()) - .foregroundColor(.white) + if viewState.state?.totalDuration.isInfinite == true { + Text("\(durationDisplayTime)") + .font(.caption.monospacedDigit().weight(.bold)) + .foregroundColor(.red) + } + else if viewState.state?.totalDuration.isNaN == true { + Text("\(durationDisplayTime)") + .font(.caption.monospacedDigit().weight(.bold)) + .foregroundColor(.red) + } + else{ + Text("\(progressDisplayTime) / \(durationDisplayTime)") + .font(.caption.monospacedDigit()) + .foregroundColor(.white) + } + + } .disabled(!canUseControls) .preferredColorScheme(.dark) } - private var progressDisplayTime: String { - if canUseControls { - formatter.playbackTimestamp(progress * (viewState.state?.totalDuration ?? .zero)) ?? Self.defaultZeroTime - } else { - Self.defaultEmptyTime + private var progressDisplayTime: String { + if canUseControls { + formatter.playbackTimestamp(progress * (viewState.state?.totalDuration ?? .zero)) ?? Self.defaultZeroTime + } else { + Self.defaultEmptyTime + } } - } private var durationDisplayTime: String { - if canUseControls { - formatter.playbackTimestamp(viewState.state?.totalDuration ?? .zero) ?? Self.defaultZeroTime - } else { - Self.defaultEmptyTime - } + if canUseControls { + formatter.playbackTimestamp(viewState.state?.totalDuration ?? .zero) ?? Self.defaultZeroTime + } else if viewState.state?.totalDuration.isInfinite == true { + Self.defaultLiveVideo + } + else if viewState.state?.totalDuration.isNaN == true { + Self.defaultLiveVideo + } + else { + Self.defaultEmptyTime + } } } diff --git a/Sources/Features/VideoPlayer/Extensions/DateComponentsFormatter+.swift b/Sources/Features/VideoPlayer/Extensions/DateComponentsFormatter+.swift index 6b38df9..9204f9f 100644 --- a/Sources/Features/VideoPlayer/Extensions/DateComponentsFormatter+.swift +++ b/Sources/Features/VideoPlayer/Extensions/DateComponentsFormatter+.swift @@ -2,7 +2,7 @@ // DateComponentsFormatter+.swift // // -// Created by ErrorErrorError on 11/22/23. +// Created by MochiTeam on 11/22/23. // // diff --git a/Sources/Features/VideoPlayer/Models.swift b/Sources/Features/VideoPlayer/Models.swift index 005d2f9..6173990 100644 --- a/Sources/Features/VideoPlayer/Models.swift +++ b/Sources/Features/VideoPlayer/Models.swift @@ -2,7 +2,7 @@ // Models.swift // // -// Created by ErrorErrorError on 11/22/23. +// Created by MochiTeam on 11/22/23. // // @@ -13,7 +13,8 @@ public struct PlayerSettings: Equatable, Sendable { public var speed = 1.0 // In Seconds - public var skipTime = 15.0 + public var skipForwardTime = UserDefaults.standard.double(forKey: "userSettings.fastForwardAmount") + public var skipBackwardTime = UserDefaults.standard.double(forKey: "userSettings.fastBackwardAmount") public init(speed: Double = 1.0) { self.speed = speed diff --git a/Sources/Features/VideoPlayer/VideoPlayerFeature+Reducer.swift b/Sources/Features/VideoPlayer/VideoPlayerFeature+Reducer.swift index b6c67af..bc33ff9 100644 --- a/Sources/Features/VideoPlayer/VideoPlayerFeature+Reducer.swift +++ b/Sources/Features/VideoPlayer/VideoPlayerFeature+Reducer.swift @@ -2,7 +2,7 @@ // VideoPlayerFeature+Reducer.swift // // -// Created ErrorErrorError on 5/26/23. +// Created MochiTeam on 5/26/23. // Copyright © 2023. All rights reserved. // @@ -14,6 +14,7 @@ import ModuleClient import PlayerClient import PlaylistHistoryClient import SharedModels +import FileClient // MARK: - Cancellables @@ -21,6 +22,7 @@ private enum Cancellables: Hashable, CaseIterable { case delayCloseTab case fetchingSources case fetchingServer + case updateTimestamp } // MARK: - VideoPlayerFeature + Reducer @@ -42,10 +44,18 @@ extension VideoPlayerFeature: Reducer { state.content.fetchContent(.page(state.selected.groupId, state.selected.variantId, state.selected.pageId)) .map { .internal(.content($0)) }, .run { send in - for await status in playerClient.observe() { - if let progress = status.playback?.progress { - try? await playlistHistoryClient.updateTimestamp(.init(repoId: repoModule.repoId.absoluteString, moduleId: repoModule.moduleId.rawValue, playlistId: groupId), progress) + try await withTaskCancellation(id: Cancellables.updateTimestamp) { + while (true) { + try await Task.sleep(nanoseconds: 1_000_000_000) + let status = playerClient.get() + if let progress = status.playback?.progress { + try? await playlistHistoryClient.updateTimestamp(.init(repoId: repoModule.repoId.absoluteString, moduleId: repoModule.moduleId.rawValue, playlistId: groupId), progress) + } } + } + }, + .run { send in + for await status in playerClient.observe() { await send(.internal(.playerStatusUpdate(status))) } } @@ -116,10 +126,9 @@ extension VideoPlayerFeature: Reducer { } case .view(.didSkipForward): - let skipTime = state.playerSettings.skipTime // In seconds let currentProgress = state.player.playback?.progress ?? .zero let totalDuration = state.player.playback?.totalDuration ?? 1 - let newProgress = min(1.0, max(0, currentProgress + (skipTime / totalDuration))) + let newProgress = min(1.0, max(0, currentProgress + (state.playerSettings.skipForwardTime / totalDuration))) return .merge( state.delayDismissOverlayIfNeeded(), .run { _ in @@ -128,10 +137,9 @@ extension VideoPlayerFeature: Reducer { ) case .view(.didSkipBackwards): - let skipTime = state.playerSettings.skipTime // In seconds let currentProgress = state.player.playback?.progress ?? .zero let totalDuration = state.player.playback?.totalDuration ?? 1 - let newProgress = min(1.0, max(0, currentProgress - (skipTime / totalDuration))) + let newProgress = min(1.0, max(0, currentProgress - (state.playerSettings.skipBackwardTime / totalDuration))) return .merge( state.delayDismissOverlayIfNeeded(), .run { _ in @@ -312,7 +320,6 @@ extension VideoPlayerFeature.State { fetchSourcesIfNecessary(), .run { _ in await playerClient.clear() - try? await playlistHistoryClient.updateTimestamp(.init(repoId: repoModule.repoId.absoluteString, moduleId: repoModule.moduleId.rawValue, playlistId: groupId.rawValue), 0) } ) } @@ -413,14 +420,24 @@ extension VideoPlayerFeature.State { public mutating func fetchSourcesIfNecessary(forced: Bool = false) -> Effect { @Dependency(\.moduleClient) var moduleClient + @Dependency(\.fileClient) var fileClient let repoModuleId = content.repoModuleId let playlist = playlist let episodeId = selected.itemId - + let prefersOffline = prefersOffline + if forced || !loadables[episodeId: episodeId].hasInitialized { loadables.update(with: episodeId, response: .loading) return .run { send in + if (prefersOffline) { + if let directory = try? fileClient.retrieveLibraryDirectory(root: .downloaded, playlist: playlist.id.rawValue, episode: episodeId.rawValue).appendingPathComponent("metadata.json") { + if let sources = try? JSONDecoder().decode(EpisodeMetadata.self, from: FileManager.default.contents(atPath: directory.path ?? "") ?? .init()) { + return await send(.internal(.sourcesResponse(episodeId, .loaded([sources.source])))) + } + } + } + try await withTaskCancellation(id: Cancellables.fetchingSources, cancelInFlight: true) { let value = try await moduleClient.withModule(id: repoModuleId) { module in try await module.playlistEpisodeSources( @@ -443,12 +460,14 @@ extension VideoPlayerFeature.State { public mutating func fetchServerIfNecessary(forced: Bool = false) -> Effect { @Dependency(\.moduleClient) var moduleClient + @Dependency(\.fileClient) var fileClient let repoModuleId = content.repoModuleId let playlist = playlist let episodeId = selected.itemId let sourceId = selected.sourceId let serverId = selected.serverId + let prefersOffline = prefersOffline guard let sourceId else { return .none @@ -460,6 +479,18 @@ extension VideoPlayerFeature.State { if forced || !loadables[serverId: serverId].hasInitialized { loadables.update(with: serverId, response: .loading) return .run { send in + if (prefersOffline) { + if let directory = try? fileClient.retrieveLibraryDirectory(root: .downloaded, playlist: playlist.id.rawValue, episode: episodeId.rawValue) { + if let sources = try? JSONDecoder().decode(EpisodeMetadata.self, from: FileManager.default.contents(atPath: directory.appendingPathComponent("metadata.json").path) ?? .init()) { + let linkPath = directory.appendingPathComponent("data").appendingPathExtension("movpkg") + return await send(.internal(.serverResponse(serverId, .loaded(.init(links: [.init(url: linkPath, quality: sources.link.quality, format: sources.link.format)], subtitles: sources.subtitles.map{ + var newValue = $0 + newValue.url = directory.appendingPathComponent(newValue.url.lastPathComponent) + return newValue + }, headers: [:], skipTimes: sources.skipTimes))))) + } + } + } try await withTaskCancellation(id: Cancellables.fetchingServer, cancelInFlight: true) { let value = try await moduleClient.withModule(id: repoModuleId) { module in try await module.playlistEpisodeServer( diff --git a/Sources/Features/VideoPlayer/VideoPlayerFeature+View.swift b/Sources/Features/VideoPlayer/VideoPlayerFeature+View.swift index 3705104..776ebcb 100644 --- a/Sources/Features/VideoPlayer/VideoPlayerFeature+View.swift +++ b/Sources/Features/VideoPlayer/VideoPlayerFeature+View.swift @@ -2,7 +2,7 @@ // VideoPlayerFeature+View.swift // // -// Created by ErrorErrorError on 11/23/23. +// Created by MochiTeam on 11/23/23. // // @@ -588,8 +588,7 @@ extension VideoPlayerFeature.View { contentType: .video, selectedGroupId: viewStore.groupId, selectedVariantId: viewStore.variantId, - selectedPageId: viewStore.pageId, - selectedItemId: viewStore.itemId + selectedPageId: viewStore.pageId ) } } @@ -808,21 +807,3 @@ extension VideoPlayerFeature.View { } } -#Preview { - VideoPlayerFeature.View( - store: .init( - initialState: .init( - repoModuleId: Repo().id(.init("")), - playlist: .empty, - loadables: .init(), - group: .init(""), - variant: .init(""), - page: .init(""), - episodeId: .init(""), - overlay: .tools - ), - reducer: { EmptyReducer() } - ) - ) - .previewInterfaceOrientation(.landscapeRight) -} diff --git a/Sources/Features/VideoPlayer/VideoPlayerFeature.swift b/Sources/Features/VideoPlayer/VideoPlayerFeature.swift index e54994a..1523a50 100644 --- a/Sources/Features/VideoPlayer/VideoPlayerFeature.swift +++ b/Sources/Features/VideoPlayer/VideoPlayerFeature.swift @@ -2,7 +2,7 @@ // VideoPlayerFeature.swift // // -// Created ErrorErrorError on 5/26/23. +// Created MochiTeam on 5/26/23. // Copyright © 2023. All rights reserved. // @@ -67,6 +67,7 @@ public struct VideoPlayerFeature: Feature { public var overlay: Overlay? public var player: PlayerClient.Status public var playerSettings: PlayerSettings + public var prefersOffline: Bool public init( repoModuleId: RepoModuleID, @@ -77,7 +78,8 @@ public struct VideoPlayerFeature: Feature { page: PagingID, episodeId: Playlist.Item.ID, overlay: Overlay? = .tools, - playerSettings: PlayerSettings = .init() + playerSettings: PlayerSettings = .init(), + prefersOffline: Bool? = false ) { @Dependency(\.playerClient.get) var status @@ -91,7 +93,8 @@ public struct VideoPlayerFeature: Feature { episodeId: episodeId, overlay: overlay, player: status(), - playerSettings: playerSettings + playerSettings: playerSettings, + prefersOffline: prefersOffline ?? false ) } @@ -105,12 +108,23 @@ public struct VideoPlayerFeature: Feature { episodeId: Playlist.Item.ID, overlay: Overlay? = .tools, player: PlayerClient.Status, - playerSettings: PlayerSettings = .init() + playerSettings: PlayerSettings = .init(), + prefersOffline: Bool ) { - self.content = .init( - repoModuleId: repoModuleId, - playlist: playlist - ) + if (prefersOffline) { + @Dependency(\.fileClient) var fileClient + let metadata = try? JSONDecoder().decode(PlaylistCache.self, from: try fileClient.retrieveLibraryMetadata(root: .playlistCache, playlist: playlist.id.rawValue) ?? .init()) + self.content = .init( + repoModuleId: repoModuleId, + playlist: playlist, + cachedGroups: metadata?.groups + ) + } else { + self.content = .init( + repoModuleId: repoModuleId, + playlist: playlist + ) + } self.loadables = loadables self.selected = .init( groupId: group, @@ -121,6 +135,7 @@ public struct VideoPlayerFeature: Feature { self.overlay = overlay self.player = player self.playerSettings = playerSettings + self.prefersOffline = prefersOffline } } diff --git a/Sources/Features/VideoPlayer/iOS/VideoPlayerFeature+iOS.swift b/Sources/Features/VideoPlayer/iOS/VideoPlayerFeature+iOS.swift index 4a1cddf..59e010d 100644 --- a/Sources/Features/VideoPlayer/iOS/VideoPlayerFeature+iOS.swift +++ b/Sources/Features/VideoPlayer/iOS/VideoPlayerFeature+iOS.swift @@ -2,7 +2,7 @@ // VideoPlayerFeature+iOS.swift // // -// Created by ErrorErrorError on 11/23/23. +// Created by MochiTeam on 11/23/23. // // diff --git a/Sources/Features/VideoPlayer/macOS/VideoPlayerFeature+macOS.swift b/Sources/Features/VideoPlayer/macOS/VideoPlayerFeature+macOS.swift index 8358af6..1fac2d8 100644 --- a/Sources/Features/VideoPlayer/macOS/VideoPlayerFeature+macOS.swift +++ b/Sources/Features/VideoPlayer/macOS/VideoPlayerFeature+macOS.swift @@ -2,7 +2,7 @@ // VideoPlayerFeature+macOS.swift // // -// Created by ErrorErrorError on 11/23/23. +// Created by MochiTeam on 11/23/23. // // diff --git a/Sources/Macros/CoreDBMacros/AttributeMacro.swift b/Sources/Macros/CoreDBMacros/AttributeMacro.swift index 0b747c7..3b7f0a4 100644 --- a/Sources/Macros/CoreDBMacros/AttributeMacro.swift +++ b/Sources/Macros/CoreDBMacros/AttributeMacro.swift @@ -2,7 +2,7 @@ // AttributeMacro.swift // // -// Created by ErrorErrorError on 12/29/23. +// Created by MochiTeam on 12/29/23. // // diff --git a/Sources/Macros/CoreDBMacros/EntityMacro.swift b/Sources/Macros/CoreDBMacros/EntityMacro.swift index 4343506..9e9570f 100644 --- a/Sources/Macros/CoreDBMacros/EntityMacro.swift +++ b/Sources/Macros/CoreDBMacros/EntityMacro.swift @@ -2,7 +2,7 @@ // EntityMacro.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // diff --git a/Sources/Macros/CoreDBMacros/Helpers.swift b/Sources/Macros/CoreDBMacros/Helpers.swift index 94169d3..0c4e787 100644 --- a/Sources/Macros/CoreDBMacros/Helpers.swift +++ b/Sources/Macros/CoreDBMacros/Helpers.swift @@ -2,7 +2,7 @@ // Helpers.swift // // -// Created by ErrorErrorError on 12/29/23. +// Created by MochiTeam on 12/29/23. // // diff --git a/Sources/Macros/CoreDBMacros/Plugins.swift b/Sources/Macros/CoreDBMacros/Plugins.swift index 34d5d7e..5c61120 100644 --- a/Sources/Macros/CoreDBMacros/Plugins.swift +++ b/Sources/Macros/CoreDBMacros/Plugins.swift @@ -2,7 +2,7 @@ // Plugins.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // diff --git a/Sources/Macros/CoreDBMacros/RelationMacro.swift b/Sources/Macros/CoreDBMacros/RelationMacro.swift index 3914f6e..995d4b3 100644 --- a/Sources/Macros/CoreDBMacros/RelationMacro.swift +++ b/Sources/Macros/CoreDBMacros/RelationMacro.swift @@ -2,7 +2,7 @@ // RelationMacro.swift // // -// Created by ErrorErrorError on 12/29/23. +// Created by MochiTeam on 12/29/23. // // diff --git a/Sources/Shared/Architecture/Dependencies/Dependencies+DateComponentsFormatter.swift b/Sources/Shared/Architecture/Dependencies/Dependencies+DateComponentsFormatter.swift index 5be73b6..2f2aae5 100644 --- a/Sources/Shared/Architecture/Dependencies/Dependencies+DateComponentsFormatter.swift +++ b/Sources/Shared/Architecture/Dependencies/Dependencies+DateComponentsFormatter.swift @@ -2,7 +2,7 @@ // Dependencies+DateComponentsFormatter.swift // // -// Created by ErrorErrorError on 6/11/23. +// Created by MochiTeam on 6/11/23. // // diff --git a/Sources/Shared/Architecture/Dependencies/Dependencies+DateFormater.swift b/Sources/Shared/Architecture/Dependencies/Dependencies+DateFormater.swift index c91099e..9ce096e 100644 --- a/Sources/Shared/Architecture/Dependencies/Dependencies+DateFormater.swift +++ b/Sources/Shared/Architecture/Dependencies/Dependencies+DateFormater.swift @@ -2,7 +2,7 @@ // Dependencies+DateFormater.swift // // -// Created by ErrorErrorError on 5/2/23. +// Created by MochiTeam on 5/2/23. // // diff --git a/Sources/Shared/Architecture/Dependencies/Dependencies+NumberFormatter.swift b/Sources/Shared/Architecture/Dependencies/Dependencies+NumberFormatter.swift index 5332f20..8588271 100644 --- a/Sources/Shared/Architecture/Dependencies/Dependencies+NumberFormatter.swift +++ b/Sources/Shared/Architecture/Dependencies/Dependencies+NumberFormatter.swift @@ -2,7 +2,7 @@ // Dependencies+NumberFormatter.swift // // -// Created by ErrorErrorError on 5/2/23. +// Created by MochiTeam on 5/2/23. // // diff --git a/Sources/Shared/Architecture/Exported.swift b/Sources/Shared/Architecture/Exported.swift index c3a78d3..b4bb471 100644 --- a/Sources/Shared/Architecture/Exported.swift +++ b/Sources/Shared/Architecture/Exported.swift @@ -2,7 +2,7 @@ // Exported.swift // // -// Created by ErrorErrorError on 4/21/23. +// Created by MochiTeam on 4/21/23. // // diff --git a/Sources/Shared/Architecture/Feature.swift b/Sources/Shared/Architecture/Feature.swift index cd53516..ddcf0c4 100644 --- a/Sources/Shared/Architecture/Feature.swift +++ b/Sources/Shared/Architecture/Feature.swift @@ -2,7 +2,7 @@ // Feature.swift // // -// Created by ErrorErrorError on 4/5/23. +// Created by MochiTeam on 4/5/23. // // diff --git a/Sources/Shared/Architecture/TCA+Extensions.swift b/Sources/Shared/Architecture/TCA+Extensions.swift index 9554a41..c29ddb9 100644 --- a/Sources/Shared/Architecture/TCA+Extensions.swift +++ b/Sources/Shared/Architecture/TCA+Extensions.swift @@ -2,7 +2,7 @@ // TCA+Extensions.swift // // -// Created by ErrorErrorError on 4/7/23. +// Created by MochiTeam on 4/7/23. // // diff --git a/Sources/Shared/Architecture/Utils/Binding+Equatable.swift b/Sources/Shared/Architecture/Utils/Binding+Equatable.swift index f8c5873..426695e 100644 --- a/Sources/Shared/Architecture/Utils/Binding+Equatable.swift +++ b/Sources/Shared/Architecture/Utils/Binding+Equatable.swift @@ -2,7 +2,7 @@ // Binding+Equatable.swift // // -// Created by ErrorErrorError on 5/20/23. +// Created by MochiTeam on 5/20/23. // // diff --git a/Sources/Shared/Architecture/Utils/SelectableState.swift b/Sources/Shared/Architecture/Utils/SelectableState.swift index b0ab641..452238b 100644 --- a/Sources/Shared/Architecture/Utils/SelectableState.swift +++ b/Sources/Shared/Architecture/Utils/SelectableState.swift @@ -2,7 +2,7 @@ // SelectableState.swift // // -// Created by ErrorErrorError on 5/11/23. +// Created by MochiTeam on 5/11/23. // // // diff --git a/Sources/Shared/Architecture/Utils/Swizzle.swift b/Sources/Shared/Architecture/Utils/Swizzle.swift index abf5419..e987787 100644 --- a/Sources/Shared/Architecture/Utils/Swizzle.swift +++ b/Sources/Shared/Architecture/Utils/Swizzle.swift @@ -2,7 +2,7 @@ // Swizzle.swift // // -// Created by ErrorErrorError on 12/2/23. +// Created by MochiTeam on 12/2/23. // // Source: https://gist.github.com/Amzd/01e1f69ecbc4c82c8586dcd292b1d30d diff --git a/Sources/Shared/CoreDB/Extension/EntityDescription.swift b/Sources/Shared/CoreDB/Extension/EntityDescription.swift index 4d8292e..bfe29ef 100644 --- a/Sources/Shared/CoreDB/Extension/EntityDescription.swift +++ b/Sources/Shared/CoreDB/Extension/EntityDescription.swift @@ -2,7 +2,7 @@ // EntityDescription.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // diff --git a/Sources/Shared/CoreDB/Extension/NSManagedObject+.swift b/Sources/Shared/CoreDB/Extension/NSManagedObject+.swift index 5512d2c..7fecf72 100644 --- a/Sources/Shared/CoreDB/Extension/NSManagedObject+.swift +++ b/Sources/Shared/CoreDB/Extension/NSManagedObject+.swift @@ -2,7 +2,7 @@ // NSManagedObject+.swift // // -// Created by ErrorErrorError on 5/15/23. +// Created by MochiTeam on 5/15/23. // // diff --git a/Sources/Shared/CoreDB/Extension/NSManagedObjectContext+.swift b/Sources/Shared/CoreDB/Extension/NSManagedObjectContext+.swift index aacafcc..d42e3f9 100644 --- a/Sources/Shared/CoreDB/Extension/NSManagedObjectContext+.swift +++ b/Sources/Shared/CoreDB/Extension/NSManagedObjectContext+.swift @@ -2,7 +2,7 @@ // NSManagedObjectContext+.swift // // -// Created by ErrorErrorError on 5/3/23. +// Created by MochiTeam on 5/3/23. // // diff --git a/Sources/Shared/CoreDB/Extension/NSManagedObjectModel+.swift b/Sources/Shared/CoreDB/Extension/NSManagedObjectModel+.swift index a15374f..8ae0b04 100644 --- a/Sources/Shared/CoreDB/Extension/NSManagedObjectModel+.swift +++ b/Sources/Shared/CoreDB/Extension/NSManagedObjectModel+.swift @@ -2,7 +2,7 @@ // NSManagedObjectModel+.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // diff --git a/Sources/Shared/CoreDB/Extension/NSPersistentContainer+.swift b/Sources/Shared/CoreDB/Extension/NSPersistentContainer+.swift index eec693a..264238c 100644 --- a/Sources/Shared/CoreDB/Extension/NSPersistentContainer+.swift +++ b/Sources/Shared/CoreDB/Extension/NSPersistentContainer+.swift @@ -2,7 +2,7 @@ // NSPersistentContainer+.swift // // -// Created by ErrorErrorError on 5/15/23. +// Created by MochiTeam on 5/15/23. // // diff --git a/Sources/Shared/CoreDB/Extension/NSPersistentStore+.swift b/Sources/Shared/CoreDB/Extension/NSPersistentStore+.swift index e105bc6..17e0992 100644 --- a/Sources/Shared/CoreDB/Extension/NSPersistentStore+.swift +++ b/Sources/Shared/CoreDB/Extension/NSPersistentStore+.swift @@ -2,7 +2,7 @@ // NSPersistentStore+.swift // // -// Created by ErrorErrorError on 5/16/23. +// Created by MochiTeam on 5/16/23. // // diff --git a/Sources/Shared/CoreDB/Extension/NSPropertyDescriptors+.swift b/Sources/Shared/CoreDB/Extension/NSPropertyDescriptors+.swift index 4f3c21c..9ea894c 100644 --- a/Sources/Shared/CoreDB/Extension/NSPropertyDescriptors+.swift +++ b/Sources/Shared/CoreDB/Extension/NSPropertyDescriptors+.swift @@ -2,7 +2,7 @@ // NSPropertyDescriptors+.swift // // -// Created by ErrorErrorError on 12/30/23. +// Created by MochiTeam on 12/30/23. // // diff --git a/Sources/Shared/CoreDB/Macros.swift b/Sources/Shared/CoreDB/Macros.swift index 3e4275c..19dce82 100644 --- a/Sources/Shared/CoreDB/Macros.swift +++ b/Sources/Shared/CoreDB/Macros.swift @@ -2,7 +2,7 @@ // Macros.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // diff --git a/Sources/Shared/CoreDB/PersistentCoreDB.swift b/Sources/Shared/CoreDB/PersistentCoreDB.swift index 12a870e..cc4c06c 100644 --- a/Sources/Shared/CoreDB/PersistentCoreDB.swift +++ b/Sources/Shared/CoreDB/PersistentCoreDB.swift @@ -2,7 +2,7 @@ // PersistentCoreDB.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // diff --git a/Sources/Shared/CoreDB/Properties/Attribute.swift b/Sources/Shared/CoreDB/Properties/Attribute.swift index 8100c4a..2a44a90 100644 --- a/Sources/Shared/CoreDB/Properties/Attribute.swift +++ b/Sources/Shared/CoreDB/Properties/Attribute.swift @@ -2,7 +2,7 @@ // Attribute.swift // // -// Created by ErrorErrorError on 5/17/23. +// Created by MochiTeam on 5/17/23. // // diff --git a/Sources/Shared/CoreDB/Properties/Property.swift b/Sources/Shared/CoreDB/Properties/Property.swift index 0bfeb75..345e8db 100644 --- a/Sources/Shared/CoreDB/Properties/Property.swift +++ b/Sources/Shared/CoreDB/Properties/Property.swift @@ -2,7 +2,7 @@ // Property.swift // // -// Created by ErrorErrorError on 5/15/23. +// Created by MochiTeam on 5/15/23. // // diff --git a/Sources/Shared/CoreDB/Properties/Relation.swift b/Sources/Shared/CoreDB/Properties/Relation.swift index 35b2b9b..b0191ed 100644 --- a/Sources/Shared/CoreDB/Properties/Relation.swift +++ b/Sources/Shared/CoreDB/Properties/Relation.swift @@ -2,7 +2,7 @@ // Relation.swift // // -// Created by ErrorErrorError on 5/15/23. +// Created by MochiTeam on 5/15/23. // // diff --git a/Sources/Shared/CoreDB/Supporting Files/Cast.swift b/Sources/Shared/CoreDB/Supporting Files/Cast.swift index df14cb4..0954c2f 100644 --- a/Sources/Shared/CoreDB/Supporting Files/Cast.swift +++ b/Sources/Shared/CoreDB/Supporting Files/Cast.swift @@ -2,7 +2,7 @@ // Cast.swift // // -// Created by ErrorErrorError on 5/16/23. +// Created by MochiTeam on 5/16/23. // // diff --git a/Sources/Shared/CoreDB/Supporting Files/Entity.swift b/Sources/Shared/CoreDB/Supporting Files/Entity.swift index 3ca0752..6fbb725 100644 --- a/Sources/Shared/CoreDB/Supporting Files/Entity.swift +++ b/Sources/Shared/CoreDB/Supporting Files/Entity.swift @@ -2,7 +2,7 @@ // Entity.swift // // -// Created by ErrorErrorError on 9/12/23. +// Created by MochiTeam on 9/12/23. // // diff --git a/Sources/Shared/CoreDB/Supporting Files/EntityID.swift b/Sources/Shared/CoreDB/Supporting Files/EntityID.swift index 3173482..98e7181 100644 --- a/Sources/Shared/CoreDB/Supporting Files/EntityID.swift +++ b/Sources/Shared/CoreDB/Supporting Files/EntityID.swift @@ -2,7 +2,7 @@ // EntityID.swift // // -// Created by ErrorErrorError on 9/18/23. +// Created by MochiTeam on 9/18/23. // // diff --git a/Sources/Shared/CoreDB/Supporting Files/Optional.swift b/Sources/Shared/CoreDB/Supporting Files/Optional.swift index e947e3b..0a20c0e 100644 --- a/Sources/Shared/CoreDB/Supporting Files/Optional.swift +++ b/Sources/Shared/CoreDB/Supporting Files/Optional.swift @@ -2,7 +2,7 @@ // Optional.swift // // -// Created by ErrorErrorError on 5/18/23. +// Created by MochiTeam on 5/18/23. // // diff --git a/Sources/Shared/CoreDB/Supporting Files/Request.swift b/Sources/Shared/CoreDB/Supporting Files/Request.swift index 3a7b7cc..fc3a179 100644 --- a/Sources/Shared/CoreDB/Supporting Files/Request.swift +++ b/Sources/Shared/CoreDB/Supporting Files/Request.swift @@ -1,7 +1,7 @@ // Request.swift // mochi // -// Created by ErrorErrorError on 11/16/22. +// Created by MochiTeam on 11/16/22. // // Modified version of https://github.com/prisma-ai/Sworm diff --git a/Sources/Shared/CoreDB/Supporting Files/Schema.swift b/Sources/Shared/CoreDB/Supporting Files/Schema.swift index e136448..6d6e4f0 100644 --- a/Sources/Shared/CoreDB/Supporting Files/Schema.swift +++ b/Sources/Shared/CoreDB/Supporting Files/Schema.swift @@ -2,7 +2,7 @@ // Schema.swift // // -// Created by ErrorErrorError on 12/28/23. +// Created by MochiTeam on 12/28/23. // // diff --git a/Sources/Shared/CoreDB/Supporting Files/TransformableValue.swift b/Sources/Shared/CoreDB/Supporting Files/TransformableValue.swift index ce86d1e..0e30163 100644 --- a/Sources/Shared/CoreDB/Supporting Files/TransformableValue.swift +++ b/Sources/Shared/CoreDB/Supporting Files/TransformableValue.swift @@ -2,7 +2,7 @@ // TransformableValue.swift // // -// Created by ErrorErrorError on 5/3/23. +// Created by MochiTeam on 5/3/23. // // diff --git a/Sources/Shared/FoundationHelpers/Array+ID.swift b/Sources/Shared/FoundationHelpers/Array+ID.swift index e575f0f..615e199 100644 --- a/Sources/Shared/FoundationHelpers/Array+ID.swift +++ b/Sources/Shared/FoundationHelpers/Array+ID.swift @@ -2,7 +2,7 @@ // Array+ID.swift // // -// Created by ErrorErrorError on 5/11/23. +// Created by MochiTeam on 5/11/23. // // diff --git a/Sources/Shared/FoundationHelpers/Equatable+.swift b/Sources/Shared/FoundationHelpers/Equatable+.swift index f7ecee7..a54017a 100644 --- a/Sources/Shared/FoundationHelpers/Equatable+.swift +++ b/Sources/Shared/FoundationHelpers/Equatable+.swift @@ -2,7 +2,7 @@ // Equatable+.swift // // -// Created by ErrorErrorError on 8/14/23. +// Created by MochiTeam on 8/14/23. // // diff --git a/Sources/Shared/FoundationHelpers/KeyPath+.swift b/Sources/Shared/FoundationHelpers/KeyPath+.swift index 92a5ff0..1ff1670 100644 --- a/Sources/Shared/FoundationHelpers/KeyPath+.swift +++ b/Sources/Shared/FoundationHelpers/KeyPath+.swift @@ -2,7 +2,7 @@ // KeyPath+.swift // // -// Created by ErrorErrorError on 4/21/23. +// Created by MochiTeam on 4/21/23. // // diff --git a/Sources/Shared/FoundationHelpers/URL+.swift b/Sources/Shared/FoundationHelpers/URL+.swift index 653ea92..bfdd7a8 100644 --- a/Sources/Shared/FoundationHelpers/URL+.swift +++ b/Sources/Shared/FoundationHelpers/URL+.swift @@ -2,7 +2,7 @@ // URL+.swift // // -// Created by ErrorErrorError on 11/12/23. +// Created by MochiTeam on 11/12/23. // // diff --git a/Sources/Shared/JSValueCoder/JSVEnumAssociatedCodable.swift b/Sources/Shared/JSValueCoder/JSVEnumAssociatedCodable.swift index f4d978f..d79e6bf 100644 --- a/Sources/Shared/JSValueCoder/JSVEnumAssociatedCodable.swift +++ b/Sources/Shared/JSValueCoder/JSVEnumAssociatedCodable.swift @@ -2,7 +2,7 @@ // JSVEnumAssociatedCodable.swift // // -// Created by ErrorErrorError on 11/11/23. +// Created by MochiTeam on 11/11/23. // // diff --git a/Sources/Shared/JSValueCoder/JSValueCodingKey.swift b/Sources/Shared/JSValueCoder/JSValueCodingKey.swift index 808fe9e..1424161 100644 --- a/Sources/Shared/JSValueCoder/JSValueCodingKey.swift +++ b/Sources/Shared/JSValueCoder/JSValueCodingKey.swift @@ -2,7 +2,7 @@ // JSValueCodingKey.swift // // -// Created by ErrorErrorError on 11/5/23. +// Created by MochiTeam on 11/5/23. // // diff --git a/Sources/Shared/JSValueCoder/JSValueDecoder.swift b/Sources/Shared/JSValueCoder/JSValueDecoder.swift index 52802ae..cdc50d5 100644 --- a/Sources/Shared/JSValueCoder/JSValueDecoder.swift +++ b/Sources/Shared/JSValueCoder/JSValueDecoder.swift @@ -2,7 +2,7 @@ // JSValueDecoder.swift // // -// Created by ErrorErrorError on 11/4/23. +// Created by MochiTeam on 11/4/23. // // from https://github.com/theolampert/JSValueCoder diff --git a/Sources/Shared/JSValueCoder/JSValueEncoder.swift b/Sources/Shared/JSValueCoder/JSValueEncoder.swift index 94228dd..8f5e9c8 100644 --- a/Sources/Shared/JSValueCoder/JSValueEncoder.swift +++ b/Sources/Shared/JSValueCoder/JSValueEncoder.swift @@ -2,7 +2,7 @@ // JSValueEncoder.swift // // -// Created by ErrorErrorError on 11/4/23. +// Created by MochiTeam on 11/4/23. // // from https://github.com/theolampert/JSValueCoder diff --git a/Sources/Shared/SharedModels/EpisodeMetadata.swift b/Sources/Shared/SharedModels/EpisodeMetadata.swift new file mode 100644 index 0000000..ea84f06 --- /dev/null +++ b/Sources/Shared/SharedModels/EpisodeMetadata.swift @@ -0,0 +1,28 @@ +// +// EpisodeMetadata.swift +// +// +// Created by MochiTeam on 20.04.2024. +// + +import Foundation + +public struct EpisodeMetadata: Codable, Equatable, Sendable, Hashable { + public static func == (lhs: EpisodeMetadata, rhs: EpisodeMetadata) -> Bool { + lhs.link.id != rhs.link.id + } + + public let link: Playlist.EpisodeServer.Link + public let source: Playlist.EpisodeSource + public let server: Playlist.EpisodeServer + public var subtitles: [Playlist.EpisodeServer.Subtitle] + public let skipTimes: [Playlist.EpisodeServer.SkipTime] + + public init(link: Playlist.EpisodeServer.Link, source: Playlist.EpisodeSource, subtitles: [Playlist.EpisodeServer.Subtitle], server: Playlist.EpisodeServer, skipTimes: [Playlist.EpisodeServer.SkipTime]) { + self.link = link + self.source = source + self.server = server + self.subtitles = subtitles + self.skipTimes = skipTimes + } +} diff --git a/Sources/Shared/SharedModels/Extensions/Entry+.swift b/Sources/Shared/SharedModels/Extensions/Entry+.swift index 204c3c8..f707849 100644 --- a/Sources/Shared/SharedModels/Extensions/Entry+.swift +++ b/Sources/Shared/SharedModels/Extensions/Entry+.swift @@ -2,7 +2,7 @@ // Entry+.swift // // -// Created by ErrorErrorError on 1/2/24. +// Created by MochiTeam on 1/2/24. // // diff --git a/Sources/Shared/SharedModels/Image.swift b/Sources/Shared/SharedModels/Image.swift index edf8b1b..7d041f5 100644 --- a/Sources/Shared/SharedModels/Image.swift +++ b/Sources/Shared/SharedModels/Image.swift @@ -2,7 +2,7 @@ // Image.swift // // -// Created by ErrorErrorError on 4/18/23. +// Created by MochiTeam on 4/18/23. // // diff --git a/Sources/Shared/SharedModels/LibraryDirectory.swift b/Sources/Shared/SharedModels/LibraryDirectory.swift new file mode 100644 index 0000000..f44426c --- /dev/null +++ b/Sources/Shared/SharedModels/LibraryDirectory.swift @@ -0,0 +1,13 @@ +// +// LibraryDirectory.swift +// +// +// Created by MochiTeam on 24.04.2024. +// + +import Foundation + +public enum LibraryDirectory: String, CaseIterable { + case playlistCache = "PlaylistCache" + case downloaded = "Downloaded" +} diff --git a/Sources/Shared/SharedModels/Meta.swift b/Sources/Shared/SharedModels/Meta.swift index a136ea6..a820f77 100644 --- a/Sources/Shared/SharedModels/Meta.swift +++ b/Sources/Shared/SharedModels/Meta.swift @@ -2,7 +2,7 @@ // Meta.swift // // -// Created by ErrorErrorError on 4/5/23. +// Created by MochiTeam on 4/5/23. // // diff --git a/Sources/Shared/SharedModels/Playlist.swift b/Sources/Shared/SharedModels/Playlist.swift index 67852e4..3a41d44 100644 --- a/Sources/Shared/SharedModels/Playlist.swift +++ b/Sources/Shared/SharedModels/Playlist.swift @@ -2,7 +2,7 @@ // Playlist.swift // // -// Created by ErrorErrorError on 5/29/23. +// Created by MochiTeam on 5/29/23. // // @@ -16,8 +16,8 @@ import Tagged public struct Playlist: Sendable, Identifiable, Hashable, Codable { public let id: Tagged public let title: String? - public let posterImage: URL? - public let bannerImage: URL? + public var posterImage: URL? + public var bannerImage: URL? public let url: URL public let status: Status public let type: PlaylistType @@ -121,7 +121,7 @@ extension Playlist { // MARK: Playlist.Item extension Playlist { - public struct Item: Sendable, Equatable, Identifiable, Codable { + public struct Item: Sendable, Equatable, Identifiable, Codable, Hashable { public let id: Tagged public let title: String? public let description: String? @@ -209,7 +209,7 @@ extension Playlist { public typealias ItemsResponse = [Playlist.Group] - public struct Group: Sendable, Equatable, Identifiable, Decodable { + public struct Group: Sendable, Equatable, Identifiable, Codable { public let id: Tagged public let number: Double public let altTitle: String? @@ -232,7 +232,7 @@ extension Playlist { self.default = `default` } - public struct Variant: Sendable, Equatable, Identifiable, Decodable { + public struct Variant: Sendable, Equatable, Identifiable, Codable { public let id: Tagged public let title: String public let pagings: Loadable diff --git a/Sources/Shared/SharedModels/PlaylistCache.swift b/Sources/Shared/SharedModels/PlaylistCache.swift new file mode 100644 index 0000000..3900004 --- /dev/null +++ b/Sources/Shared/SharedModels/PlaylistCache.swift @@ -0,0 +1,36 @@ +// +// PlaylistCache.swift +// +// +// Created by MochiTeam on 17.04.2024. +// + +import Foundation + +public struct RepoModuleId: Codable, Sendable { + public let repoId: URL + public let moduleId: String + + public init(repoId: URL, moduleId: String) { + self.repoId = repoId + self.moduleId = moduleId + } +} + +public struct PlaylistCache: Codable, Equatable, Sendable { + public static func == (lhs: PlaylistCache, rhs: PlaylistCache) -> Bool { + lhs.playlist != rhs.playlist || lhs.details != rhs.details || lhs.repoModuleId.moduleId != rhs.repoModuleId.moduleId || lhs.repoModuleId.repoId != rhs.repoModuleId.repoId || lhs.groups != rhs.groups + } + + public var playlist: Playlist + public var details: Playlist.Details? + public var repoModuleId: RepoModuleId + public var groups: [Playlist.Group]? + + public init(playlist: Playlist, groups: [Playlist.Group]?, details: Playlist.Details?, repoModuleId: RepoModuleID) { + self.playlist = playlist + self.groups = groups + self.details = details + self.repoModuleId = .init(repoId: repoModuleId.repoId.rawValue, moduleId: repoModuleId.moduleId.rawValue) + } +} diff --git a/Sources/Shared/SharedModels/RepoModuleID.swift b/Sources/Shared/SharedModels/RepoModuleID.swift index ed22952..1ca2dda 100644 --- a/Sources/Shared/SharedModels/RepoModuleID.swift +++ b/Sources/Shared/SharedModels/RepoModuleID.swift @@ -2,7 +2,7 @@ // RepoModuleID.swift // // -// Created by ErrorErrorError on 6/2/23. +// Created by MochiTeam on 6/2/23. // // @@ -26,7 +26,7 @@ public struct RepoModuleID: Hashable, Sendable { extension Repo.ID { // Follow reverse domain name notation public var displayIdentifier: String { - // "dev.errorerrorerror.mochi.repo.local" for local storage + // "dev.MochiTeam.mochi.repo.local" for local storage rawValue.host?.split(separator: ".").reversed().joined(separator: ".").lowercased() ?? rawValue.absoluteString } } diff --git a/Sources/Shared/SharedModels/Text.swift b/Sources/Shared/SharedModels/Text.swift index fcfe1ab..18d3887 100644 --- a/Sources/Shared/SharedModels/Text.swift +++ b/Sources/Shared/SharedModels/Text.swift @@ -2,7 +2,7 @@ // Text.swift // // -// Created by ErrorErrorError on 4/18/23. +// Created by MochiTeam on 4/18/23. // // diff --git a/Sources/Shared/SharedModels/Utilities/Loadable.swift b/Sources/Shared/SharedModels/Utilities/Loadable.swift index eb6158d..30d9c14 100644 --- a/Sources/Shared/SharedModels/Utilities/Loadable.swift +++ b/Sources/Shared/SharedModels/Utilities/Loadable.swift @@ -2,7 +2,7 @@ // Loadable.swift // // -// Created by ErrorErrorError on 4/5/23. +// Created by MochiTeam on 4/5/23. // // diff --git a/Sources/Shared/SharedModels/Utilities/Paging.swift b/Sources/Shared/SharedModels/Utilities/Paging.swift index 8f0e18d..f623fbe 100644 --- a/Sources/Shared/SharedModels/Utilities/Paging.swift +++ b/Sources/Shared/SharedModels/Utilities/Paging.swift @@ -2,7 +2,7 @@ // Paging.swift // // -// Created by ErrorErrorError on 4/18/23. +// Created by MochiTeam on 4/18/23. // // diff --git a/Sources/Shared/SharedModels/Video.swift b/Sources/Shared/SharedModels/Video.swift index cc31e16..7365ca9 100644 --- a/Sources/Shared/SharedModels/Video.swift +++ b/Sources/Shared/SharedModels/Video.swift @@ -2,7 +2,7 @@ // Video.swift // // -// Created by ErrorErrorError on 4/18/23. +// Created by MochiTeam on 4/18/23. // // @@ -42,7 +42,7 @@ extension Playlist { } } - public struct EpisodeSource: Sendable, Equatable, Identifiable, Decodable { + public struct EpisodeSource: Sendable, Equatable, Identifiable, Codable, Hashable { public let id: Tagged public let displayName: String public let description: String? @@ -61,7 +61,7 @@ extension Playlist { } } - public struct EpisodeServer: Sendable, Equatable, Identifiable, Decodable { + public struct EpisodeServer: Sendable, Equatable, Identifiable, Codable, Hashable { public let id: Tagged public let displayName: String public let description: String? @@ -76,7 +76,7 @@ extension Playlist { self.description = description } - public struct Link: Sendable, Equatable, Identifiable, Decodable { + public struct Link: Sendable, Equatable, Identifiable, Codable, Hashable { public var id: Tagged { .init(url) } public let url: URL public let quality: Quality @@ -92,7 +92,7 @@ extension Playlist { self.format = format } - public enum Quality: Int, Sendable, Equatable, CustomStringConvertible, Decodable { + public enum Quality: Int, Sendable, Equatable, CustomStringConvertible, Codable { case auto case q360 case q480 @@ -115,15 +115,15 @@ extension Playlist { } } - public enum Format: Int, Equatable, Sendable, Decodable { + public enum Format: Int, Equatable, Sendable, Codable { case hls case dash } } - public struct Subtitle: Sendable, Equatable, Identifiable, Decodable { + public struct Subtitle: Sendable, Equatable, Identifiable, Codable, Hashable { public var id: Tagged { .init(url) } - public let url: URL + public var url: URL public let name: String public let format: Format public let `default`: Bool @@ -143,14 +143,14 @@ extension Playlist { self.autoselect = autoselect } - public enum Format: Int32, Sendable, Equatable, Decodable { + public enum Format: Int32, Sendable, Equatable, Codable { case vtt case ass case srt } } - public struct SkipTime: Hashable, Sendable, Decodable { + public struct SkipTime: Hashable, Sendable, Codable { public let startTime: Double public let endTime: Double public let type: SkipType @@ -165,7 +165,7 @@ extension Playlist { self.type = type } - public enum SkipType: Int32, Equatable, Sendable, CustomStringConvertible, Decodable { + public enum SkipType: Int32, Equatable, Sendable, CustomStringConvertible, Codable { case opening case ending case recap diff --git a/Sources/Shared/Styling/NavStack.swift b/Sources/Shared/Styling/NavStack.swift index 596a406..ef85d36 100644 --- a/Sources/Shared/Styling/NavStack.swift +++ b/Sources/Shared/Styling/NavStack.swift @@ -2,7 +2,7 @@ // NavStack.swift // // -// Created by ErrorErrorError on 5/20/23. +// Created by MochiTeam on 5/20/23. // // diff --git a/Sources/Shared/Styling/Popups.swift b/Sources/Shared/Styling/Popups.swift index 46b2e88..df36f81 100644 --- a/Sources/Shared/Styling/Popups.swift +++ b/Sources/Shared/Styling/Popups.swift @@ -2,7 +2,7 @@ // Popups.swift // // -// Created by ErrorErrorError on 4/20/23. +// Created by MochiTeam on 4/20/23. // // diff --git a/Sources/Shared/Styling/ScaledButtonStyle.swift b/Sources/Shared/Styling/ScaledButtonStyle.swift index 1fb4f0c..3a250ec 100644 --- a/Sources/Shared/Styling/ScaledButtonStyle.swift +++ b/Sources/Shared/Styling/ScaledButtonStyle.swift @@ -2,7 +2,7 @@ // ScaledButtonStyle.swift // // -// Created by ErrorErrorError on 10/12/23. +// Created by MochiTeam on 10/12/23. // // diff --git a/Sources/Shared/Styling/Settings/SettingsGroup.swift b/Sources/Shared/Styling/Settings/SettingsGroup.swift index 41b6170..60e4f36 100644 --- a/Sources/Shared/Styling/Settings/SettingsGroup.swift +++ b/Sources/Shared/Styling/Settings/SettingsGroup.swift @@ -2,7 +2,7 @@ // SettingsGroup.swift // // -// Created by ErrorErrorError on 10/11/23. +// Created by MochiTeam on 10/11/23. // // diff --git a/Sources/Shared/Styling/Settings/SettingsRow.swift b/Sources/Shared/Styling/Settings/SettingsRow.swift index cffafed..4e8615f 100644 --- a/Sources/Shared/Styling/Settings/SettingsRow.swift +++ b/Sources/Shared/Styling/Settings/SettingsRow.swift @@ -2,7 +2,7 @@ // SettingsRow.swift // // -// Created by ErrorErrorError on 10/11/23. +// Created by MochiTeam on 10/11/23. // // diff --git a/Sources/Shared/Styling/SheetView.swift b/Sources/Shared/Styling/SheetView.swift index f5c41b0..8af2dd1 100644 --- a/Sources/Shared/Styling/SheetView.swift +++ b/Sources/Shared/Styling/SheetView.swift @@ -2,7 +2,7 @@ // SheetView.swift // // -// Created by ErrorErrorError on 5/31/23. +// Created by MochiTeam on 5/31/23. // // diff --git a/Sources/Shared/Styling/StatusView.swift b/Sources/Shared/Styling/StatusView.swift index d40ef41..c48f758 100644 --- a/Sources/Shared/Styling/StatusView.swift +++ b/Sources/Shared/Styling/StatusView.swift @@ -2,7 +2,7 @@ // StatusView.swift // // -// Created by ErrorErrorError on 12/14/23. +// Created by MochiTeam on 12/14/23. // // diff --git a/Sources/Shared/Styling/ThemeModifier.swift b/Sources/Shared/Styling/ThemeModifier.swift index 2aed733..a2de2e4 100644 --- a/Sources/Shared/Styling/ThemeModifier.swift +++ b/Sources/Shared/Styling/ThemeModifier.swift @@ -2,7 +2,7 @@ // ThemeModifier.swift // // -// Created by ErrorErrorError on 10/11/23. +// Created by MochiTeam on 10/11/23. // // diff --git a/Sources/Shared/Styling/TopBar.swift b/Sources/Shared/Styling/TopBar.swift index 532c9ff..69937d8 100644 --- a/Sources/Shared/Styling/TopBar.swift +++ b/Sources/Shared/Styling/TopBar.swift @@ -2,7 +2,7 @@ // TopBar.swift // // -// Created by ErrorErrorError on 4/25/23. +// Created by MochiTeam on 4/25/23. // // diff --git a/Sources/Shared/Styling/_Exported.swift b/Sources/Shared/Styling/_Exported.swift index 60412e5..c423a33 100644 --- a/Sources/Shared/Styling/_Exported.swift +++ b/Sources/Shared/Styling/_Exported.swift @@ -2,7 +2,7 @@ // _Exported.swift // // -// Created by ErrorErrorError on 10/10/23. +// Created by MochiTeam on 10/10/23. // // diff --git a/Sources/Shared/Styling/macOS/NSWindow+.swift b/Sources/Shared/Styling/macOS/NSWindow+.swift index 612311e..d3d7851 100644 --- a/Sources/Shared/Styling/macOS/NSWindow+.swift +++ b/Sources/Shared/Styling/macOS/NSWindow+.swift @@ -2,7 +2,7 @@ // NSWindow+.swift // Mochi // -// Created by ErrorErrorError on 11/23/23. +// Created by MochiTeam on 11/23/23. // // diff --git a/Sources/Shared/ViewComponents/ChipView.swift b/Sources/Shared/ViewComponents/ChipView.swift index 2742f9d..6bf7a29 100644 --- a/Sources/Shared/ViewComponents/ChipView.swift +++ b/Sources/Shared/ViewComponents/ChipView.swift @@ -2,7 +2,7 @@ // ChipView.swift // // -// Created by ErrorErrorError on 7/23/23. +// Created by MochiTeam on 7/23/23. // // diff --git a/Sources/Shared/ViewComponents/CircularProgressView.swift b/Sources/Shared/ViewComponents/CircularProgressView.swift index 5528321..c097712 100644 --- a/Sources/Shared/ViewComponents/CircularProgressView.swift +++ b/Sources/Shared/ViewComponents/CircularProgressView.swift @@ -2,7 +2,7 @@ // CircularProgressView.swift // // -// Created by ErrorErrorError on 5/5/23. +// Created by MochiTeam on 5/5/23. // // diff --git a/Sources/Shared/ViewComponents/DynamicStack.swift b/Sources/Shared/ViewComponents/DynamicStack.swift index de6057b..42f58c4 100644 --- a/Sources/Shared/ViewComponents/DynamicStack.swift +++ b/Sources/Shared/ViewComponents/DynamicStack.swift @@ -2,7 +2,7 @@ // DynamicStack.swift // // -// Created by ErrorErrorError on 6/7/23. +// Created by MochiTeam on 6/7/23. // // diff --git a/Sources/Shared/ViewComponents/ElasticParallaxView.swift b/Sources/Shared/ViewComponents/ElasticParallaxView.swift index 691129d..4fd3016 100644 --- a/Sources/Shared/ViewComponents/ElasticParallaxView.swift +++ b/Sources/Shared/ViewComponents/ElasticParallaxView.swift @@ -2,7 +2,7 @@ // ElasticParallaxView.swift // // -// Created by ErrorErrorError on 5/19/23. +// Created by MochiTeam on 5/19/23. // // diff --git a/Sources/Shared/ViewComponents/ExpandableText.swift b/Sources/Shared/ViewComponents/ExpandableText.swift index 09e01ed..2de687b 100644 --- a/Sources/Shared/ViewComponents/ExpandableText.swift +++ b/Sources/Shared/ViewComponents/ExpandableText.swift @@ -2,7 +2,7 @@ // ExpandableText.swift // // -// Created by ErrorErrorError on 5/23/23. +// Created by MochiTeam on 5/23/23. // // diff --git a/Sources/Shared/ViewComponents/Extensions/Color+Ext.swift b/Sources/Shared/ViewComponents/Extensions/Color+Ext.swift index d4cfc9b..6d13731 100644 --- a/Sources/Shared/ViewComponents/Extensions/Color+Ext.swift +++ b/Sources/Shared/ViewComponents/Extensions/Color+Ext.swift @@ -2,7 +2,7 @@ // Color+Ext.swift // // -// Created by ErrorErrorError on 5/21/23. +// Created by MochiTeam on 5/21/23. // // diff --git a/Sources/Shared/ViewComponents/Extensions/Gradient+Easing.swift b/Sources/Shared/ViewComponents/Extensions/Gradient+Easing.swift index f02015b..9ae4ca9 100644 --- a/Sources/Shared/ViewComponents/Extensions/Gradient+Easing.swift +++ b/Sources/Shared/ViewComponents/Extensions/Gradient+Easing.swift @@ -2,7 +2,7 @@ // Gradient+Easing.swift // // -// Created by ErrorErrorError on 5/20/23. +// Created by MochiTeam on 5/20/23. // // // diff --git a/Sources/Shared/ViewComponents/Extensions/PlatformColor+Ext.swift b/Sources/Shared/ViewComponents/Extensions/PlatformColor+Ext.swift index 3013f09..c294509 100644 --- a/Sources/Shared/ViewComponents/Extensions/PlatformColor+Ext.swift +++ b/Sources/Shared/ViewComponents/Extensions/PlatformColor+Ext.swift @@ -2,7 +2,7 @@ // PlatformColor+Ext.swift // // -// Created by ErrorErrorError on 5/21/23. +// Created by MochiTeam on 5/21/23. // // diff --git a/Sources/Shared/ViewComponents/Extensions/Shape+Ext.swift b/Sources/Shared/ViewComponents/Extensions/Shape+Ext.swift index 93aba72..204c331 100644 --- a/Sources/Shared/ViewComponents/Extensions/Shape+Ext.swift +++ b/Sources/Shared/ViewComponents/Extensions/Shape+Ext.swift @@ -2,7 +2,7 @@ // Shape+Ext.swift // // -// Created by ErrorErrorError on 10/4/23. +// Created by MochiTeam on 10/4/23. // // diff --git a/Sources/Shared/ViewComponents/Extensions/View+Squircle.swift b/Sources/Shared/ViewComponents/Extensions/View+Squircle.swift index 62a746d..93fbf18 100644 --- a/Sources/Shared/ViewComponents/Extensions/View+Squircle.swift +++ b/Sources/Shared/ViewComponents/Extensions/View+Squircle.swift @@ -2,7 +2,7 @@ // View+Squircle.swift // // -// Created by ErrorErrorError on 5/29/23. +// Created by MochiTeam on 5/29/23. // // diff --git a/Sources/Shared/ViewComponents/FillAspectImage.swift b/Sources/Shared/ViewComponents/FillAspectImage.swift index 47185d7..ccc9bc0 100644 --- a/Sources/Shared/ViewComponents/FillAspectImage.swift +++ b/Sources/Shared/ViewComponents/FillAspectImage.swift @@ -2,7 +2,7 @@ // FillAspectImage.swift // // -// Created by ErrorErrorError on 10/25/22. +// Created by MochiTeam on 10/25/22. // // diff --git a/Sources/Shared/ViewComponents/InsetValue+Values.swift b/Sources/Shared/ViewComponents/InsetValue+Values.swift index 2ccbc72..9d17312 100644 --- a/Sources/Shared/ViewComponents/InsetValue+Values.swift +++ b/Sources/Shared/ViewComponents/InsetValue+Values.swift @@ -2,7 +2,7 @@ // InsetValue+Values.swift // // -// Created by ErrorErrorError on 4/19/23. +// Created by MochiTeam on 4/19/23. // // diff --git a/Sources/Shared/ViewComponents/InsetValue.swift b/Sources/Shared/ViewComponents/InsetValue.swift index f58b4bf..a03bc31 100644 --- a/Sources/Shared/ViewComponents/InsetValue.swift +++ b/Sources/Shared/ViewComponents/InsetValue.swift @@ -2,7 +2,7 @@ // InsetValue.swift // // -// Created by ErrorErrorError on 4/18/23. +// Created by MochiTeam on 4/18/23. // // diff --git a/Sources/Shared/ViewComponents/LazyView.swift b/Sources/Shared/ViewComponents/LazyView.swift index 55af459..25a4071 100644 --- a/Sources/Shared/ViewComponents/LazyView.swift +++ b/Sources/Shared/ViewComponents/LazyView.swift @@ -2,7 +2,7 @@ // LazyView.swift // // -// Created by ErrorErrorError on 10/6/23. +// Created by MochiTeam on 10/6/23. // // diff --git a/Sources/Shared/ViewComponents/LoadableView.swift b/Sources/Shared/ViewComponents/LoadableView.swift index 8e1b750..befdf23 100644 --- a/Sources/Shared/ViewComponents/LoadableView.swift +++ b/Sources/Shared/ViewComponents/LoadableView.swift @@ -2,7 +2,7 @@ // LoadableView.swift // // -// Created by ErrorErrorError on 1/7/23. +// Created by MochiTeam on 1/7/23. // // diff --git a/Sources/Shared/ViewComponents/NukeImage.swift b/Sources/Shared/ViewComponents/NukeImage.swift index 469cdef..6ae474e 100644 --- a/Sources/Shared/ViewComponents/NukeImage.swift +++ b/Sources/Shared/ViewComponents/NukeImage.swift @@ -2,7 +2,7 @@ // NukeImage.swift // // -// Created by ErrorErrorError on 10/10/23. +// Created by MochiTeam on 10/10/23. // // diff --git a/Sources/Shared/ViewComponents/OnInitialTask.swift b/Sources/Shared/ViewComponents/OnInitialTask.swift index 943c8ec..cab6669 100644 --- a/Sources/Shared/ViewComponents/OnInitialTask.swift +++ b/Sources/Shared/ViewComponents/OnInitialTask.swift @@ -2,7 +2,7 @@ // OnInitialTask.swift // // -// Created by ErrorErrorError on 12/15/23. +// Created by MochiTeam on 12/15/23. // // diff --git a/Sources/Shared/ViewComponents/PlatformViewRepresentable.swift b/Sources/Shared/ViewComponents/PlatformViewRepresentable.swift index b9e29c6..b2e5d7a 100644 --- a/Sources/Shared/ViewComponents/PlatformViewRepresentable.swift +++ b/Sources/Shared/ViewComponents/PlatformViewRepresentable.swift @@ -2,7 +2,7 @@ // PlatformViewRepresentable.swift // // -// Created by ErrorErrorError on 10/12/22. +// Created by MochiTeam on 10/12/22. // import SwiftUI diff --git a/Sources/Shared/ViewComponents/Refreshable.swift b/Sources/Shared/ViewComponents/Refreshable.swift index 8fb9fb9..aee4305 100644 --- a/Sources/Shared/ViewComponents/Refreshable.swift +++ b/Sources/Shared/ViewComponents/Refreshable.swift @@ -2,7 +2,7 @@ // Refreshable.swift // // -// Created by ErrorErrorError on 12/15/23. +// Created by MochiTeam on 12/15/23. // // diff --git a/Sources/Shared/ViewComponents/ScrollViewTracker.swift b/Sources/Shared/ViewComponents/ScrollViewTracker.swift index 7dd982a..41b2105 100644 --- a/Sources/Shared/ViewComponents/ScrollViewTracker.swift +++ b/Sources/Shared/ViewComponents/ScrollViewTracker.swift @@ -2,7 +2,7 @@ // ScrollViewTracker.swift // // -// Created by ErrorErrorError on 12/12/23. +// Created by MochiTeam on 12/12/23. // // Source: https://github.com/danielsaidi/ScrollKit/blob/main/Sources/ScrollKit/ScrollViewWithOffsetTracking.swift diff --git a/Sources/Shared/ViewComponents/SheetDetent.swift b/Sources/Shared/ViewComponents/SheetDetent.swift index 9d51529..d835360 100644 --- a/Sources/Shared/ViewComponents/SheetDetent.swift +++ b/Sources/Shared/ViewComponents/SheetDetent.swift @@ -2,7 +2,7 @@ // SheetDetent.swift // // -// Created by ErrorErrorError on 10/5/23. +// Created by MochiTeam on 10/5/23. // // diff --git a/Sources/Shared/ViewComponents/SnapScroll.swift b/Sources/Shared/ViewComponents/SnapScroll.swift index 13a5547..35b3545 100644 --- a/Sources/Shared/ViewComponents/SnapScroll.swift +++ b/Sources/Shared/ViewComponents/SnapScroll.swift @@ -2,7 +2,7 @@ // SnapScroll.swift // // -// Created by ErrorErrorError on 4/21/23. +// Created by MochiTeam on 4/21/23. // // diff --git a/Sources/Shared/ViewComponents/Swipable.swift b/Sources/Shared/ViewComponents/Swipable.swift index 04c67a9..acbaead 100644 --- a/Sources/Shared/ViewComponents/Swipable.swift +++ b/Sources/Shared/ViewComponents/Swipable.swift @@ -2,7 +2,7 @@ // Swipable.swift // // -// Created by ErrorErrorError on 6/27/23. +// Created by MochiTeam on 6/27/23. // // diff --git a/Sources/Shared/ViewComponents/View+ReadSize.swift b/Sources/Shared/ViewComponents/View+ReadSize.swift index 8f7f054..6ab1bd5 100644 --- a/Sources/Shared/ViewComponents/View+ReadSize.swift +++ b/Sources/Shared/ViewComponents/View+ReadSize.swift @@ -2,7 +2,7 @@ // View+ReadSize.swift // // -// Created by ErrorErrorError on 4/21/23. +// Created by MochiTeam on 4/21/23. // // diff --git a/Sources/Shared/ViewComponents/iOS/View+HomeIndicator.swift b/Sources/Shared/ViewComponents/iOS/View+HomeIndicator.swift index bccc523..62c1907 100644 --- a/Sources/Shared/ViewComponents/iOS/View+HomeIndicator.swift +++ b/Sources/Shared/ViewComponents/iOS/View+HomeIndicator.swift @@ -2,7 +2,7 @@ // View+HomeIndicator.swift // // -// Created by ErrorErrorError on 6/27/23. +// Created by MochiTeam on 6/27/23. // // diff --git a/Sources/Shared/ViewComponents/macOS/ToolbarAccessory.swift b/Sources/Shared/ViewComponents/macOS/ToolbarAccessory.swift index 3f6a9dc..2996530 100644 --- a/Sources/Shared/ViewComponents/macOS/ToolbarAccessory.swift +++ b/Sources/Shared/ViewComponents/macOS/ToolbarAccessory.swift @@ -2,7 +2,7 @@ // ToolbarAccessory.swift // // -// Created by ErrorErrorError on 11/28/23. +// Created by MochiTeam on 11/28/23. // // diff --git a/Tests/CoreDBTests/CoreDBTests.swift b/Tests/CoreDBTests/CoreDBTests.swift index 8f246ca..68e8a75 100644 --- a/Tests/CoreDBTests/CoreDBTests.swift +++ b/Tests/CoreDBTests/CoreDBTests.swift @@ -2,7 +2,7 @@ // CoreDBTests.swift // // -// Created by ErrorErrorError on 5/17/23. +// Created by MochiTeam on 5/17/23. // // diff --git a/Tests/CoreDBTests/Models.swift b/Tests/CoreDBTests/Models.swift index 1a5792c..bebd1a4 100644 --- a/Tests/CoreDBTests/Models.swift +++ b/Tests/CoreDBTests/Models.swift @@ -2,7 +2,7 @@ // Models.swift // // -// Created by ErrorErrorError on 5/19/23. +// Created by MochiTeam on 5/19/23. // // diff --git a/Tests/DatabaseClientTests/DatabaseClientTests.swift b/Tests/DatabaseClientTests/DatabaseClientTests.swift index 823a4b5..6376c34 100644 --- a/Tests/DatabaseClientTests/DatabaseClientTests.swift +++ b/Tests/DatabaseClientTests/DatabaseClientTests.swift @@ -2,7 +2,7 @@ // DatabaseClientTests.swift // // -// Created by ErrorErrorError on 5/13/23. +// Created by MochiTeam on 5/13/23. // // diff --git a/Tests/JSValueCoderTests/JSValueCoderTests.swift b/Tests/JSValueCoderTests/JSValueCoderTests.swift index 777adaa..3a69c16 100644 --- a/Tests/JSValueCoderTests/JSValueCoderTests.swift +++ b/Tests/JSValueCoderTests/JSValueCoderTests.swift @@ -2,7 +2,7 @@ // JSValueCoderTests.swift // // -// Created by ErrorErrorError on 11/7/23. +// Created by MochiTeam on 11/7/23. // // diff --git a/Tests/ModuleClientTests/JSRunnerTests.swift b/Tests/ModuleClientTests/JSRunnerTests.swift index 4d65f14..f465926 100644 --- a/Tests/ModuleClientTests/JSRunnerTests.swift +++ b/Tests/ModuleClientTests/JSRunnerTests.swift @@ -2,7 +2,7 @@ // JSRunnerTests.swift // // -// Created by ErrorErrorError on 11/8/23. +// Created by MochiTeam on 11/8/23. // // diff --git a/codemagic.yaml b/codemagic.yaml new file mode 100644 index 0000000..7c8c8ac --- /dev/null +++ b/codemagic.yaml @@ -0,0 +1,53 @@ +scripts: + - name: Get Flutter packages + script: | + flutter pub get + - name: Install pods + script: | + cd $CM_BUILD_DIR/ios + pod install + - name: Build the .app + script: | + # build using workspace + xcodebuild build \ + -project "$CM_BUILD_DIR/App/Mochi.xcodeproj" \ + -scheme MochiScheme \ + -skipMacroValidation \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ + - name: Create a Payload directory and move the .app file into it + script: | + # Locate the .app in the specified location + BUILD_OUTPUT_DIR=$(find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/*" -type d -name "*.app" -print -quit) + echo "Build output directory: $BUILD_OUTPUT_DIR" + + if [ -d "$BUILD_OUTPUT_DIR" ]; then + # Create Payload directory + mkdir -p Payload + + # Move the .app file to the Payload directory + mv "$BUILD_OUTPUT_DIR" Payload/ + if [ $? -eq 0 ]; then + echo "App moved to Payload directory successfully." + else + echo "Failed to move app to Payload directory." + exit 1 + fi + + # Zip the Payload directory + zip -r RunnerTest.ipa Payload + if [ $? -eq 0 ]; then + echo "IPA file created successfully: RunnerTest.ipa" + else + echo "Failed to create IPA file." + exit 1 + fi + else + echo "Error: .app file not found in the specified location." + exit 1 + fi +artifacts: + - $HOME/Library/Developer/Xcode/DerivedData/**/Build/**/*.app + - /Users/builder/clone/build/ios/ipa/*.ipa + - /Users/builder/clone/*.ipa diff --git a/cog.toml b/cog.toml index b0bb1c4..8223722 100644 --- a/cog.toml +++ b/cog.toml @@ -14,5 +14,5 @@ owner = "Mochi-Team" # intended to map git signature to remote username # and generate changelog links to their remote profiles authors = [ - { signature = "ErrorErrrorError", username = "errorerrorerror" } + { signature = "ErrorErrrorError", username = "MochiTeam" } ] diff --git a/exportOptions.plist b/exportOptions.plist new file mode 100644 index 0000000..f7982a3 --- /dev/null +++ b/exportOptions.plist @@ -0,0 +1,18 @@ + + + + + method + development + signingStyle + manual + stripSwiftSymbols + + compileBitcode + + uploadBitcode + + uploadSymbols + + + diff --git a/fastlane/Appfile b/fastlane/Appfile index a8aba97..4764529 100644 --- a/fastlane/Appfile +++ b/fastlane/Appfile @@ -1,4 +1,4 @@ -app_identifier("dev.errorerrorerror.mochi") # The bundle identifier of your app +app_identifier("dev.MochiTeam.mochi") # The bundle identifier of your app # apple_id("[[APPLE_ID]]") # Your Apple Developer Portal username