mirror of
https://github.com/XITRIX/iTorrent.git
synced 2026-05-30 11:46:50 +00:00
Storage containers implemented
WIP: Select download path Storage containers implemented Rework torrents Array to Dictionary
This commit is contained in:
@@ -18,6 +18,7 @@ extension ProgressWidgetAttributes {
|
||||
case seeding
|
||||
case checkingResumeData
|
||||
case paused
|
||||
case storageError
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +39,8 @@ extension ProgressWidgetAttributes.State {
|
||||
return %"torrent.state.resuming"
|
||||
case .paused:
|
||||
return %"torrent.state.paused"
|
||||
case .storageError:
|
||||
return %"torrent.state.storageError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,8 @@ struct ProgressWidgetLiveActivityWatchSupportContent: View {
|
||||
case .paused:
|
||||
Text(context.state.state.name)
|
||||
Spacer()
|
||||
case .storageError:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.font(.caption2)
|
||||
@@ -208,6 +210,8 @@ struct ProgressWidgetLiveActivityContent: View {
|
||||
Text(context.state.state.name)
|
||||
case .paused:
|
||||
Text(context.state.state.name)
|
||||
case .storageError:
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -253,6 +257,8 @@ struct LeadingView: View {
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
case .paused:
|
||||
Image(systemName: "pause.fill")
|
||||
case .storageError:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Submodule Submodules/LibTorrent-Swift updated: e7121a7c0a...a4aab2d266
Submodule Submodules/MVVMFoundation updated: 9612851bdc...74194bf191
@@ -51,6 +51,8 @@
|
||||
7C5FBE702BC2EF8B0069E5A0 /* EditTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5FBE6D2BC2EF8B0069E5A0 /* EditTextViewController.swift */; };
|
||||
7C5FBE742BC2F0A30069E5A0 /* MvvmViewModelProtocol+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5FBE732BC2F0A30069E5A0 /* MvvmViewModelProtocol+Alert.swift */; };
|
||||
7C609A5B2C1F5D9E00586635 /* SceneDelegate+AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C609A5A2C1F5D9E00586635 /* SceneDelegate+AVPlayer.swift */; };
|
||||
7C95B7B42C35DE5C000EC50F /* StoragePreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95B7B32C35DE49000EC50F /* StoragePreferencesView.swift */; };
|
||||
7C95B7B72C385B99000EC50F /* UICellAccessory+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95B7B62C385B97000EC50F /* UICellAccessory+Image.swift */; };
|
||||
7CAD30192BC3455800592990 /* SWXMLHash in Frameworks */ = {isa = PBXBuildFile; productRef = 7CAD30182BC3455800592990 /* SWXMLHash */; };
|
||||
7CAD301C2BC3457900592990 /* RssModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CAD301B2BC3457900592990 /* RssModel.swift */; };
|
||||
7CAD301E2BC347D900592990 /* Published+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CAD301D2BC347D900592990 /* Published+Codable.swift */; };
|
||||
@@ -205,6 +207,8 @@
|
||||
D1EFCD172AF6AE1600D33A7A /* TorrentFilesDictionaryItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EFCD162AF6AE1600D33A7A /* TorrentFilesDictionaryItemView.swift */; };
|
||||
D1EFCD192AF6AEC700D33A7A /* TorrentFilesDictionaryItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EFCD182AF6AEC700D33A7A /* TorrentFilesDictionaryItemViewModel.swift */; };
|
||||
D1F8BC852AFC405A00A6258C /* MvvmViewModel+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F8BC842AFC405A00A6258C /* MvvmViewModel+Alert.swift */; };
|
||||
D1FFC9652C38136600233C2F /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FFC9642C38135F00233C2F /* URL+Extensions.swift */; };
|
||||
D1FFC96C2C382FC900233C2F /* StorageModel+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FFC96B2C382FBE00233C2F /* StorageModel+Extensions.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -287,6 +291,8 @@
|
||||
7C609A572C1F1D6400586635 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
7C609A592C1F2A2700586635 /* iTorrent.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = iTorrent.xcconfig; sourceTree = "<group>"; };
|
||||
7C609A5A2C1F5D9E00586635 /* SceneDelegate+AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SceneDelegate+AVPlayer.swift"; sourceTree = "<group>"; };
|
||||
7C95B7B32C35DE49000EC50F /* StoragePreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoragePreferencesView.swift; sourceTree = "<group>"; };
|
||||
7C95B7B62C385B97000EC50F /* UICellAccessory+Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICellAccessory+Image.swift"; sourceTree = "<group>"; };
|
||||
7CAD301B2BC3457900592990 /* RssModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RssModel.swift; sourceTree = "<group>"; };
|
||||
7CAD301D2BC347D900592990 /* Published+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Published+Codable.swift"; sourceTree = "<group>"; };
|
||||
7CAD301F2BC34BCE00592990 /* RssFeedProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RssFeedProvider.swift; sourceTree = "<group>"; };
|
||||
@@ -437,6 +443,8 @@
|
||||
D1EFCD162AF6AE1600D33A7A /* TorrentFilesDictionaryItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentFilesDictionaryItemView.swift; sourceTree = "<group>"; };
|
||||
D1EFCD182AF6AEC700D33A7A /* TorrentFilesDictionaryItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentFilesDictionaryItemViewModel.swift; sourceTree = "<group>"; };
|
||||
D1F8BC842AFC405A00A6258C /* MvvmViewModel+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MvvmViewModel+Alert.swift"; sourceTree = "<group>"; };
|
||||
D1FFC9642C38135F00233C2F /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D1FFC96B2C382FBE00233C2F /* StorageModel+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageModel+Extensions.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -597,6 +605,22 @@
|
||||
path = MvvmFoundation;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7C95B7A32C34B554000EC50F /* Storage */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7C95B7B32C35DE49000EC50F /* StoragePreferencesView.swift */,
|
||||
);
|
||||
path = Storage;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7C95B7B52C385B8E000EC50F /* UIKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7C95B7B62C385B97000EC50F /* UICellAccessory+Image.swift */,
|
||||
);
|
||||
path = UIKit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7CAD301A2BC3456700592990 /* RssFeed */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -764,6 +788,7 @@
|
||||
D111384C2AF9663F008907F7 /* Preferences */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7C95B7A32C34B554000EC50F /* Storage */,
|
||||
D1DB718E2BD92206007F9267 /* Patreon */,
|
||||
7CB6F6CC2BD82B8A00D0813B /* FileSharing */,
|
||||
D1352D1F2BBC338300104E7B /* Base */,
|
||||
@@ -786,8 +811,8 @@
|
||||
D11333B32AF19C3400FA017E /* TorrentService */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1FFC9632C37F44800233C2F /* Extensions */,
|
||||
D1A226F12AEF019400669D6D /* TorrentService.swift */,
|
||||
D11333B42AF19C4900FA017E /* TorrentHandle+Extension.swift */,
|
||||
);
|
||||
path = TorrentService;
|
||||
sourceTree = "<group>";
|
||||
@@ -999,6 +1024,7 @@
|
||||
D19E00242AF045DF000A17A2 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7C95B7B52C385B8E000EC50F /* UIKit */,
|
||||
7C5FBE722BC2F0940069E5A0 /* MvvmFoundation */,
|
||||
D173D9DF2BC0285800E4F9EB /* UIMenu */,
|
||||
7C5FBE2F2BBF5C990069E5A0 /* SwiftUI */,
|
||||
@@ -1006,6 +1032,7 @@
|
||||
D1048D872BBEA23C0027EF2F /* Combine */,
|
||||
D1352D282BBD418200104E7B /* UIViewController */,
|
||||
D1D1279A2BC7CA7600C04533 /* SwiftUILayoutGuides.swift */,
|
||||
D1FFC9642C38135F00233C2F /* URL+Extensions.swift */,
|
||||
D19E00252AF045F9000A17A2 /* SpeedFormat.swift */,
|
||||
D1ACFDC82AF6ED200098FF56 /* UIImage+File.swift */,
|
||||
D1AA00D12AFAC95500B74629 /* String+Localized.swift */,
|
||||
@@ -1305,6 +1332,15 @@
|
||||
path = TorrentFilesDictionaryItem;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D1FFC9632C37F44800233C2F /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D11333B42AF19C4900FA017E /* TorrentHandle+Extension.swift */,
|
||||
D1FFC96B2C382FBE00233C2F /* StorageModel+Extensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -1497,6 +1533,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7CF6DA352C0F3DB90033D03F /* BCHostingConfiguration.swift in Sources */,
|
||||
D1FFC96C2C382FC900233C2F /* StorageModel+Extensions.swift in Sources */,
|
||||
D1F8BC852AFC405A00A6258C /* MvvmViewModel+Alert.swift in Sources */,
|
||||
D17871FF2AF2C0A700F11F9F /* UIKitSwiftUIInarop.swift in Sources */,
|
||||
D135C59C2AEFB96F00440680 /* TorrentDetailsViewModel.swift in Sources */,
|
||||
@@ -1528,6 +1565,7 @@
|
||||
7C5FBE642BC158BC0069E5A0 /* SceneDelegate+URLProcessing.swift in Sources */,
|
||||
D1A2269E2AEEEFCC00669D6D /* AppDelegate.swift in Sources */,
|
||||
7C5FBE702BC2EF8B0069E5A0 /* EditTextViewController.swift in Sources */,
|
||||
7C95B7B42C35DE5C000EC50F /* StoragePreferencesView.swift in Sources */,
|
||||
7CFEBEA02BC6F3CD0013233F /* Date+Extensions.swift in Sources */,
|
||||
D1DB718B2BD91606007F9267 /* ImageCache.swift in Sources */,
|
||||
7CBDBAAD2C31EF0C008C986B /* UserDefaults+AppGroup.swift in Sources */,
|
||||
@@ -1572,6 +1610,7 @@
|
||||
7CAD301C2BC3457900592990 /* RssModel.swift in Sources */,
|
||||
7CFEBE882BC439C30013233F /* RssChannelItemCell.swift in Sources */,
|
||||
7C5FBE312BBF5CAB0069E5A0 /* MinHeightModifier.swift in Sources */,
|
||||
7C95B7B72C385B99000EC50F /* UICellAccessory+Image.swift in Sources */,
|
||||
D1EFCD192AF6AEC700D33A7A /* TorrentFilesDictionaryItemViewModel.swift in Sources */,
|
||||
D1B99D892BEE613B00F51514 /* PatreonWebServer.swift in Sources */,
|
||||
7CFEBE762BC3ED550013233F /* RssFeedCellViewModel.swift in Sources */,
|
||||
@@ -1586,6 +1625,7 @@
|
||||
D135C5992AEFB96100440680 /* TorrentDetailsViewController.swift in Sources */,
|
||||
D1048D8B2BBEB6DE0027EF2F /* CombineLatest.swift in Sources */,
|
||||
D11138572AF976CE008907F7 /* TorrentAddDirectoryItemViewModel.swift in Sources */,
|
||||
D1FFC9652C38136600233C2F /* URL+Extensions.swift in Sources */,
|
||||
7C1C08AB2C31F31900569B45 /* PauseTorrentIntent.swift in Sources */,
|
||||
D1CAB8872AF3B52E00EB6AFF /* ToggleCellViewModel.swift in Sources */,
|
||||
D11BE5492AFBA03D00780C1B /* PRButtonView.swift in Sources */,
|
||||
@@ -1706,7 +1746,6 @@
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "iTorrent2 ProgressWidget Dev";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -1748,7 +1787,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -1769,7 +1808,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 2.0.5;
|
||||
MARKETING_VERSION = 2.0.6;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -1778,6 +1817,7 @@
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_STRICT_CONCURRENCY = minimal;
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = RelWithDebInfo;
|
||||
};
|
||||
@@ -1851,7 +1891,6 @@
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "iTorrent2 ProgressWidget Dev";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -1893,7 +1932,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -1909,7 +1948,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 2.0.5;
|
||||
MARKETING_VERSION = 2.0.6;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1918,6 +1957,7 @@
|
||||
SWIFT_STRICT_CONCURRENCY = minimal;
|
||||
SWIFT_VERSION = 5.0;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -1991,7 +2031,6 @@
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "iTorrent2 ProgressWidget Distrib";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -2033,7 +2072,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -2054,7 +2093,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.1;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 2.0.5;
|
||||
MARKETING_VERSION = 2.0.6;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -2063,6 +2102,7 @@
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_STRICT_CONCURRENCY = minimal;
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"originHash" : "11bb8e91059647586820412f0c343b58cc80736343ec98a17b3c6d40900620dd",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "abseil-cpp-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/abseil-cpp-binary.git",
|
||||
"state" : {
|
||||
"revision" : "748c7837511d0e6a507737353af268484e1745e2",
|
||||
"version" : "1.2024011601.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "app-check",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/app-check.git",
|
||||
"state" : {
|
||||
"revision" : "3b62f154d00019ae29a71e9738800bb6f18b236d",
|
||||
"version" : "10.19.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "combinecocoa",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/XITRIX/CombineCocoa.git",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "c1bd62b53589e053c56958beebb6d9aee1367d5d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "firebase-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "e57841b296d04370ea23580f908881b0ccab17b9",
|
||||
"version" : "10.28.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googleappmeasurement",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||
"state" : {
|
||||
"revision" : "fe727587518729046fc1465625b9afd80b5ab361",
|
||||
"version" : "10.28.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googledatatransport",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleDataTransport.git",
|
||||
"state" : {
|
||||
"revision" : "a637d318ae7ae246b02d7305121275bc75ed5565",
|
||||
"version" : "9.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googleutilities",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||
"state" : {
|
||||
"revision" : "57a1d307f42df690fdef2637f3e5b776da02aad6",
|
||||
"version" : "7.13.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "grpc-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/grpc-binary.git",
|
||||
"state" : {
|
||||
"revision" : "e9fad491d0673bdda7063a0341fb6b47a30c5359",
|
||||
"version" : "1.62.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "gtm-session-fetcher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||
"state" : {
|
||||
"revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b",
|
||||
"version" : "3.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "interop-ios-for-google-sdks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
|
||||
"state" : {
|
||||
"revision" : "2d12673670417654f08f5f90fdd62926dc3a2648",
|
||||
"version" : "100.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "leveldb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/leveldb.git",
|
||||
"state" : {
|
||||
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
|
||||
"version" : "1.22.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "marqueelabel",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/cbpowell/MarqueeLabel.git",
|
||||
"state" : {
|
||||
"revision" : "877e810534cda9afabb8143ae319b7c3341b121b",
|
||||
"version" : "4.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "marqueetext",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/joekndy/MarqueeText.git",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "e18d514455563ad00581e0d825146c6f15b1c6c4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nanopb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/nanopb.git",
|
||||
"state" : {
|
||||
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
|
||||
"version" : "2.30910.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "openssl",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/krzyzanowskim/OpenSSL.git",
|
||||
"state" : {
|
||||
"revision" : "bb0cbdf1e01c6a9fde4077f041abc766ea48bd7f",
|
||||
"version" : "3.1.5006"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "promises",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/promises.git",
|
||||
"state" : {
|
||||
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
|
||||
"version" : "2.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-protobuf",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-protobuf.git",
|
||||
"state" : {
|
||||
"revision" : "9f0c76544701845ad98716f3f6a774a892152bcb",
|
||||
"version" : "1.26.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swxmlhash",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/drmohundro/SWXMLHash.git",
|
||||
"state" : {
|
||||
"revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f",
|
||||
"version" : "7.0.2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@@ -58,3 +58,59 @@ class BaseViewController<ViewModel: MvvmViewModelProtocol>: SAViewController<Vie
|
||||
return titleLabel
|
||||
}()
|
||||
}
|
||||
|
||||
class BaseHostingViewController<View: MvvmSwiftUIViewProtocol>: SAHostingViewController<View>, ToolbarHidingProtocol {
|
||||
var isToolbarItemsHidden: Bool { toolbarItems?.isEmpty ?? true }
|
||||
var useMarqueeLabel: Bool { true }
|
||||
|
||||
// override var title: String? {
|
||||
// get { super.title }
|
||||
// set {
|
||||
// super.title = newValue
|
||||
// titleLabel.text = newValue
|
||||
// titleLabel.sizeToFit()
|
||||
// }
|
||||
// }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
#if os(visionOS)
|
||||
view.backgroundColor = nil
|
||||
#endif
|
||||
|
||||
// #if !os(visionOS) // Not renders properly on VisionOS
|
||||
// if useMarqueeLabel {
|
||||
// navigationItem.titleView = titleLabel
|
||||
// titleLabel.sizeToFit()
|
||||
// }
|
||||
// #endif
|
||||
|
||||
title = rootView.title
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.setToolbarHidden(isToolbarItemsHidden, animated: false)
|
||||
|
||||
if navigationController?.viewControllers.count == 1 {
|
||||
navigationItem.trailingItemGroups = [.fixedGroup(items: [
|
||||
UIBarButtonItem(systemItem: .close, primaryAction: .init { [unowned self] _ in
|
||||
dismiss(animated: true)
|
||||
})
|
||||
])]
|
||||
}
|
||||
}
|
||||
|
||||
private let titleLabel: MarqueeLabel = {
|
||||
let titleLabel = MarqueeLabel()
|
||||
#if !os(visionOS)
|
||||
titleLabel.font = .preferredFont(forTextStyle: .headline)
|
||||
#else
|
||||
titleLabel.font = .preferredFont(forTextStyle: .title1)
|
||||
#endif
|
||||
titleLabel.fadeLength = 16
|
||||
titleLabel.adjustsFontForContentSizeCategory = true
|
||||
titleLabel.textAlignment = .center
|
||||
return titleLabel
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ struct DetailCellView: MvvmSwiftUICellProtocol {
|
||||
if isHorizontal {
|
||||
HStack {
|
||||
Text(viewModel.title)
|
||||
.fontWeight(.semibold)
|
||||
.fontWeight(viewModel.isBold ? .semibold : .regular)
|
||||
.foregroundStyle(viewModel.isEnabled ? Color.primary : Color.secondary)
|
||||
Spacer(minLength: viewModel.spacer)
|
||||
Text(LocalizedStringKey(viewModel.detail))
|
||||
.foregroundStyle(Color.accentColor)
|
||||
@@ -43,6 +44,7 @@ struct DetailCellView: MvvmSwiftUICellProtocol {
|
||||
cell.contentConfiguration = UIHostingConfiguration {
|
||||
Self(viewModel: itemIdentifier)
|
||||
}
|
||||
cell.isUserInteractionEnabled = itemIdentifier.isEnabled
|
||||
cell.accessories = itemIdentifier.selectAction == nil ? [] : [.disclosureIndicator(displayed: .always)]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,16 @@ class DetailCellViewModel: BaseViewModel, ObservableObject, MvvmSelectableProtoc
|
||||
@Published var title: String = ""
|
||||
@Published var detail: String = ""
|
||||
@Published var spacer: Double = 0
|
||||
@Published var isBold: Bool = true
|
||||
@Published var isEnabled: Bool = true
|
||||
|
||||
init(title: String = "", detail: String = "", spacer: Double = 24, selectAction: (() -> Void)? = nil) {
|
||||
init(title: String = "", detail: String = "", spacer: Double = 24, isBold: Bool = true, isEnabled: Bool = true, selectAction: (() -> Void)? = nil) {
|
||||
self.title = title
|
||||
self.detail = detail
|
||||
self.spacer = spacer
|
||||
self.selectAction = selectAction
|
||||
self.isEnabled = isEnabled
|
||||
self.isBold = isBold
|
||||
}
|
||||
|
||||
required init() {
|
||||
|
||||
@@ -26,7 +26,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
|
||||
guard let hash = response.notification.request.content.userInfo["hash"] as? String,
|
||||
let torrentHandle = TorrentService.shared.torrents.first(where: { $0.snapshot.infoHashes.best.hex == hash })
|
||||
let torrentHandle = TorrentService.shared.torrents.values.first(where: { $0.snapshot.infoHashes.best.hex == hash })
|
||||
else { return }
|
||||
|
||||
Self.showTorrentDetailScreen(with: torrentHandle)
|
||||
|
||||
@@ -102,6 +102,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"addTorrent.storage.manage" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Manage..."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Настроить..."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"addTorrent.storage.selected" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Select storage"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Выберите хранилище"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common.add" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -150,6 +182,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"common.continue" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Continue"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Продолжить"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common.delete" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -647,6 +695,55 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"details.path" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Storage"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Хранилище"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"details.path.browse" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Current"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Текущее"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"details.path.migrate" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Migrate storage"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Мигрировать хранилище"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"details.pause" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -663,6 +760,54 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"details.refreshStorage.fail.message" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Storage not found"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Хранилище не найдено"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"details.refreshStorage.message" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "iTorrent will try to find missing storage"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "iTorrent попробует найти утерянное хранилище"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"details.refreshStorage.title" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Refresh storage"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Перезагрузить хранилище"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"details.rehash" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -2975,6 +3120,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences.storage.add" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Add more..."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Добавить..."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences.storage.add.error.notDirectory" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Selected path is not a directory"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Выбранный путь не является директорией"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences.storage.allocate" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -3007,6 +3184,86 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences.storage.storages" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Storages"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Хранилища"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences.storage.storages.available%lld/%lld" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Available: %1$lld/%2$lld"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Доступно: %1$lld/%2$lld"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences.storage.warning.accept" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "I understood, continue"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Я понял, продолжить"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences.storage.warning.message" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "In case of using external drives, you should eject it only after closing iTorrent app. Otherwise iTorrent cannot garantee safety storing of your downloaded files and could potentially corrups current states of torrents from ejected drive."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "В случае использования внешних дисков извлекать их следует только после закрытия приложения iTorrent. В противном случае iTorrent не может гарантировать безопасное хранение загруженных вами файлов и потенциально может испортить текущее состояние торрентов с извлеченного диска."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences.storage.warning.title" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "WARNING!"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ВНИМАНИЕ!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"preferences.version" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -3775,6 +4032,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"torrent.state.storageError" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Storage error"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Ошибка хранилища"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tracker.leeches" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
@@ -27,7 +27,7 @@ private extension SceneDelegate {
|
||||
guard url.absoluteString.hasPrefix(prefix) else { return false }
|
||||
let hash = url.absoluteString.replacingOccurrences(of: prefix, with: "")
|
||||
|
||||
guard let torrent = TorrentService.shared.torrents.first(where: { $0.snapshot.infoHashes.best.hex == hash })
|
||||
guard let torrent = TorrentService.shared.torrents.values.first(where: { $0.snapshot.infoHashes.best.hex == hash })
|
||||
else { return false }
|
||||
|
||||
AppDelegate.showTorrentDetailScreen(with: torrent)
|
||||
|
||||
@@ -52,6 +52,8 @@ class SceneDelegate: MvvmSceneDelegate {
|
||||
router.register(PRColorPickerCell.self)
|
||||
|
||||
// MARK: Controllers
|
||||
router.register(BaseHostingViewController<StoragePreferencesView>.self)
|
||||
|
||||
router.register(TorrentListViewController<TorrentListViewModel>.self)
|
||||
router.register(TorrentDetailsViewController<TorrentDetailsViewModel>.self)
|
||||
router.register(TorrentFilesViewController<TorrentFilesViewModel>.self)
|
||||
|
||||
@@ -14,6 +14,8 @@ struct PRButtonView: MvvmSwiftUICellProtocol {
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(viewModel.title)
|
||||
.fontWeight(viewModel.isBold ? .semibold : .regular)
|
||||
.foregroundStyle(viewModel.tintedTitle ? Color.accentColor : Color.primary)
|
||||
Spacer()
|
||||
Text(viewModel.value)
|
||||
.foregroundStyle(viewModel.tinted ? Color.accentColor : Color.secondary)
|
||||
@@ -21,6 +23,15 @@ struct PRButtonView: MvvmSwiftUICellProtocol {
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
.systemMinimumHeight()
|
||||
.swipeActions {
|
||||
if let removeAction = viewModel.removeAction {
|
||||
Button(role: .destructive) {
|
||||
removeAction()
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var registration: UICollectionView.CellRegistration<MvvmCollectionViewListCell<PRButtonViewModel>, PRButtonViewModel> = .init { cell, indexPath, itemIdentifier in
|
||||
|
||||
@@ -12,13 +12,16 @@ import SwiftUI
|
||||
extension PRButtonViewModel {
|
||||
struct Config {
|
||||
var id: String?
|
||||
var removeAction: (() -> Void)? = nil
|
||||
var title: String
|
||||
var value: AnyPublisher<String, Never>?// = Just("").eraseToAnyPublisher()
|
||||
var tintedTitle: Bool = false
|
||||
var isBold: Bool = false
|
||||
var value: AnyPublisher<String, Never>?
|
||||
var canReorder: Bool = false
|
||||
var tinted: Bool = true
|
||||
var singleLine: Bool = false
|
||||
var accessories: [UICellAccessory] = []
|
||||
var selectAction: () -> Void = {}
|
||||
var selectAction: (() -> Void)? = {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,17 +30,22 @@ class PRButtonViewModel: BaseViewModelWith<PRButtonViewModel.Config>, Observable
|
||||
|
||||
var id: String?
|
||||
@Published var title = ""
|
||||
@Published var tintedTitle: Bool = false
|
||||
@Published var isBold: Bool = false
|
||||
@Published var value: String = ""
|
||||
@Published var tinted: Bool = true
|
||||
@Published var singleLine: Bool = false
|
||||
@Published var canReorder: Bool = false
|
||||
@Published var accessories: [UICellAccessory] = []
|
||||
@Published var removeAction: (() -> Void)?
|
||||
|
||||
var metadata: Any?
|
||||
|
||||
override func prepare(with model: Config) {
|
||||
id = model.id
|
||||
title = model.title
|
||||
tintedTitle = model.tintedTitle
|
||||
isBold = model.isBold
|
||||
if let value = model.value {
|
||||
value.assign(to: &self.$value)
|
||||
}
|
||||
@@ -46,15 +54,21 @@ class PRButtonViewModel: BaseViewModelWith<PRButtonViewModel.Config>, Observable
|
||||
canReorder = model.canReorder
|
||||
tinted = model.tinted
|
||||
singleLine = model.singleLine
|
||||
removeAction = model.removeAction
|
||||
}
|
||||
|
||||
override func isEqual(to other: MvvmViewModel) -> Bool {
|
||||
guard let other = other as? Self else { return false }
|
||||
return id == other.id && title == other.title
|
||||
return id == other.id &&
|
||||
title == other.title &&
|
||||
isBold == other.isBold &&
|
||||
accessories.count == other.accessories.count
|
||||
}
|
||||
|
||||
override func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
hasher.combine(title)
|
||||
hasher.combine(isBold)
|
||||
hasher.combine(accessories.count)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,13 @@ private extension PreferencesViewModel {
|
||||
|
||||
sections.append(.init(id: "memory", header: %"preferences.storage") {
|
||||
storageVM
|
||||
PRSwitchViewModel(with: .init(title: %"preferences.storage.allocate", value: preferences.$allocateMemory.binding))
|
||||
PRButtonViewModel(with: .init(title: %"preferences", accessories: [.disclosureIndicator()]) { [unowned self] in
|
||||
navigate(to: StoragePreferencesViewModel.self, by: .show)
|
||||
})
|
||||
// PRButtonViewModel(with: .init(title: "preferences2", accessories: [.disclosureIndicator()]) { [unowned self] in
|
||||
// navigate(to: StoragePreferencesViewModel.self, by: .show)
|
||||
//// navigate(to: SUIStoragePreferencesViewModel.self, by: .show)
|
||||
// })
|
||||
})
|
||||
|
||||
#if IS_SUPPORT_LOCATION_BG
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
//
|
||||
// StoragePreferencesView.swift
|
||||
// iTorrent
|
||||
//
|
||||
// Created by Даниил Виноградов on 03.07.2024.
|
||||
//
|
||||
|
||||
import LibTorrent
|
||||
import MvvmFoundation
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
class StoragePreferencesViewModel: BaseViewModel, ObservableObject {
|
||||
@Published var allocateMemory: Bool = false
|
||||
@Published var customStoragesVM: [UUID: StorageModel] = [:]
|
||||
@Published var currentStorage: UUID?
|
||||
@Published var isStorageRulesAccepted: Bool = false
|
||||
|
||||
required init() {
|
||||
super.init()
|
||||
allocateMemory = preferences.allocateMemory
|
||||
preferences.$storageScopes.assign(to: &$customStoragesVM)
|
||||
|
||||
isStorageRulesAccepted = preferences.isStorageRulesAccepted
|
||||
preferences.$isStorageRulesAccepted.assign(to: &$isStorageRulesAccepted)
|
||||
|
||||
preferences.$defaultStorage.assign(to: &$currentStorage)
|
||||
|
||||
disposeBag.bind {
|
||||
$allocateMemory.sink { [unowned self] in preferences.allocateMemory = $0 }
|
||||
}
|
||||
}
|
||||
|
||||
@Injected var preferences: PreferencesStorage
|
||||
}
|
||||
|
||||
struct StoragePreferencesView<VM: StoragePreferencesViewModel>: MvvmSwiftUIViewProtocol {
|
||||
@ObservedObject var viewModel: VM
|
||||
@State var filePickerPresented: Bool = false
|
||||
|
||||
let storagesLimit = 5
|
||||
var title: String = %"preferences.storage"
|
||||
|
||||
init(viewModel: VM) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
Toggle("preferences.storage.allocate", isOn: $viewModel.allocateMemory)
|
||||
}
|
||||
|
||||
if !viewModel.isStorageRulesAccepted {
|
||||
Section {
|
||||
VStack(spacing: 16) {
|
||||
VStack(spacing: 8) {
|
||||
Text("preferences.storage.warning.title")
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text("preferences.storage.warning.message")
|
||||
}
|
||||
|
||||
Button {
|
||||
withAnimation {
|
||||
viewModel.preferences.isStorageRulesAccepted = true
|
||||
}
|
||||
} label: {
|
||||
Spacer()
|
||||
Text("preferences.storage.warning.accept")
|
||||
.fontWeight(.semibold)
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.accentColor(.red)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
.listRowBackground(Color.red.opacity(0.1))
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
viewModel.preferences.defaultStorage = nil
|
||||
} label: {
|
||||
HStack {
|
||||
Text(StorageModel.defaultName)
|
||||
.foregroundStyle(Color.primary)
|
||||
Spacer()
|
||||
if viewModel.preferences.defaultStorage == nil {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
ForEach(Array(viewModel.customStoragesVM.values.sorted(by: { $0.name.localizedStandardCompare($1.name) == .orderedAscending }))) { scope in
|
||||
Button {
|
||||
if scope.allowed {
|
||||
viewModel.preferences.defaultStorage = scope.uuid
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text(scope.name)
|
||||
.foregroundStyle(scope.allowed ? Color.primary : Color.secondary)
|
||||
Spacer()
|
||||
if viewModel.preferences.defaultStorage == scope.uuid {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.fontWeight(.medium)
|
||||
} else if !scope.allowed {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}.swipeActions {
|
||||
Button(role: .destructive) {
|
||||
viewModel.preferences.storageScopes[scope.uuid] = nil
|
||||
if viewModel.preferences.defaultStorage == scope.uuid {
|
||||
viewModel.preferences.defaultStorage = nil
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
if viewModel.customStoragesVM.count < storagesLimit - 1 {
|
||||
Button("preferences.storage.add") {
|
||||
filePickerPresented = true
|
||||
}.disabled(!viewModel.isStorageRulesAccepted)
|
||||
}
|
||||
} header: {
|
||||
HStack {
|
||||
Text("preferences.storage.storages")
|
||||
Spacer()
|
||||
Text("preferences.storage.storages.available\(viewModel.customStoragesVM.count + 1)/\(storagesLimit)")
|
||||
}
|
||||
}
|
||||
}.fileImporter(isPresented: $filePickerPresented, allowedContentTypes: [.folder]) { result in
|
||||
guard let url = try? result.get() else { return }
|
||||
|
||||
guard url.isDirectory else {
|
||||
return viewModel.alert(title: %"common.error", message: %"preferences.storage.add.error.notDirectory", actions: [
|
||||
.init(title: %"common.close", style: .cancel)
|
||||
])
|
||||
}
|
||||
|
||||
let allowed = url.startAccessingSecurityScopedResource()
|
||||
print("Path - \(url) | write permissions - \(allowed)")
|
||||
|
||||
guard let bookmark = try? url.bookmarkData(options: [.minimalBookmark])
|
||||
else { return }
|
||||
|
||||
if let storage = viewModel.preferences.storageScopes.values.first(where: {
|
||||
$0.url == url || $0.url == TorrentService.downloadPath
|
||||
}) {
|
||||
storage.pathBookmark = bookmark
|
||||
return
|
||||
}
|
||||
|
||||
let storage = StorageModel()
|
||||
storage.uuid = UUID()
|
||||
storage.name = url.lastPathComponent
|
||||
storage.url = url
|
||||
storage.allowed = allowed
|
||||
storage.resolved = true
|
||||
|
||||
do {
|
||||
let name = try url.resourceValues(forKeys: [.localizedNameKey])
|
||||
if let name = name.allValues[.localizedNameKey] as? String {
|
||||
storage.name = name
|
||||
}
|
||||
} catch {}
|
||||
|
||||
storage.pathBookmark = bookmark
|
||||
|
||||
withAnimation {
|
||||
viewModel.preferences.storageScopes[storage.uuid] = storage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,18 +11,18 @@ import UIKit
|
||||
class TorrentAddViewController<VM: TorrentAddViewModel>: BaseViewController<VM> {
|
||||
@IBOutlet private var collectionView: UICollectionView!
|
||||
private lazy var delegates = Deletates(parent: self)
|
||||
private lazy var dataPickerDelegate = DataPickerDelegate(parent: self)
|
||||
private let cancelButton = UIBarButtonItem(systemItem: .close)
|
||||
private let downloadButton = UIModernBarButtonItem(image: .init(systemName: "arrow.down"))
|
||||
private let diskLabel = makeDiskLabel()
|
||||
private let moreButton = UIBarButtonItem(title: "More", image: .init(systemName: "ellipsis.circle"))
|
||||
private let priorityButton = UIBarButtonItem(title: %"prioriry.change.title", image: .init(resource: .icSort))
|
||||
private let storageButton = UIBarButtonItem(title: %"addTorrent.storage.selected", image: .init(systemName: "externaldrive"))
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = viewModel.title
|
||||
|
||||
moreButton.menu = .makeForChangePriority { [unowned self] priority in
|
||||
viewModel.setAllFilesPriority(priority)
|
||||
}
|
||||
updateMenu()
|
||||
|
||||
collectionView.register(TorrentFilesDictionaryItemViewCell<TorrentAddDirectoryItemViewModel>.self, forCellWithReuseIdentifier: TorrentFilesDictionaryItemViewCell<TorrentAddDirectoryItemViewModel>.reusableId)
|
||||
collectionView.register(type: TorrentFilesFileListCell<TorrentAddFileItemViewModel>.self, hasXib: false)
|
||||
@@ -45,7 +45,9 @@ class TorrentAddViewController<VM: TorrentAddViewModel>: BaseViewController<VM>
|
||||
toolbarItems = [
|
||||
.init(customView: diskLabel),
|
||||
.init(systemItem: .flexibleSpace),
|
||||
moreButton
|
||||
storageButton,
|
||||
.fixedSpace(16),
|
||||
priorityButton
|
||||
]
|
||||
|
||||
disposeBag.bind {
|
||||
@@ -75,6 +77,36 @@ private extension TorrentAddViewController {
|
||||
label.font = .preferredFont(forTextStyle: .callout)
|
||||
return label
|
||||
}
|
||||
|
||||
func updateMenu() {
|
||||
priorityButton.menu = UIMenu.makeForChangePriority { [unowned self] priority in
|
||||
viewModel.setAllFilesPriority(priority)
|
||||
}
|
||||
|
||||
storageButton.menu = UIMenu(
|
||||
title: %"addTorrent.storage.selected",
|
||||
image: .init(systemName: "externaldrive"),
|
||||
children:
|
||||
[
|
||||
UIAction(title: %"addTorrent.storage.manage") { [unowned self] _ in
|
||||
viewModel.navigate(to: StoragePreferencesViewModel.self, by: .present(wrapInNavigation: true))
|
||||
}
|
||||
] +
|
||||
viewModel.storages.map { storage in
|
||||
var attributes: UIMenuElement.Attributes = []
|
||||
var image: UIImage?
|
||||
|
||||
if !storage.allowed {
|
||||
attributes = .disabled
|
||||
image = .init(systemName: "exclamationmark.triangle.fill")?.withTintColor(.systemRed, renderingMode: .alwaysOriginal)
|
||||
}
|
||||
|
||||
return UIAction(title: storage.name, image: image, attributes: attributes, state: storage.selected ? .on : .off) { [unowned self] _ in
|
||||
viewModel.downloadStorage.value = storage.uuid
|
||||
updateMenu()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private extension TorrentAddViewController {
|
||||
@@ -129,3 +161,11 @@ private extension TorrentAddViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension TorrentAddViewController {
|
||||
class DataPickerDelegate: DelegateObject<TorrentAddViewController>, UIDocumentPickerDelegate {
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
print(urls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,15 @@ class TorrentAddViewModel: BaseViewModelWith<TorrentAddViewModel.Config> {
|
||||
private(set) var isRoot: Bool = false
|
||||
|
||||
let updatePublisher = CurrentValueRelay<Void>(())
|
||||
let downloadStorage = CurrentValueRelay<UUID?>(nil)
|
||||
let downloadStorages = CurrentValueRelay<[StorageModel]>([])
|
||||
|
||||
override func prepare(with model: Config) {
|
||||
torrentFile = model.torrentFile
|
||||
isRoot = model.rootDirectory == nil
|
||||
rootDirectory = model.rootDirectory ?? generateRoot()
|
||||
downloadStorage.value = preferences.defaultStorage
|
||||
|
||||
keys = rootDirectory.storage
|
||||
.sorted(by: { first, second in
|
||||
let f = first.value.name
|
||||
@@ -49,6 +53,8 @@ class TorrentAddViewModel: BaseViewModelWith<TorrentAddViewModel.Config> {
|
||||
override func willAppear() {
|
||||
updatePublisher.send()
|
||||
}
|
||||
|
||||
@Injected private var preferences: PreferencesStorage
|
||||
}
|
||||
|
||||
extension TorrentAddViewModel {
|
||||
@@ -91,7 +97,7 @@ extension TorrentAddViewModel {
|
||||
}
|
||||
|
||||
func download() {
|
||||
TorrentService.shared.addTorrent(by: torrentFile)
|
||||
TorrentService.shared.addTorrent(by: torrentFile, at: downloadStorage.value)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@@ -113,6 +119,12 @@ extension TorrentAddViewModel {
|
||||
return "\(selected.bitrateToHumanReadable) / \(total.bitrateToHumanReadable)"
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
var storages: [(name: String, selected: Bool, uuid: UUID?, allowed: Bool)] {
|
||||
[(StorageModel.defaultName, downloadStorage.value == nil, nil, true)] +
|
||||
preferences.storageScopes.values.sorted(by: { $0.name.localizedStandardCompare($1.name) == .orderedAscending })
|
||||
.map { ($0.name, downloadStorage.value == $0.uuid, $0.uuid, $0.allowed ) }
|
||||
}
|
||||
}
|
||||
|
||||
private extension TorrentAddViewModel {
|
||||
@@ -152,7 +164,7 @@ extension TorrentAddViewModel {
|
||||
}
|
||||
|
||||
private static func presentAlert(from navigationContext: NavigationProtocol, ifTorrentExists torrentFile: TorrentFile) -> Bool {
|
||||
guard TorrentService.shared.torrents.contains(where: { $0.snapshot.infoHashes == torrentFile.infoHashes })
|
||||
guard TorrentService.shared.torrents[torrentFile.infoHashes] != nil
|
||||
else { return false }
|
||||
|
||||
let alert = UIAlertController(title: %"addTorrent.exists", message: %"addTorrent.\(torrentFile.infoHashes.best.hex)_exists", preferredStyle: .alert)
|
||||
|
||||
@@ -41,9 +41,12 @@ class TorrentDetailsViewController<VM: TorrentDetailsViewModel>: BaseViewControl
|
||||
shareButton.isEnabled = available
|
||||
}
|
||||
|
||||
viewModel.$isPaused.sink { [unowned self] isPaused in
|
||||
playButton.isEnabled = isPaused
|
||||
pauseButton.isEnabled = !isPaused
|
||||
viewModel.$canResume.sink { [unowned self] canResume in
|
||||
playButton.isEnabled = canResume
|
||||
}
|
||||
|
||||
viewModel.$canPause.sink { [unowned self] canPause in
|
||||
pauseButton.isEnabled = canPause
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ class TorrentDetailsViewModel: BaseViewModelWith<TorrentHandle> {
|
||||
@Published var sections: [MvvmCollectionSectionModel] = []
|
||||
@Published var title: String = ""
|
||||
@Published var isPaused: Bool = false
|
||||
@Published var canResume: Bool = false
|
||||
@Published var canPause: Bool = false
|
||||
|
||||
@Published private var storageError: Bool = false
|
||||
|
||||
let dismissSignal = PassthroughSubject<Void, Never>()
|
||||
|
||||
@@ -46,6 +50,25 @@ class TorrentDetailsViewModel: BaseViewModelWith<TorrentHandle> {
|
||||
sequentialModel.$isOn.sink { [unowned self] value in
|
||||
torrentHandle.setSequentialDownload(value)
|
||||
}
|
||||
|
||||
$storageError.removeDuplicates().sink { [unowned self] error in
|
||||
runOnMainThreadIfNeeded { [self] in
|
||||
downloadPathModel.accessories = error ?
|
||||
[
|
||||
.image(.init(systemName: "exclamationmark.triangle.fill"), options: .init(tintColor: .systemRed))
|
||||
] :
|
||||
[
|
||||
// .popUpMenu(
|
||||
// .init(title: %"details.path.migrate", children: [
|
||||
// UIAction(title: "Default", state: .off) { _ in },
|
||||
// UIAction(title: "Browse", state: .off) { _ in },
|
||||
// ]), options: .init(tintColor: .tintColor)
|
||||
// )
|
||||
]
|
||||
|
||||
downloadPathModel.selectAction = nil //error ? nil : {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hashModel.longPressAction = { [unowned self] in
|
||||
@@ -84,6 +107,7 @@ class TorrentDetailsViewModel: BaseViewModelWith<TorrentHandle> {
|
||||
private let commentModel = DetailCellViewModel(title: %"details.info.comment", spacer: 80)
|
||||
private let createdModel = DetailCellViewModel(title: %"details.info.created")
|
||||
private let addedModel = DetailCellViewModel(title: %"details.info.added")
|
||||
private let downloadPath2Model = DetailCellViewModel(title: "Download Path")
|
||||
|
||||
private let selectedModel = DetailCellViewModel(title: %"details.transfer.selectedTotal")
|
||||
private let completedModel = DetailCellViewModel(title: %"details.transfer.completed")
|
||||
@@ -93,12 +117,17 @@ class TorrentDetailsViewModel: BaseViewModelWith<TorrentHandle> {
|
||||
private let seedersModel = DetailCellViewModel(title: %"details.transfer.seeders")
|
||||
private let leechersModel = DetailCellViewModel(title: %"details.transfer.leechers")
|
||||
|
||||
private lazy var downloadPathModel = PRButtonViewModel(with: .init(title: %"details.path.browse", isBold: true, value: nil))
|
||||
|
||||
private lazy var trackersModel = DetailCellViewModel(title: %"details.actions.trackers") { [unowned self] in
|
||||
navigate(to: TorrentTrackersViewModel.self, with: torrentHandle, by: .show)
|
||||
}
|
||||
private lazy var filesModel = DetailCellViewModel(title: %"details.actions.files") { [unowned self] in
|
||||
navigate(to: TorrentFilesViewModel.self, with: .init(torrentHandle: torrentHandle), by: .show)
|
||||
}
|
||||
|
||||
@Injected private var torrentService: TorrentService
|
||||
@Injected private var preferences: PreferencesStorage
|
||||
}
|
||||
|
||||
extension TorrentDetailsViewModel {
|
||||
@@ -117,10 +146,30 @@ extension TorrentDetailsViewModel {
|
||||
}
|
||||
|
||||
func rehash() {
|
||||
// If Storage is not available, try to reconnect storage
|
||||
if torrentHandle.snapshot.friendlyState == .storageError {
|
||||
return refreshStorage()
|
||||
}
|
||||
|
||||
alert(title: %"details.rehash.title", message: %"details.rehash.message", actions: [
|
||||
.init(title: %"common.cancel", style: .cancel),
|
||||
.init(title: %"details.rehash.action", style: .destructive, action: { [unowned self] in
|
||||
torrentHandle.rehash()
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
func refreshStorage() {
|
||||
guard let storage = torrentHandle.storage else { return }
|
||||
alert(title: %"details.refreshStorage.title", message: %"details.refreshStorage.message", actions: [
|
||||
.init(title: %"common.cancel", style: .cancel),
|
||||
.init(title: %"common.continue", style: .default, action: { [self] in
|
||||
Task {
|
||||
guard !torrentService.refreshStorage(storage) else { return }
|
||||
alert(title: %"common.error", message: %"details.refreshStorage.fail.message", actions: [
|
||||
.init(title: %"common.close", style: .cancel)
|
||||
])
|
||||
}
|
||||
})
|
||||
])
|
||||
}
|
||||
@@ -133,7 +182,7 @@ extension TorrentDetailsViewModel {
|
||||
.init(title: %"torrent.remove.action.keepData", style: .default, action: { [unowned self] in
|
||||
TorrentService.shared.removeTorrent(by: torrentHandle.snapshot.infoHashes, deleteFiles: false)
|
||||
}),
|
||||
.init(title: %"common.cancel", style: .cancel)
|
||||
.init(title: %"common.cancel", style: .cancel),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -154,7 +203,11 @@ extension TorrentDetailsViewModel {
|
||||
private extension TorrentDetailsViewModel {
|
||||
func dataUpdate() {
|
||||
isPaused = torrentHandle.snapshot.isPaused
|
||||
stateModel.detail = "\(torrentHandle.snapshot.friendlyState.name)" // "\(torrentHandle.snapshot.state.rawValue) | \(torrentHandle.snapshot.isPaused ? "Paused" : "Running")"
|
||||
canResume = torrentHandle.snapshot.canResume
|
||||
canPause = torrentHandle.snapshot.canPause
|
||||
storageError = torrentHandle.snapshot.friendlyState == .storageError
|
||||
|
||||
stateModel.detail = torrentHandle.snapshot.friendlyState.name
|
||||
|
||||
downloadModel.detail = "\(torrentHandle.snapshot.downloadRate.bitrateToHumanReadable)/s"
|
||||
uploadModel.detail = "\(torrentHandle.snapshot.uploadRate.bitrateToHumanReadable)/s"
|
||||
@@ -191,6 +244,11 @@ private extension TorrentDetailsViewModel {
|
||||
uploadedModel.detail = "\(torrentHandle.snapshot.totalUpload.bitrateToHumanReadable)"
|
||||
seedersModel.detail = "\(torrentHandle.snapshot.numberOfSeeds)(\(torrentHandle.snapshot.numberOfTotalSeeds))"
|
||||
leechersModel.detail = "\(torrentHandle.snapshot.numberOfLeechers)(\(torrentHandle.snapshot.numberOfTotalLeechers))"
|
||||
|
||||
downloadPath2Model.detail = torrentHandle.snapshot.downloadPath.path()
|
||||
downloadPathModel.value = torrentHandle.snapshot.storage.name
|
||||
|
||||
filesModel.isEnabled = torrentHandle.snapshot.friendlyState != .storageError
|
||||
}
|
||||
|
||||
func reload() {
|
||||
@@ -253,6 +311,13 @@ private extension TorrentDetailsViewModel {
|
||||
leechersModel
|
||||
})
|
||||
|
||||
if !preferences.storageScopes.isEmpty {
|
||||
sections.append(.init(id: "path", header: %"details.path") {
|
||||
// downloadPath2Model
|
||||
downloadPathModel
|
||||
})
|
||||
}
|
||||
|
||||
sections.append(.init(id: "actions", header: %"details.actions") {
|
||||
trackersModel
|
||||
filesModel
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ class TorrentFilesFileItemViewModel: BaseViewModelWith<(TorrentHandle, Int)>, Mv
|
||||
}
|
||||
|
||||
var path: URL {
|
||||
TorrentService.downloadPath.appending(path: file.path)
|
||||
torrentHandle.snapshot.downloadPath.appending(path: file.path)
|
||||
}
|
||||
|
||||
func setPriority(_ priority: FileEntry.Priority) {
|
||||
|
||||
+5
-5
@@ -57,17 +57,17 @@ class TorrentFilesFileListCell<VM: FileItemViewModelProtocol>: MvvmCollectionVie
|
||||
|
||||
disposeBag.bind {
|
||||
viewModel.updatePublisher
|
||||
.receive(on: DispatchQueue.global(qos: .userInitiated))
|
||||
.receive(on: .global(qos: .userInitiated))
|
||||
.sink
|
||||
{ [weak self] _ in
|
||||
self?.reload()
|
||||
{ [weak self, viewModel] _ in
|
||||
self?.reload(with: viewModel)
|
||||
}
|
||||
viewModel.selected.sink { [unowned self] _ in
|
||||
switchView.setOn(!switchView.isOn, animated: true)
|
||||
switcher(switchView)
|
||||
}
|
||||
}
|
||||
reload()
|
||||
reload(with: viewModel)
|
||||
}
|
||||
|
||||
func shareAction() {
|
||||
@@ -96,7 +96,7 @@ class TorrentFilesFileListCell<VM: FileItemViewModelProtocol>: MvvmCollectionVie
|
||||
}
|
||||
|
||||
private extension TorrentFilesFileListCell {
|
||||
func reload() {
|
||||
func reload(with viewModel: VM) {
|
||||
let file = viewModel.file
|
||||
|
||||
let percent = "\(String(format: "%.2f", file.progress * 100))%"
|
||||
|
||||
@@ -139,7 +139,8 @@ private extension TorrentFilesViewController {
|
||||
// @MainActor
|
||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||
let path = parent.viewModel.filesForPreview[index].path
|
||||
return TorrentService.downloadPath.appending(path: path) as NSURL
|
||||
let res = parent.viewModel.downloadPath.appending(path: path).standardized as NSURL
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +149,7 @@ private extension TorrentFilesViewController {
|
||||
else { return }
|
||||
|
||||
let path = viewModel.filesForPreview[startIndex].path
|
||||
let url = TorrentService.downloadPath.appending(path: path)
|
||||
let url = viewModel.downloadPath.appending(path: path)
|
||||
|
||||
Task {
|
||||
// Allow to choose be
|
||||
@@ -183,7 +184,7 @@ private extension TorrentFilesViewController {
|
||||
} catch {}
|
||||
|
||||
let path = viewModel.filesForPreview[startIndex].path
|
||||
let url = TorrentService.downloadPath.appending(path: path)
|
||||
let url = viewModel.downloadPath.appending(path: path)
|
||||
let player = AVPlayer(url: url)
|
||||
let playerController = AVPlayerViewController.resolve()
|
||||
playerController.canStartPictureInPictureAutomaticallyFromInline = true
|
||||
|
||||
@@ -102,6 +102,10 @@ extension TorrentFilesViewModel {
|
||||
keys.count
|
||||
}
|
||||
|
||||
var downloadPath: URL {
|
||||
torrentHandle.snapshot.downloadPath
|
||||
}
|
||||
|
||||
var filesForPreview: [FileEntry] {
|
||||
keys.flatMap {
|
||||
switch rootDirectory.storage[$0] {
|
||||
|
||||
@@ -157,10 +157,10 @@ class TorrentListViewController<VM: TorrentListViewModel>: BaseViewController<VM
|
||||
return UIContextMenuConfiguration {
|
||||
TorrentDetailsViewModel.resolveVC(with: torrentHandle)
|
||||
} actionProvider: { _ in
|
||||
let start = UIAction(title: %"details.start", image: .init(systemName: "play.fill"), attributes: torrentHandle.snapshot.isPaused ? [] : .hidden, handler: { _ in
|
||||
let start = UIAction(title: %"details.start", image: .init(systemName: "play.fill"), attributes: torrentHandle.snapshot.canResume ? [] : .hidden, handler: { _ in
|
||||
torrentHandle.resume()
|
||||
})
|
||||
let pause = UIAction(title: %"details.pause", image: .init(systemName: "pause.fill"), attributes: !torrentHandle.snapshot.isPaused ? [] : .hidden, handler: { _ in
|
||||
let pause = UIAction(title: %"details.pause", image: .init(systemName: "pause.fill"), attributes: torrentHandle.snapshot.canPause ? [] : .hidden, handler: { _ in
|
||||
torrentHandle.pause()
|
||||
})
|
||||
let delete = UIAction(title: %"common.delete", image: UIImage(systemName: "trash.fill"), attributes: .destructive) { [unowned self] _ in
|
||||
@@ -317,14 +317,7 @@ extension TorrentListViewController {
|
||||
return
|
||||
}
|
||||
|
||||
guard !TorrentService.shared.checkTorrentExists(with: torrentFile.infoHashes) else {
|
||||
let alert = UIAlertController(title: %"addTorrent.exists", message: %"addTorrent.\(torrentFile.infoHashes.best.hex)_exists", preferredStyle: .alert)
|
||||
alert.addAction(.init(title: %"common.close", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
TorrentService.shared.addTorrent(by: torrentFile)
|
||||
TorrentAddViewModel.present(with: torrentFile, from: self)
|
||||
}
|
||||
})
|
||||
return alert
|
||||
|
||||
@@ -68,7 +68,7 @@ class TorrentListViewModel: BaseViewModel {
|
||||
|
||||
Publishers.combineLatest(
|
||||
torrentSectionChanged,
|
||||
TorrentService.shared.$torrents,
|
||||
TorrentService.shared.$torrents.map { Array($0.values) },
|
||||
$searchQuery,
|
||||
sortingType,
|
||||
sortingReverced,
|
||||
@@ -124,7 +124,10 @@ extension TorrentListViewModel {
|
||||
|
||||
func resumeAllSelected(at indexPaths: [IndexPath]) {
|
||||
let torrentModels = indexPaths.compactMap { sections[$0.section].items[$0.item] as? TorrentListItemViewModel }
|
||||
torrentModels.forEach { $0.torrentHandle.resume() }
|
||||
torrentModels.forEach {
|
||||
guard $0.torrentHandle.snapshot.canResume else { return }
|
||||
$0.torrentHandle.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func pauseAllSelected(at indexPaths: [IndexPath]) {
|
||||
|
||||
@@ -63,7 +63,7 @@ class BackgroundService: BackgroundServiceProtocol {
|
||||
// MARK: Backgroud requirements
|
||||
extension BackgroundService {
|
||||
static var isBackgroundNeeded: Bool {
|
||||
TorrentService.shared.torrents.contains(where: { $0.snapshot.needBackground })
|
||||
TorrentService.shared.torrents.values.contains(where: { $0.snapshot.needBackground })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ actor IntentsService {
|
||||
disposeBag.bind {
|
||||
NotificationCenter.default.publisher(for: .pauseTorrent).sink { notification in
|
||||
guard let hash = notification.object as? String,
|
||||
let torrentHandle = TorrentService.shared.torrents.first(where: { $0.snapshot.infoHashes.best.hex == hash })
|
||||
let torrentHandle = TorrentService.shared.torrents.values.first(where: { $0.snapshot.infoHashes.best.hex == hash })
|
||||
else { return }
|
||||
|
||||
torrentHandle.pause()
|
||||
|
||||
@@ -99,13 +99,15 @@ private extension TorrentHandle.State {
|
||||
return .checkingResumeData
|
||||
case .paused:
|
||||
return .paused
|
||||
case .storageError:
|
||||
return .storageError
|
||||
@unknown default:
|
||||
fatalError("\(ProgressWidgetAttributes.State.self) has no such case \(self)")
|
||||
}
|
||||
}
|
||||
|
||||
var shouldShowLiveActivity: Bool {
|
||||
let notShow: [Self] = [.finished, .paused]
|
||||
let notShow: [Self] = [.finished, .paused, .storageError]
|
||||
return !notShow.contains(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,17 @@ class PreferencesStorage: Resolvable {
|
||||
// Location mode is not allowed by Apple policy
|
||||
backgroundMode = .audio
|
||||
#endif
|
||||
|
||||
// Sanity check for defaultStorage if something went wrong
|
||||
if defaultStorage != nil, !storageScopes.contains(where: { $0.key == defaultStorage }) { defaultStorage = nil }
|
||||
|
||||
// Fix sorting array in case new status appeared
|
||||
if torrentListGroupsSortingArray.count != Self.defaultTorrentListGroupsSortingArray.count {
|
||||
torrentListGroupsSortingArray = Self.defaultTorrentListGroupsSortingArray
|
||||
}
|
||||
|
||||
// TODO: REMOVE LATER!!!
|
||||
isStorageRulesAccepted = false
|
||||
}
|
||||
|
||||
private var disposeBag: [AnyCancellable] = []
|
||||
@@ -27,12 +38,17 @@ class PreferencesStorage: Resolvable {
|
||||
.checkingFiles,
|
||||
.downloadingMetadata,
|
||||
.downloading,
|
||||
.finished,
|
||||
.seeding,
|
||||
.finished,
|
||||
.checkingResumeData,
|
||||
.paused
|
||||
.paused,
|
||||
.storageError
|
||||
]
|
||||
|
||||
@UserDefaultItem("torrentDefaultStorage", nil) var defaultStorage: UUID?
|
||||
@UserDefaultItem("torrentIsStorageRulesAccepted", false) var isStorageRulesAccepted: Bool
|
||||
@UserDefaultItem("torrentStorageScopes", [:]) var storageScopes: [UUID: StorageModel]
|
||||
|
||||
@UserDefaultItem("torrentListSortType", .alphabetically) var torrentListSortType: TorrentListViewModel.Sort
|
||||
@UserDefaultItem("torrentListSortReverced", false) var torrentListSortReverced: Bool
|
||||
@UserDefaultItem("torrentListIsGroubedByState", false) var torrentListGroupedByState: Bool
|
||||
@@ -99,7 +115,6 @@ class PreferencesStorage: Resolvable {
|
||||
|
||||
var settingsUpdatePublisher: AnyPublisher<Void, Never> {
|
||||
Just<Void>(())
|
||||
.combineLatest($allocateMemory)
|
||||
.combineLatest($maxActiveTorrents)
|
||||
.combineLatest($maxDownloadingTorrents)
|
||||
.combineLatest($maxUploadingTorrents)
|
||||
|
||||
@@ -17,12 +17,22 @@ class TorrentMonitoringService {
|
||||
disposeBag.bind {
|
||||
torrentService.updateNotifier.sink { [unowned self] updateModel in
|
||||
checkDoneNotification(with: updateModel)
|
||||
checkStorageAvailability(with: updateModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension TorrentMonitoringService {
|
||||
func checkStorageAvailability(with model: TorrentService.TorrentUpdateModel) {
|
||||
guard let storage = model.handle.storage,
|
||||
!storage.allowed,
|
||||
!model.handle.snapshot.isPaused
|
||||
else { return }
|
||||
|
||||
model.handle.pause()
|
||||
}
|
||||
|
||||
func checkDoneNotification(with model: TorrentService.TorrentUpdateModel) {
|
||||
guard PreferencesStorage.shared.isDownloadNotificationsEnabled,
|
||||
model.oldSnapshot.state != .checkingFiles,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// StorageModel+Extensions.swift
|
||||
// iTorrent
|
||||
//
|
||||
// Created by Daniil Vinogradov on 05/07/2024.
|
||||
//
|
||||
|
||||
import LibTorrent
|
||||
|
||||
extension StorageModel {
|
||||
static var defaultName: String { "iTorrent Default" }
|
||||
}
|
||||
|
||||
extension Optional where Wrapped: StorageModel {
|
||||
var name: String {
|
||||
switch self {
|
||||
case .none:
|
||||
return StorageModel.defaultName
|
||||
case .some(let wrapped):
|
||||
return wrapped.name
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -66,6 +66,10 @@ extension TorrentHandle {
|
||||
|
||||
extension TorrentHandle.Snapshot {
|
||||
var friendlyState: TorrentHandle.State {
|
||||
if isStorageMissing {
|
||||
return .storageError
|
||||
}
|
||||
|
||||
switch state {
|
||||
case .downloading:
|
||||
if isPaused { return .paused }
|
||||
@@ -80,6 +84,14 @@ extension TorrentHandle.Snapshot {
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
var canResume: Bool {
|
||||
isPaused && friendlyState != .storageError
|
||||
}
|
||||
|
||||
var canPause: Bool {
|
||||
!isPaused
|
||||
}
|
||||
}
|
||||
|
||||
extension TorrentHandle.State {
|
||||
@@ -99,6 +111,8 @@ extension TorrentHandle.State {
|
||||
return String(localized: "torrent.state.resuming")
|
||||
case .paused:
|
||||
return String(localized: "torrent.state.paused")
|
||||
case .storageError:
|
||||
return String(localized: "torrent.state.storageError")
|
||||
@unknown default:
|
||||
assertionFailure("Unregistered \(Self.self) enum value is not allowed: \(self)")
|
||||
return ""
|
||||
@@ -106,6 +120,14 @@ extension TorrentHandle.State {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Storage
|
||||
extension TorrentHandle.Snapshot {
|
||||
var storage: StorageModel? {
|
||||
guard let storageUUID else { return nil }
|
||||
return TorrentService.shared.storages[storageUUID]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Metadata
|
||||
extension TorrentHandle {
|
||||
struct Metadata: Codable {
|
||||
@@ -8,6 +8,7 @@
|
||||
import Combine
|
||||
import LibTorrent
|
||||
import MvvmFoundation
|
||||
import UIKit
|
||||
|
||||
extension TorrentService {
|
||||
struct TorrentUpdateModel {
|
||||
@@ -17,7 +18,7 @@ extension TorrentService {
|
||||
}
|
||||
|
||||
class TorrentService {
|
||||
@Published var torrents: [TorrentHandle] = []
|
||||
@Published var torrents: [TorrentHashes: TorrentHandle] = [:]
|
||||
var updateNotifier = PassthroughSubject<TorrentUpdateModel, Never>()
|
||||
|
||||
static let shared = TorrentService()
|
||||
@@ -30,10 +31,10 @@ class TorrentService {
|
||||
static var fastResumePath: URL { downloadPath.appending(path: "config") }
|
||||
static var metadataPath: URL { downloadPath.appending(path: "config") }
|
||||
|
||||
private let session: Session = {
|
||||
private lazy var session: Session = {
|
||||
var settings = Session.Settings()
|
||||
print("Working directory: \(downloadPath.path())")
|
||||
return .init(downloadPath.path(), torrentsPath: torrentPath.path(), fastResumePath: fastResumePath.path(), settings: .fromPreferences(with: []))
|
||||
print("Working directory: \(Self.downloadPath.path())")
|
||||
return .init(Self.downloadPath.path(), torrentsPath: Self.torrentPath.path(), fastResumePath: Self.fastResumePath.path(), settings: .fromPreferences(with: []), storages: PreferencesStorage.shared.storageScopes)
|
||||
}()
|
||||
|
||||
private let disposeBag = DisposeBag()
|
||||
@@ -43,35 +44,26 @@ class TorrentService {
|
||||
}
|
||||
|
||||
extension TorrentService {
|
||||
var storages: Dictionary<UUID, StorageModel> {
|
||||
get { session.storages }
|
||||
set { session.storages = newValue }
|
||||
}
|
||||
|
||||
func checkTorrentExists(with hash: TorrentHashes) -> Bool {
|
||||
torrents.contains(where: { $0.snapshot.infoHashes == hash })
|
||||
session.torrentsMap[hash] != nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addTorrent(by file: Downloadable) -> Bool {
|
||||
guard !torrents.contains(where: { file.infoHashes == $0.snapshot.infoHashes })
|
||||
func addTorrent(by file: Downloadable, at storage: UUID? = nil) -> Bool {
|
||||
guard session.torrentsMap[file.infoHashes] == nil
|
||||
else { return false }
|
||||
|
||||
session.addTorrent(file)
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addTorrent(by path: URL) -> Bool {
|
||||
defer { path.stopAccessingSecurityScopedResource() }
|
||||
guard path.startAccessingSecurityScopedResource(),
|
||||
let file = TorrentFile(with: path)
|
||||
else { return false }
|
||||
|
||||
guard !torrents.contains(where: { file.infoHashes == $0.snapshot.infoHashes })
|
||||
else { return false }
|
||||
|
||||
session.addTorrent(file)
|
||||
session.addTorrent(file, to: storage)
|
||||
return true
|
||||
}
|
||||
|
||||
func removeTorrent(by infoHashes: TorrentHashes, deleteFiles: Bool) {
|
||||
guard let handle = torrents.first(where: { $0.snapshot.infoHashes == infoHashes })
|
||||
guard let handle = session.torrentsMap[infoHashes]
|
||||
else { return }
|
||||
|
||||
handle.deleteMetadata()
|
||||
@@ -81,35 +73,33 @@ extension TorrentService {
|
||||
func updateSettings(_ settings: Session.Settings) {
|
||||
session.settings = settings
|
||||
}
|
||||
|
||||
func refreshStorage(_ storage: StorageModel) -> Bool {
|
||||
guard storage.resolveSequrityScopes() else { return false }
|
||||
|
||||
let handles = torrents.values.filter { $0.snapshot.storageUUID == storage.uuid }
|
||||
handles.forEach { $0.reload() }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SessionDelegate
|
||||
extension TorrentService: SessionDelegate {
|
||||
func torrentManager(_ manager: Session, didAddTorrent torrent: TorrentHandle) {
|
||||
// Do not use snapshot for 'torrent' because it is not generated yet
|
||||
guard torrents.firstIndex(where: { $0.snapshot.infoHashes == torrent.infoHashes }) == nil
|
||||
else { return }
|
||||
|
||||
torrent.prepareToAdd(into: self)
|
||||
torrents.append(torrent)
|
||||
torrents[torrent.snapshot.infoHashes] = torrent
|
||||
}
|
||||
|
||||
func torrentManager(_ manager: Session, didRemoveTorrentWithHash hashesData: TorrentHashes) {
|
||||
// Already on Main thread
|
||||
guard let index = torrents.firstIndex(where: { $0.snapshot.infoHashes == hashesData })
|
||||
guard let torrent = torrents[hashesData]
|
||||
else { return }
|
||||
|
||||
let torrent = torrents[index]
|
||||
torrent.removePublisher.send(torrent)
|
||||
torrents.remove(at: index)
|
||||
torrents[hashesData] = nil
|
||||
}
|
||||
|
||||
func torrentManager(_ manager: Session, didReceiveUpdateForTorrent torrent: TorrentHandle) {
|
||||
// Do not use snapshot for 'torrent' because it could be not generated yet
|
||||
guard let existingTorrent = torrents.first(where: { $0.snapshot.infoHashes == torrent.infoHashes })
|
||||
else { return }
|
||||
|
||||
existingTorrent.__unthrottledUpdatePublisher.send()
|
||||
torrent.__unthrottledUpdatePublisher.send()
|
||||
}
|
||||
|
||||
func torrentManager(_ manager: Session, didErrorOccur error: Error) {}
|
||||
@@ -117,9 +107,12 @@ extension TorrentService: SessionDelegate {
|
||||
|
||||
private extension TorrentService {
|
||||
func setup() {
|
||||
// Resolve storage scopes, so Core will be able to restore states
|
||||
resolveStorageScopes()
|
||||
|
||||
// Pause the core, so it will not deadlock app on making first snapshots
|
||||
session.pause();
|
||||
torrents = session.torrents.map { torrent in
|
||||
torrents = session.torrentsMap.mapValues { torrent in
|
||||
torrent.prepareToAdd(into: self)
|
||||
return torrent
|
||||
}
|
||||
@@ -139,6 +132,21 @@ private extension TorrentService {
|
||||
session.settings = Session.Settings.fromPreferences(with: interfaces)
|
||||
}
|
||||
}
|
||||
|
||||
preferences.$storageScopes.sink { [unowned self] storages in
|
||||
session.storages = storages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveStorageScopes() {
|
||||
preferences.storageScopes.values.forEach { scope in
|
||||
scope.resolveSequrityScopes()
|
||||
|
||||
// If storage is not allowed and it used as default, reset default
|
||||
if !scope.allowed, preferences.defaultStorage == scope.uuid {
|
||||
preferences.defaultStorage = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,19 @@ extension Publisher where Self.Failure == Never {
|
||||
/// - Returns: A cancellable instance, which you use when you end assignment of the received value. Deallocation of the result will tear down the subscription stream.
|
||||
public func uiSink(receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable {
|
||||
sink { value in
|
||||
if Thread.isMainThread {
|
||||
runOnMainThreadIfNeeded {
|
||||
receiveValue(value)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
receiveValue(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runOnMainThreadIfNeeded(_ action: @escaping () -> Void) {
|
||||
if Thread.isMainThread {
|
||||
action()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// UICellAccessory+Image.swift
|
||||
// iTorrent
|
||||
//
|
||||
// Created by Даниил Виноградов on 05.07.2024.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UICellAccessory {
|
||||
public struct ImageOptions {
|
||||
public var isHidden: Bool = false
|
||||
public var tintColor: UIColor? = nil
|
||||
}
|
||||
|
||||
static func image(_ image: UIImage?, displayed: UICellAccessory.DisplayedState = .always, options: ImageOptions = .init()) -> UICellAccessory {
|
||||
.customView(configuration: .init(customView: {
|
||||
if let tintColor = options.tintColor {
|
||||
return UIImageView(image: image?.withTintColor(tintColor, renderingMode: .alwaysOriginal))
|
||||
} else {
|
||||
return UIImageView(image: image)
|
||||
}
|
||||
}(), placement: .trailing(displayed: displayed, at: { _ in 0 })))
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import UIKit
|
||||
|
||||
extension UIMenu {
|
||||
static func makeForChangePriority( options: UIMenu.Options = [], _ setPriority: @escaping (FileEntry.Priority) -> ()) -> UIMenu {
|
||||
.init(title: %"prioriry.change.title", options: options, children: [
|
||||
.init(title: %"prioriry.change.title", image: .init(resource: .icSort), options: options, children: [
|
||||
UIAction(title: String(localized: "prioriry.top"), image: .init(systemName: "gauge.with.dots.needle.100percent"), handler: { _ in
|
||||
setPriority(.topPriority)
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// URL+Normalization.swift
|
||||
// iTorrent
|
||||
//
|
||||
// Created by Daniil Vinogradov on 05/07/2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
var isDirectory: Bool {
|
||||
(try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
|
||||
}
|
||||
}
|
||||
@@ -20,3 +20,14 @@ class SAViewController<ViewModel: MvvmViewModelProtocol>: MvvmViewController<Vie
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SAHostingViewController<View: MvvmSwiftUIViewProtocol>: MvvmHostingViewController<View> {
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if let nav = navigationController as? SANavigationController,
|
||||
nav.viewControllers.last == self
|
||||
{
|
||||
nav.locker = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,25 +13,22 @@ import MvvmFoundation
|
||||
struct UserDefaultItem<T: Codable> {
|
||||
private let key: String
|
||||
private let defaultValue: T
|
||||
private let value: CurrentValueSubject<T, Never>
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
var wrappedValue: T {
|
||||
get { value.value }
|
||||
set { value.value = newValue }
|
||||
}
|
||||
let projectedValue: CurrentValueSubject<T, Never>
|
||||
|
||||
var projectedValue: CurrentValueSubject<T, Never> {
|
||||
value
|
||||
var wrappedValue: T {
|
||||
get { projectedValue.value }
|
||||
set { projectedValue.value = newValue }
|
||||
}
|
||||
|
||||
init(_ key: String, _ defaultValue: T) {
|
||||
self.key = key
|
||||
self.defaultValue = defaultValue
|
||||
value = CurrentValueSubject(Self.get(by: key) ?? defaultValue)
|
||||
projectedValue = CurrentValueSubject(Self.get(by: key) ?? defaultValue)
|
||||
|
||||
disposeBag.bind {
|
||||
value.sink { value in
|
||||
projectedValue.sink { value in
|
||||
Self.set(by: key, value)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user