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