Ferrite: Forward port UI

Remove all iOS 14 specific components and workarounds and comply
with SwiftUI 3.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2023-03-29 14:43:49 -04:00
parent 8f9f522846
commit 3828ffa539
57 changed files with 738 additions and 1081 deletions

View file

@ -22,13 +22,10 @@
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; };
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; };
0C2D9653299316CC00A504B6 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2D9652299316CC00A504B6 /* Tag.swift */; };
0C2DD4CF29A6D47400293FC3 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 0C2DD4CE29A6D47400293FC3 /* SwiftUIX */; };
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; };
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; };
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; };
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; };
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; };
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */; };
0C3DD43F29B6968D006429DB /* KodiEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3DD43E29B6968D006429DB /* KodiEditorView.swift */; };
0C3DD44229B6ACD9006429DB /* KodiServer+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3DD44029B6ACD9006429DB /* KodiServer+CoreDataClass.swift */; };
0C3DD44329B6ACD9006429DB /* KodiServer+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3DD44129B6ACD9006429DB /* KodiServer+CoreDataProperties.swift */; };
@ -46,6 +43,7 @@
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2A728D4DDDC007711AE /* Application.swift */; };
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AC28D51C63007711AE /* BackupManager.swift */; };
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AE28D52E8A007711AE /* BackupsView.swift */; };
0C45E6A529D4B2FE00F047D2 /* SearchListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C45E6A429D4B2FE00F047D2 /* SearchListener.swift */; };
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; };
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; };
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */; };
@ -56,8 +54,6 @@
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; };
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; };
0C5708EB29B8F89300BE07F9 /* SettingsLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5708EA29B8F89300BE07F9 /* SettingsLogView.swift */; };
0C572D4C2993FC2A003EEC05 /* ViewDidAppearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */; };
0C572D4E299403B7003EEC05 /* ViewDidAppear.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */; };
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; };
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; };
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
@ -71,6 +67,8 @@
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.swift */; };
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */; };
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9C29315292002DF910 /* AllDebridModels.swift */; };
0C7075E429D374C50093DB2D /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7075E329D374C50093DB2D /* Color.swift */; };
0C7075E629D3845D0093DB2D /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7075E529D3845D0093DB2D /* ShareSheet.swift */; };
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */; };
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */; };
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; };
@ -127,6 +125,7 @@
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA429F728C5098D000D0610 /* DateFormatter.swift */; };
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAF9318296399190050812A /* PremiumizeCloudView.swift */; };
0CB0115B29D36D9E009AFEDE /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB0115A29D36D9E009AFEDE /* SearchResultsView.swift */; };
0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB0AB5E29BD2A200015422C /* KodiServerView.swift */; };
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; };
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; };
@ -145,9 +144,9 @@
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; };
0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */ = {isa = PBXBuildFile; productRef = 0CDDDE042935235E006810B1 /* BetterSafariView */; };
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1C4172981E8D700418F20 /* Plugin.swift */; };
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE66B3928E640D200F69346 /* Backport.swift */; };
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */; };
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */; };
0CF2C0A529D1EBD400E716DD /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF2C0A429D1EBD400E716DD /* UIApplication.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -170,8 +169,6 @@
0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
0C32FB562890D1F2002BD219 /* ListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViews.swift; sourceTree = "<group>"; };
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicFetchRequest.swift; sourceTree = "<group>"; };
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = "<group>"; };
0C3DD43E29B6968D006429DB /* KodiEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiEditorView.swift; sourceTree = "<group>"; };
0C3DD44029B6ACD9006429DB /* KodiServer+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KodiServer+CoreDataClass.swift"; sourceTree = "<group>"; };
0C3DD44129B6ACD9006429DB /* KodiServer+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KodiServer+CoreDataProperties.swift"; sourceTree = "<group>"; };
@ -189,6 +186,7 @@
0C44E2A728D4DDDC007711AE /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
0C44E2AC28D51C63007711AE /* BackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupManager.swift; sourceTree = "<group>"; };
0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.swift; sourceTree = "<group>"; };
0C45E6A429D4B2FE00F047D2 /* SearchListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchListener.swift; sourceTree = "<group>"; };
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = "<group>"; };
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C5005512992B6750064606A /* PluginTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginTagsView.swift; sourceTree = "<group>"; };
@ -198,8 +196,6 @@
0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = "<group>"; };
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C5708EA29B8F89300BE07F9 /* SettingsLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLogView.swift; sourceTree = "<group>"; };
0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppearHandler.swift; sourceTree = "<group>"; };
0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppear.swift; sourceTree = "<group>"; };
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; };
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = "<group>"; };
0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiWrapper.swift; sourceTree = "<group>"; };
@ -211,6 +207,8 @@
0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = "<group>"; };
0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridWrapper.swift; sourceTree = "<group>"; };
0C6C7C9C29315292002DF910 /* AllDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridModels.swift; sourceTree = "<group>"; };
0C7075E329D374C50093DB2D /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
0C7075E529D3845D0093DB2D /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = "<group>"; };
0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalContextMenu.swift; sourceTree = "<group>"; };
0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenter.swift; sourceTree = "<group>"; };
0C733286289C4C820058D1FE /* SourceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsView.swift; sourceTree = "<group>"; };
@ -266,6 +264,7 @@
0CA429F728C5098D000D0610 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = "<group>"; };
0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; };
0CAF9318296399190050812A /* PremiumizeCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeCloudView.swift; sourceTree = "<group>"; };
0CB0115A29D36D9E009AFEDE /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
0CB0AB5E29BD2A200015422C /* KodiServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiServerView.swift; sourceTree = "<group>"; };
0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = "<group>"; };
0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = "<group>"; };
@ -284,9 +283,9 @@
0CD72E16293D9928001A7EA4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = "<group>"; };
0CE1C4172981E8D700418F20 /* Plugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plugin.swift; sourceTree = "<group>"; };
0CE66B3928E640D200F69346 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFilterHeaderView.swift; sourceTree = "<group>"; };
0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryPickerView.swift; sourceTree = "<group>"; };
0CF2C0A429D1EBD400E716DD /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -297,7 +296,6 @@
0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */,
0C448BE929A135F100F4E266 /* Introspect-Static in Frameworks */,
0C64A4B4288903680079976D /* Base32 in Frameworks */,
0C2DD4CF29A6D47400293FC3 /* SwiftUIX in Frameworks */,
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */,
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */,
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */,
@ -448,7 +446,7 @@
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */,
0CB6516428C5A5D700DCA721 /* InlinedList.swift */,
0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */,
0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */,
0C45E6A429D4B2FE00F047D2 /* SearchListener.swift */,
);
path = Modifiers;
sourceTree = "<group>";
@ -468,6 +466,7 @@
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */,
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */,
0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */,
0CB0115A29D36D9E009AFEDE /* SearchResultsView.swift */,
);
path = SearchResult;
sourceTree = "<group>";
@ -529,9 +528,6 @@
isa = PBXGroup;
children = (
0C44E2A928D4DFC4007711AE /* Modifiers */,
0CE66B3928E640D200F69346 /* Backport.swift */,
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */,
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */,
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */,
0C871BDE29994D9D005279AC /* FilterLabelView.swift */,
0CA148C1288903F000DE2211 /* NavView.swift */,
@ -558,6 +554,7 @@
0CD72E16293D9928001A7EA4 /* Array.swift */,
0C445C61293F9A0B0060744D /* Bundle.swift */,
0CA148C9288903F000DE2211 /* Collection.swift */,
0C7075E329D374C50093DB2D /* Color.swift */,
0CA148CA288903F000DE2211 /* Data.swift */,
0CA429F728C5098D000D0610 /* DateFormatter.swift */,
0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */,
@ -569,6 +566,7 @@
0C42B5972932F6DD008057A0 /* Set.swift */,
0C7C128528DAA3CD00381CD1 /* URL.swift */,
0C50B7CF299DF63C00A9FA3C /* UIDevice.swift */,
0CF2C0A429D1EBD400E716DD /* UIApplication.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -608,7 +606,7 @@
isa = PBXGroup;
children = (
0CA148CE288903F000DE2211 /* WebView.swift */,
0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */,
0C7075E529D3845D0093DB2D /* ShareSheet.swift */,
);
path = RepresentableViews;
sourceTree = "<group>";
@ -690,7 +688,6 @@
0C7506D628B1AC9A008BEE38 /* SwiftyJSON */,
0CDDDE042935235E006810B1 /* BetterSafariView */,
0C448BE829A135F100F4E266 /* Introspect-Static */,
0C2DD4CE29A6D47400293FC3 /* SwiftUIX */,
);
productName = Torrenter;
productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */;
@ -728,7 +725,6 @@
0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */,
0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
0C2DD4CD29A6D47400293FC3 /* XCRemoteSwiftPackageReference "SwiftUIX" */,
);
productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */;
projectDirPath = "";
@ -803,11 +799,10 @@
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */,
0CA0545B288EEA4E00850554 /* PluginListEditorView.swift in Sources */,
0C3E00D8296F5B9A00ECECB2 /* PluginModels.swift in Sources */,
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */,
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */,
0C5005522992B6750064606A /* PluginTagsView.swift in Sources */,
0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */,
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */,
0CF2C0A529D1EBD400E716DD /* UIApplication.swift in Sources */,
0C42B5962932F2D5008057A0 /* DebridPickerView.swift in Sources */,
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */,
0C3DD44229B6ACD9006429DB /* KodiServer+CoreDataClass.swift in Sources */,
@ -831,7 +826,6 @@
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */,
0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */,
0C572D4C2993FC2A003EEC05 /* ViewDidAppearHandler.swift in Sources */,
0C95D8D828A55B03005E22B3 /* DefaultActionPickerView.swift in Sources */,
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */,
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
@ -877,12 +871,15 @@
0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */,
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */,
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */,
0C45E6A529D4B2FE00F047D2 /* SearchListener.swift in Sources */,
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */,
0C7075E629D3845D0093DB2D /* ShareSheet.swift in Sources */,
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */,
0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */,
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */,
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
0C7075E429D374C50093DB2D /* Color.swift in Sources */,
0C8DC35229CE287E008A83AD /* PluginInfoView.swift in Sources */,
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */,
0C8DC35629CE2ABF008A83AD /* SourceSettingsApiView.swift in Sources */,
@ -890,10 +887,10 @@
0C0974B029CCAAAF006DE7A3 /* OperatingSystemVersion.swift in Sources */,
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
0C572D4E299403B7003EEC05 /* ViewDidAppear.swift in Sources */,
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */,
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */,
0CB0115B29D36D9E009AFEDE /* SearchResultsView.swift in Sources */,
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */,
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */,
0C6771F629B3B602005D38D2 /* SettingsKodiView.swift in Sources */,
@ -902,7 +899,6 @@
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */,
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */,
0C3DD43F29B6968D006429DB /* KodiEditorView.swift in Sources */,
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */,
0C3DD44329B6ACD9006429DB /* KodiServer+CoreDataProperties.swift in Sources */,
0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */,
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */,
@ -1124,14 +1120,6 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
0C2DD4CD29A6D47400293FC3 /* XCRemoteSwiftPackageReference "SwiftUIX" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SwiftUIX/SwiftUIX";
requirement = {
branch = master;
kind = branch;
};
};
0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect/";
@ -1191,11 +1179,6 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
0C2DD4CE29A6D47400293FC3 /* SwiftUIX */ = {
isa = XCSwiftPackageProductDependency;
package = 0C2DD4CD29A6D47400293FC3 /* XCRemoteSwiftPackageReference "SwiftUIX" */;
productName = SwiftUIX;
};
0C448BE829A135F100F4E266 /* Introspect-Static */ = {
isa = XCSwiftPackageProductDependency;
package = 0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;

View file

@ -0,0 +1,35 @@
//
// Color.swift
// Ferrite
//
// Created by Brian Dashore on 3/28/23.
//
import SwiftUI
extension Color {
public init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}

View file

@ -0,0 +1,15 @@
//
// UIApplication.swift
// Ferrite
//
// Created by Brian Dashore on 3/27/23.
//
import UIKit
extension UIApplication {
// From https://stackoverflow.com/questions/69650504/how-to-get-rid-of-message-windows-was-deprecated-in-ios-15-0-use-uiwindowsc
var currentUIWindow: UIWindow? {
return UIApplication.shared.connectedScenes.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }.first { $0.isKeyWindow }
}
}

View file

@ -5,14 +5,14 @@
// Created by Brian Dashore on 2/16/23.
//
import SwiftUI
import UIKit
extension UIDevice {
var hasNotch: Bool {
if #available(iOS 11.0, *) {
let keyWindow = UIApplication.shared.windows.filter(\.isKeyWindow).first
return keyWindow?.safeAreaInsets.bottom ?? 0 > 0
return UIApplication.shared.currentUIWindow?.safeAreaInsets.bottom ?? 0 > 0
} else {
return false
}
return false
}
}

View file

@ -33,11 +33,11 @@ extension View {
modifier(InlinedListModifier(inset: inset))
}
func viewDidAppear(_ callback: @escaping () -> Void) -> some View {
modifier(ViewDidAppearModifier(callback: callback))
}
func customScopeBar(_ content: @escaping () -> some View) -> some View {
modifier(CustomScopeBarModifier(scopeBarContent: content()))
}
func searchListener(isSearching: Binding<Bool>) -> some View {
modifier(SearchListenerModifier(isSearching: isSearching))
}
}

View file

@ -21,7 +21,7 @@ struct FerriteApp: App {
var body: some Scene {
WindowGroup {
MainView()
.backport.onAppear {
.onAppear {
scrapingModel.logManager = logManager
debridManager.logManager = logManager
pluginManager.logManager = logManager

View file

@ -186,14 +186,7 @@ public class BackupManager: ObservableObject {
PersistenceController.shared.save(backgroundContext)
// if iOS 14 is available, sleep to prevent any issues with alerts
if #available(iOS 15, *) {
await toggleRestoreCompletedAlert()
} else {
try? await Task.sleep(seconds: 0.1)
await toggleRestoreCompletedAlert()
}
await toggleRestoreCompletedAlert()
} catch {
await logManager?.error(
"Backup restore: \(error)",

View file

@ -31,7 +31,7 @@ struct AboutView: View {
Text("Ferrite is a free and open source application developed by kingbri under the GNU-GPLv3 license.")
.textCase(.none)
.foregroundColor(.label)
.foregroundColor(.init(uiColor: .label))
.font(.body)
.padding(.top, 8)
.padding(.bottom, 20)

View file

@ -1,71 +0,0 @@
//
// AlertButton.swift
// Ferrite
//
// Created by Brian Dashore on 9/8/22.
//
// Universal alert button for dynamic alert views
//
import SwiftUI
struct AlertButton: Identifiable {
enum Role {
case destructive
case cancel
}
let id: UUID
let label: String
let action: () -> Void
let role: Role?
// Used for all buttons
init(_ label: String, role: Role? = nil, action: @escaping () -> Void) {
id = UUID()
self.label = label
self.action = action
self.role = role
}
// Used for buttons with no action
init(_ label: String? = nil, role: Role? = nil) {
id = UUID()
self.label = label ?? (role == .cancel ? "Cancel" : "OK")
action = {}
self.role = role
}
func toActionButton() -> Alert.Button {
if let role {
switch role {
case .cancel:
return .cancel(Text(label))
case .destructive:
return .destructive(Text(label), action: action)
}
} else {
return .default(Text(label), action: action)
}
}
@available(iOS 15.0, *)
@ViewBuilder
func toButtonView() -> some View {
Button(label, role: toButtonRole(role), action: action)
}
@available(iOS 15.0, *)
func toButtonRole(_ role: Role?) -> ButtonRole? {
if let role {
switch role {
case .destructive:
return .destructive
case .cancel:
return .cancel
}
} else {
return nil
}
}
}

View file

@ -1,138 +0,0 @@
//
// Backport.swift
// Ferrite
//
// Created by Brian Dashore on 9/29/22.
//
import Introspect
import SwiftUI
public struct Backport<Content> {
public let content: Content
public init(_ content: Content) {
self.content = content
}
}
extension View {
var backport: Backport<Self> { Backport(self) }
}
extension Backport where Content: View {
@ViewBuilder func alert(isPresented: Binding<Bool>,
title: String,
message: String?,
buttons: [AlertButton] = []) -> some View
{
if #available(iOS 15, *) {
content
.alert(
title,
isPresented: isPresented,
actions: {
ForEach(buttons) { button in
button.toButtonView()
}
},
message: {
if let message {
Text(message)
}
}
)
} else {
content
.background {
Color.clear
.alert(isPresented: isPresented) {
if let primaryButton = buttons[safe: 0],
let secondaryButton = buttons[safe: 1]
{
return Alert(
title: Text(title),
message: message.map { Text($0) } ?? nil,
primaryButton: primaryButton.toActionButton(),
secondaryButton: secondaryButton.toActionButton()
)
} else {
return Alert(
title: Text(title),
message: message.map { Text($0) } ?? nil,
dismissButton: buttons[safe: 0].map { $0.toActionButton() } ?? .cancel()
)
}
}
}
}
}
@ViewBuilder func confirmationDialog(isPresented: Binding<Bool>,
title: String, message: String?,
buttons: [AlertButton]) -> some View
{
if #available(iOS 15, *) {
content
.confirmationDialog(
title,
isPresented: isPresented,
titleVisibility: .visible
) {
ForEach(buttons) { button in
button.toButtonView()
}
} message: {
if let message {
Text(message)
}
}
} else {
content
.actionSheet(isPresented: isPresented) {
ActionSheet(
title: Text(title),
message: message.map { Text($0) } ?? nil,
buttons: [buttons.map { $0.toActionButton() }, [.cancel()]].flatMap { $0 }
)
}
}
}
@ViewBuilder func tint(_ color: Color) -> some View {
if #available(iOS 15, *) {
content
.tint(color)
} else {
content
.accentColor(color)
}
}
@ViewBuilder func onAppear(callback: @escaping () -> Void) -> some View {
if #available(iOS 15, *) {
content
.onAppear {
callback()
}
} else {
content
.viewDidAppear {
callback()
}
}
}
@ViewBuilder func introspectSearchController(customize: @escaping (UISearchController) -> Void) -> some View {
if #available(iOS 15, *) {
content.introspectSearchController(customize: customize)
} else {
content.introspectNavigationController { navigationController in
let navigationBar = navigationController.navigationBar
if let searchController = navigationBar.topItem?.searchController {
customize(searchController)
}
}
}
}
}

View file

@ -1,30 +0,0 @@
//
// DynamicFetchRequest.swift
// Ferrite
//
// Created by Brian Dashore on 9/6/22.
//
// Used for FetchRequests with a dynamic predicate
// iOS 14 compatible view
//
import CoreData
import SwiftUI
struct DynamicFetchRequest<T: NSManagedObject, Content: View>: View {
@FetchRequest var fetchRequest: FetchedResults<T>
let content: (FetchedResults<T>) -> Content
var body: some View {
content(fetchRequest)
}
init(predicate: NSPredicate?,
sortDescriptors: [NSSortDescriptor] = [],
@ViewBuilder content: @escaping (FetchedResults<T>) -> Content)
{
_fetchRequest = FetchRequest<T>(entity: T.entity(), sortDescriptors: sortDescriptors, predicate: predicate)
self.content = content
}
}

View file

@ -20,7 +20,7 @@ struct EmptyInstructionView: View {
.padding(.horizontal, 50)
}
.multilineTextAlignment(.center)
.foregroundColor(.secondaryLabel)
.foregroundColor(.init(uiColor: .secondaryLabel))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
}

View file

@ -17,11 +17,14 @@ struct FilterLabelView: View {
.foregroundColor(.primary)
Image(systemName: "chevron.down")
.foregroundColor(.tertiaryLabel)
.foregroundColor(.init(uiColor: .tertiaryLabel))
}
.padding(.horizontal, 9)
.padding(.vertical, 7)
.font(.caption, weight: .medium)
.background(Capsule().foregroundColor(.secondarySystemFill))
.font(
.caption
.weight(.medium)
)
.background(Capsule().foregroundColor(.init(uiColor: .secondarySystemFill)))
}
}

View file

@ -25,7 +25,7 @@ struct IndeterminateProgressView: View {
.offset(x: -reader.size.width * 0.6, y: 0)
.offset(x: reader.size.width * 1.2 * self.offset, y: 0)
.animation(.default.repeatForever().speed(0.5), value: self.offset)
.backport.onAppear {
.onAppear {
withAnimation {
self.offset = 1
}

View file

@ -19,11 +19,9 @@ struct InlineHeader: View {
var body: some View {
if #available(iOS 16, *) {
Text(title)
} else if #available(iOS 15, *) {
Text(title)
.listRowInsets(EdgeInsets(top: 10, leading: 15, bottom: 0, trailing: 0))
} else {
Text(title)
.listRowInsets(EdgeInsets(top: 10, leading: 15, bottom: 0, trailing: 0))
}
}
}

View file

@ -13,50 +13,42 @@ struct CustomScopeBarModifier<V: View>: ViewModifier {
@State private var hostingController: UIHostingController<V>?
func body(content: Content) -> some View {
if #available(iOS 15, *) {
content
.backport.introspectSearchController { searchController in
content
.introspectSearchController { searchController in
// MARK: One-time setup
// MARK: One-time setup
guard hostingController == nil else { return }
guard hostingController == nil else { return }
searchController.hidesNavigationBarDuringPresentation = true
searchController.searchBar.showsScopeBar = true
searchController.searchBar.scopeButtonTitles = [""]
(searchController.searchBar.value(forKey: "_scopeBar") as? UIView)?.isHidden = true
searchController.hidesNavigationBarDuringPresentation = true
searchController.searchBar.showsScopeBar = true
searchController.searchBar.scopeButtonTitles = [""]
(searchController.searchBar.value(forKey: "_scopeBar") as? UIView)?.isHidden = true
let hostingController = UIHostingController(rootView: scopeBarContent)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.backgroundColor = .clear
let hostingController = UIHostingController(rootView: scopeBarContent)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.backgroundColor = .clear
guard let containerView = searchController.searchBar.value(forKey: "_scopeBarContainerView") as? UIView else {
return
}
containerView.addSubview(hostingController.view)
NSLayoutConstraint.activate([
hostingController.view.widthAnchor.constraint(equalTo: containerView.widthAnchor),
hostingController.view.topAnchor.constraint(equalTo: containerView.topAnchor),
hostingController.view.heightAnchor.constraint(equalTo: containerView.heightAnchor)
])
self.hostingController = hostingController
guard let containerView = searchController.searchBar.value(forKey: "_scopeBarContainerView") as? UIView else {
return
}
.introspectNavigationController { navigationController in
if #available(iOS 16, *) {
navigationController.viewControllers.first?.navigationItem.preferredSearchBarPlacement = .stacked
}
containerView.addSubview(hostingController.view)
navigationController.navigationBar.prefersLargeTitles = true
navigationController.navigationBar.sizeToFit()
}
} else {
VStack {
scopeBarContent
content
Spacer()
NSLayoutConstraint.activate([
hostingController.view.widthAnchor.constraint(equalTo: containerView.widthAnchor),
hostingController.view.topAnchor.constraint(equalTo: containerView.topAnchor),
hostingController.view.heightAnchor.constraint(equalTo: containerView.heightAnchor)
])
self.hostingController = hostingController
}
.introspectNavigationController { navigationController in
if #available(iOS 16, *) {
navigationController.viewControllers.first?.navigationItem.preferredSearchBarPlacement = .stacked
}
navigationController.navigationBar.prefersLargeTitles = true
navigationController.navigationBar.sizeToFit()
}
}
}
}

View file

@ -0,0 +1,26 @@
//
// SearchListener.swift
// Ferrite
//
// Created by Brian Dashore on 3/29/23.
//
// Communicate isSearching back to the parent view
//
import SwiftUI
struct SearchListenerModifier: ViewModifier {
@Environment(\.isSearching) var isSearchingEnvironment
@Binding var isSearching: Bool
func body(content: Content) -> some View {
content
.background {
EmptyView()
.onChange(of: isSearchingEnvironment) { newValue in
isSearching = newValue
}
}
}
}

View file

@ -1,17 +0,0 @@
//
// ViewDidAppearModifier.swift
// Ferrite
//
// Created by Brian Dashore on 2/8/23.
//
import SwiftUI
struct ViewDidAppearModifier: ViewModifier {
let callback: () -> Void
func body(content: Content) -> some View {
content
.background(ViewDidAppearHandler(callback: callback))
}
}

View file

@ -21,7 +21,7 @@ struct Tag: View {
.padding(.vertical, verticalPadding)
.background(
RoundedRectangle(cornerRadius: 5)
.foregroundColor(color.map { $0 } ?? .tertiaryLabel)
.foregroundColor(color.map { $0 } ?? .init(uiColor: .tertiaryLabel))
.opacity(0.3)
)
}

View file

@ -8,84 +8,65 @@
import SwiftUI
struct BookmarksView: View {
@Environment(\.verticalSizeClass) var verticalSizeClass
@EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var debridManager: DebridManager
let backgroundContext = PersistenceController.shared.backgroundContext
@Binding var searchText: String
@Binding var bookmarksEmpty: Bool
@State private var viewTask: Task<Void, Never>?
@State private var bookmarkPredicate: NSPredicate?
var bookmarks: FetchedResults<Bookmark>
var body: some View {
DynamicFetchRequest(
predicate: bookmarkPredicate,
sortDescriptors: [NSSortDescriptor(keyPath: \Bookmark.orderNum, ascending: true)]
) { (bookmarks: FetchedResults<Bookmark>) in
List {
if !bookmarks.isEmpty {
ForEach(bookmarks, id: \.self) { bookmark in
SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark)
}
.onDelete { offsets in
for index in offsets {
if let bookmark = bookmarks[safe: index] {
PersistenceController.shared.delete(bookmark, context: backgroundContext)
NotificationCenter.default.post(name: .didDeleteBookmark, object: bookmark)
}
List {
if !bookmarks.isEmpty {
ForEach(bookmarks, id: \.self) { bookmark in
SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark)
}
.onDelete { offsets in
for index in offsets {
if let bookmark = bookmarks[safe: index] {
PersistenceController.shared.delete(bookmark, context: backgroundContext)
NotificationCenter.default.post(name: .didDeleteBookmark, object: bookmark)
}
}
.onMove { source, destination in
var changedBookmarks = bookmarks.map { $0 }
changedBookmarks.move(fromOffsets: source, toOffset: destination)
for reverseIndex in stride(from: changedBookmarks.count - 1, through: 0, by: -1) {
changedBookmarks[reverseIndex].orderNum = Int16(reverseIndex)
}
PersistenceController.shared.save()
}
}
}
.listStyle(.insetGrouped)
.inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 15 : -25)
.backport.onAppear {
bookmarksEmpty = bookmarks.isEmpty
.onMove { source, destination in
var changedBookmarks = bookmarks.map { $0 }
if debridManager.enabledDebrids.count > 0 {
viewTask = Task {
let magnets = bookmarks.compactMap {
if let magnetHash = $0.magnetHash {
return Magnet(hash: magnetHash, link: $0.magnetLink)
} else {
return nil
}
}
await debridManager.populateDebridIA(magnets)
changedBookmarks.move(fromOffsets: source, toOffset: destination)
for reverseIndex in stride(from: changedBookmarks.count - 1, through: 0, by: -1) {
changedBookmarks[reverseIndex].orderNum = Int16(reverseIndex)
}
PersistenceController.shared.save()
}
}
.onDisappear {
viewTask?.cancel()
}
.onChange(of: bookmarks.count) { newCount in
bookmarksEmpty = newCount == 0
}
}
.backport.onAppear {
applyPredicate()
.onAppear {
fetchPredicate()
}
.onChange(of: searchText) { _ in
applyPredicate()
fetchPredicate()
}
.listStyle(.insetGrouped)
.inlinedList(inset: 15)
.task {
if debridManager.enabledDebrids.count > 0 {
let magnets = bookmarks.compactMap {
if let magnetHash = $0.magnetHash {
return Magnet(hash: magnetHash, link: $0.magnetLink)
} else {
return nil
}
}
await debridManager.populateDebridIA(magnets)
}
}
}
func applyPredicate() {
bookmarkPredicate = searchText.isEmpty ? nil : NSPredicate(format: "title CONTAINS[cd] %@", searchText)
func fetchPredicate() {
bookmarks.nsPredicate = searchText.isEmpty ? nil : NSPredicate(format: "title CONTAINS[cd] %@", searchText)
}
}

View file

@ -14,8 +14,6 @@ struct AllDebridCloudView: View {
@Binding var searchText: String
@State private var viewTask: Task<Void, Never>?
var body: some View {
DisclosureGroup("Magnets") {
ForEach(debridManager.allDebridCloudMagnets.filter {
@ -72,7 +70,7 @@ struct AllDebridCloudView: View {
}
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
.backport.tint(.black)
.tint(.black)
}
.onDelete { offsets in
for index in offsets {

View file

@ -6,7 +6,6 @@
//
import SwiftUI
import SwiftUIX
struct PremiumizeCloudView: View {
@EnvironmentObject var debridManager: DebridManager
@ -15,8 +14,6 @@ struct PremiumizeCloudView: View {
@Binding var searchText: String
@State private var viewTask: Task<Void, Never>?
var body: some View {
DisclosureGroup("Items") {
ForEach(debridManager.premiumizeCloudItems.filter {
@ -47,7 +44,7 @@ struct PremiumizeCloudView: View {
}
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
.backport.tint(.black)
.tint(.black)
}
.onDelete { offsets in
for index in offsets {

View file

@ -14,8 +14,6 @@ struct RealDebridCloudView: View {
@Binding var searchText: String
@State private var viewTask: Task<Void, Never>?
var body: some View {
Group {
DisclosureGroup("Downloads") {
@ -41,7 +39,7 @@ struct RealDebridCloudView: View {
navModel: navModel
)
}
.backport.tint(.primary)
.tint(.primary)
}
.onDelete { offsets in
for index in offsets {
@ -111,7 +109,7 @@ struct RealDebridCloudView: View {
}
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
.backport.tint(.primary)
.tint(.primary)
}
.onDelete { offsets in
for index in offsets {

View file

@ -12,8 +12,6 @@ struct DebridCloudView: View {
@Binding var searchText: String
@State private var viewTask: Task<Void, Never>?
var body: some View {
List {
switch debridManager.selectedDebridType {
@ -28,19 +26,12 @@ struct DebridCloudView: View {
}
}
.listStyle(.plain)
.backport.onAppear {
viewTask = Task {
await debridManager.fetchDebridCloud()
}
}
.onDisappear {
viewTask?.cancel()
.task {
await debridManager.fetchDebridCloud()
}
.onChange(of: debridManager.selectedDebridType) { newType in
viewTask?.cancel()
if newType != nil {
viewTask = Task {
Task {
await debridManager.fetchDebridCloud()
}
}

View file

@ -16,26 +16,27 @@ struct HistoryActionsView: View {
Button("Clear") {
showActionSheet.toggle()
}
.backport.tint(.red)
.backport.confirmationDialog(
.tint(.red)
.confirmationDialog(
"Clear watch history",
isPresented: $showActionSheet,
title: "Clear watch history",
message: "This is an irreversible action!",
buttons: [
AlertButton("Past day", role: .destructive) {
deleteHistory(.day)
},
AlertButton("Past week", role: .destructive) {
deleteHistory(.week)
},
AlertButton("Past month", role: .destructive) {
deleteHistory(.month)
},
AlertButton("All time", role: .destructive) {
deleteHistory(.allTime)
}
]
)
titleVisibility: .visible
) {
Button("Past day", role: .destructive) {
deleteHistory(.day)
}
Button("Past week", role: .destructive) {
deleteHistory(.week)
}
Button("Past month", role: .destructive) {
deleteHistory(.month)
}
Button("All time", role: .destructive) {
deleteHistory(.allTime)
}
} message: {
Text("This is an irreversible action!")
}
}
func deleteHistory(_ deleteRange: HistoryDeleteRange) {

View file

@ -84,7 +84,7 @@ struct HistoryButtonView: View {
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
}
.backport.tint(.primary)
.tint(.primary)
.disableInteraction(navModel.currentChoiceSheet != nil)
}

View file

@ -17,29 +17,24 @@ struct HistoryView: View {
]
) var history: FetchedResults<History>
var allHistoryEntries: FetchedResults<HistoryEntry>
@Binding var searchText: String
@Binding var historyEmpty: Bool
@State private var historyPredicate: NSPredicate?
var body: some View {
DynamicFetchRequest(predicate: historyPredicate) { (allEntries: FetchedResults<HistoryEntry>) in
List {
if !history.isEmpty {
ForEach(groupedHistory(history), id: \.self) { historyGroup in
HistorySectionView(allEntries: allEntries, historyGroup: historyGroup)
}
List {
if !history.isEmpty {
ForEach(groupedHistory(history), id: \.self) { historyGroup in
HistorySectionView(allEntries: allHistoryEntries, historyGroup: historyGroup)
}
}
.listStyle(.insetGrouped)
}
.backport.onAppear {
historyEmpty = history.isEmpty
.listStyle(.insetGrouped)
.onAppear {
applyPredicate()
}
.onChange(of: history.count) { newCount in
historyEmpty = newCount == 0
}
.onChange(of: searchText) { _ in
applyPredicate()
}
@ -47,11 +42,11 @@ struct HistoryView: View {
func applyPredicate() {
if searchText.isEmpty {
historyPredicate = nil
allHistoryEntries.nsPredicate = nil
} else {
let namePredicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText.lowercased())
let subNamePredicate = NSPredicate(format: "subName CONTAINS[cd] %@", searchText.lowercased())
historyPredicate = NSCompoundPredicate(type: .or, subpredicates: [namePredicate, subNamePredicate])
allHistoryEntries.nsPredicate = NSCompoundPredicate(type: .or, subpredicates: [namePredicate, subNamePredicate])
}
}

View file

@ -51,20 +51,11 @@ struct InstalledPluginButtonView<P: Plugin>: View {
Image(systemName: "gear")
}
if #available(iOS 15.0, *) {
Button(role: .destructive) {
PersistenceController.shared.delete(installedPlugin, context: backgroundContext)
} label: {
Text("Remove")
Image(systemName: "trash")
}
} else {
Button {
PersistenceController.shared.delete(installedPlugin, context: backgroundContext)
} label: {
Text("Remove")
Image(systemName: "trash")
}
Button(role: .destructive) {
PersistenceController.shared.delete(installedPlugin, context: backgroundContext)
} label: {
Text("Remove")
Image(systemName: "trash")
}
}
}

View file

@ -57,7 +57,7 @@ struct PluginCatalogButtonView<PJ: PluginJson>: View {
)
.padding(.horizontal, 7)
.padding(.vertical, 5)
.background(.tertiarySystemBackground)
.background(Color.init(uiColor: .tertiarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.buttonStyle(.borderless)

View file

@ -17,10 +17,11 @@ struct PluginAggregateView<P: Plugin, PJ: PluginJson>: View {
sortDescriptors: []
) var pluginLists: FetchedResults<PluginList>
var installedPlugins: FetchedResults<P>
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
@Binding var searchText: String
@Binding var pluginsEmpty: Bool
@State private var isEditingSearch = false
@State private var isSearching = false
@ -31,67 +32,63 @@ struct PluginAggregateView<P: Plugin, PJ: PluginJson>: View {
@State private var selectedPlugin: P?
var body: some View {
ZStack {
DynamicFetchRequest(predicate: sourcePredicate) { (installedPlugins: FetchedResults<P>) in
List {
if
let filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(
forType: PJ.self,
installedPlugins: installedPlugins,
searchText: searchText
),
!filteredUpdatedPlugins.isEmpty
{
Section(header: InlineHeader("Updates")) {
ForEach(filteredUpdatedPlugins, id: \.self) { (updatedPlugin: PJ) in
PluginCatalogButtonView(availablePlugin: updatedPlugin, needsUpdate: true)
}
}
List {
if
let filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(
forType: PJ.self,
installedPlugins: installedPlugins,
searchText: searchText
),
!filteredUpdatedPlugins.isEmpty
{
Section(header: InlineHeader("Updates")) {
ForEach(filteredUpdatedPlugins, id: \.self) { (updatedPlugin: PJ) in
PluginCatalogButtonView(availablePlugin: updatedPlugin, needsUpdate: true)
}
if !installedPlugins.isEmpty {
Section(header: InlineHeader("Installed")) {
ForEach(installedPlugins, id: \.self) { installedPlugin in
InstalledPluginButtonView(
installedPlugin: installedPlugin,
showPluginOptions: $showPluginOptions,
selectedPlugin: $selectedPlugin
)
}
}
}
if
let filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(
forType: PJ.self,
installedPlugins: installedPlugins,
searchText: searchText
),
!filteredAvailablePlugins.isEmpty
{
Section(header: InlineHeader("Catalog")) {
ForEach(filteredAvailablePlugins, id: \.self) { availablePlugin in
PluginCatalogButtonView(availablePlugin: availablePlugin, needsUpdate: false)
}
}
}
}
.inlinedList(inset: 0)
.listStyle(.insetGrouped)
.id(UUID())
.backport.onAppear {
pluginsEmpty = installedPlugins.isEmpty
}
.onChange(of: searchText) { _ in
sourcePredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText)
}
.onChange(of: installedPlugins.count) { newCount in
pluginsEmpty = newCount == 0
}
}
if !installedPlugins.isEmpty {
Section(header: InlineHeader("Installed")) {
ForEach(installedPlugins, id: \.self) { installedPlugin in
InstalledPluginButtonView(
installedPlugin: installedPlugin,
showPluginOptions: $showPluginOptions,
selectedPlugin: $selectedPlugin
)
}
}
}
if
let filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(
forType: PJ.self,
installedPlugins: installedPlugins,
searchText: searchText
),
!filteredAvailablePlugins.isEmpty
{
Section(header: InlineHeader("Catalog")) {
ForEach(filteredAvailablePlugins, id: \.self) { availablePlugin in
PluginCatalogButtonView(availablePlugin: availablePlugin, needsUpdate: false)
}
}
}
}
.inlinedList(inset: 0)
.listStyle(.insetGrouped)
.onAppear {
fetchPredicate()
}
.onChange(of: searchText) { _ in
fetchPredicate()
}
.sheet(isPresented: $showPluginOptions) {
PluginInfoView(selectedPlugin: $selectedPlugin)
}
}
func fetchPredicate() {
installedPlugins.nsPredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText)
}
}

View file

@ -8,7 +8,7 @@
import SwiftUI
struct PluginInfoView<P: Plugin>: View {
@Environment(\.presentationMode) var presentationMode
@Environment(\.dismiss) var dismiss
@Binding var selectedPlugin: P?
@ -69,7 +69,7 @@ struct PluginInfoView<P: Plugin>: View {
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
presentationMode.wrappedValue.dismiss()
dismiss()
}
}
}

View file

@ -14,7 +14,7 @@ struct PluginTagsView: View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(tags, id: \.self) { tag in
Tag(name: tag.name, color: tag.colorHex.map { Color(hexadecimal: $0) })
Tag(name: tag.name, color: tag.colorHex.map { Color(hex: $0) })
}
}
}

View file

@ -31,7 +31,7 @@ struct SourceSettingsApiView: View {
})
.autocorrectionDisabled(true)
.autocapitalization(.none)
.backport.onAppear {
.onAppear {
tempClientId = clientId.value ?? ""
}
}
@ -45,7 +45,7 @@ struct SourceSettingsApiView: View {
})
.autocorrectionDisabled(true)
.autocapitalization(.none)
.backport.onAppear {
.onAppear {
tempClientSecret = clientSecret.value ?? ""
}
}

View file

@ -28,7 +28,7 @@ struct SourceSettingsBaseUrlView: View {
.keyboardType(.URL)
.autocorrectionDisabled(true)
.autocapitalization(.none)
.backport.onAppear {
.onAppear {
tempBaseUrl = selectedSource.baseUrl ?? ""
}
}

View file

@ -57,6 +57,6 @@ struct SourceSettingsMethodView: View {
}
}
}
.backport.tint(.primary)
.tint(.primary)
}
}

View file

@ -42,8 +42,7 @@ struct SearchFilterHeaderView: View {
}
.id(debridManager.selectedDebridType)
}
.padding(.horizontal, verticalSizeClass == .compact ? (Application.shared.osVersion.majorVersion > 14 ? 65 : 18) : 18)
.padding(.top, Application.shared.osVersion.majorVersion > 14 ? 0 : 10)
.padding(.horizontal, verticalSizeClass == .compact ? 65 : 18)
}
}
}

View file

@ -90,7 +90,7 @@ struct SearchResultButtonView: View {
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
}
.disableInteraction(navModel.currentChoiceSheet != nil)
.backport.tint(.primary)
.tint(.primary)
.conditionalContextMenu(id: existingBookmark) {
ZStack {
if let bookmark = existingBookmark {
@ -123,19 +123,19 @@ struct SearchResultButtonView: View {
}
}
}
.backport.alert(
isPresented: $debridManager.showDeleteAlert,
title: "Caching file",
message: "RealDebrid is currently caching this file. Would you like to delete it? \n\nProgress can be checked on the RealDebrid website.",
buttons: [
AlertButton("Yes", role: .destructive) {
Task {
await debridManager.deleteRdTorrent()
}
},
AlertButton(role: .cancel)
]
)
.alert("Caching file", isPresented: $debridManager.showDeleteAlert) {
Button("Yes", role: .destructive) {
Task {
await debridManager.deleteRdTorrent()
}
}
Button("Cancel", role: .cancel) {}
} message: {
Text(
"RealDebrid is currently caching this file. Would you like to delete it? \n\n" +
"Progress can be checked on the RealDebrid website."
)
}
.onReceive(NotificationCenter.default.publisher(for: .didDeleteBookmark)) { notification in
// If the instance contains the deleted bookmark, remove it.
if let deletedBookmark = notification.object as? Bookmark,
@ -145,7 +145,7 @@ struct SearchResultButtonView: View {
existingBookmark = nil
}
}
.backport.onAppear {
.onAppear {
// Only run a exists request if a bookmark isn't passed to the view
if existingBookmark == nil, !runOnce {
let bookmarkRequest = Bookmark.fetchRequest()

View file

@ -0,0 +1,95 @@
//
// SearchResultsView.swift
// Ferrite
//
// Created by Brian Dashore on 3/28/23.
//
import SwiftUI
struct SearchResultsView: View {
@Environment(\.isSearching) var isSearching
@Environment(\.dismissSearch) var dismissSearch
@EnvironmentObject var scrapingModel: ScrapingViewModel
@EnvironmentObject var navModel: NavigationViewModel
@AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText: Bool = false
@Binding var searchText: String
@Binding var searchPrompt: String
@State private var lastSearchPromptIndex: Int = -1
let searchBarTextArray: [String] = [
"What's on your mind?",
"Discover something interesting",
"Find an engaging show",
"Feeling adventurous?",
"Look for something new",
"The classics are a good idea"
]
var body: some View {
ForEach(scrapingModel.searchResults, id: \.self) { result in
if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil {
SearchResultButtonView(result: result)
}
}
.onAppear {
searchPrompt = getSearchPrompt()
}
.onChange(of: searchText) { newText in
if newText.isEmpty, isSearching {
searchPrompt = getSearchPrompt()
}
}
.onChange(of: navModel.selectedTab) { tab in
// Cancel the search if tab is switched while search is in progress
if tab != .search, scrapingModel.runningSearchTask != nil {
scrapingModel.searchResults = []
scrapingModel.runningSearchTask?.cancel()
scrapingModel.runningSearchTask = nil
dismissSearch()
}
}
.onChange(of: scrapingModel.searchResults) { _ in
// Cleans up any leftover search results in the event of an abrupt cancellation
if !isSearching {
scrapingModel.searchResults = []
}
}
.onChange(of: isSearching) { newValue in
if !newValue {
scrapingModel.searchResults = []
scrapingModel.runningSearchTask?.cancel()
scrapingModel.runningSearchTask = nil
}
}
.overlay {
if
scrapingModel.searchResults.isEmpty,
isSearching,
scrapingModel.runningSearchTask == nil
{
Text("No results found")
}
}
}
// Fetches random searchbar text if enabled, otherwise deinit the last case value
func getSearchPrompt() -> String {
if usesRandomSearchText {
let num = Int.random(in: 0 ..< searchBarTextArray.count - 1)
if num == lastSearchPromptIndex {
lastSearchPromptIndex = num + 1
return searchBarTextArray[safe: num + 1] ?? "Search"
} else {
lastSearchPromptIndex = num
return searchBarTextArray[safe: num] ?? "Search"
}
} else {
lastSearchPromptIndex = -1
return "Search"
}
}
}

View file

@ -34,7 +34,7 @@ struct BackupsView: View {
Label("Export", systemImage: "square.and.arrow.up")
}
}
.backport.tint(.primary)
.tint(.primary)
}
.onDelete { offsets in
for index in offsets {
@ -48,7 +48,7 @@ struct BackupsView: View {
.listStyle(.insetGrouped)
}
}
.backport.onAppear {
.onAppear {
backupManager.backupUrls = FileManager.default.appDirectory
.appendingPathComponent("Backups", isDirectory: true).contentsByDateAdded
}

View file

@ -40,46 +40,11 @@ struct DefaultActionPickerView: View {
// Handle custom here
ForEach(actions.filter { $0.requires.contains(actionRequirement.rawValue) }, id: \.id) { action in
Button {
if let actionListId = action.listId?.uuidString {
defaultAction = .custom(name: action.name, listId: actionListId)
} else {
logManager.error(
"Default action: This action doesn't have a corresponding plugin list! Please uninstall the action"
)
}
} label: {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(action.name)
Group {
if let pluginList = pluginLists.first(where: { $0.id == action.listId }) {
Text("List: \(pluginList.name)")
Text(pluginList.id.uuidString)
.font(.caption)
} else {
Text("No plugin list found")
.font(.caption)
}
}
.foregroundColor(.secondary)
.lineLimit(1)
}
Spacer()
if
case let .custom(name, listId) = defaultAction,
action.listId?.uuidString == listId,
action.name == name
{
Image(systemName: "checkmark")
.foregroundColor(.blue)
}
}
}
.backport.tint(.primary)
CustomChoiceButton(
action: action,
defaultAction: $defaultAction,
associatedPluginList: pluginLists.first(where: { $0.id == action.listId })
)
}
}
.listStyle(.insetGrouped)
@ -89,6 +54,59 @@ struct DefaultActionPickerView: View {
}
}
private struct CustomChoiceButton: View {
@EnvironmentObject var logManager: LoggingManager
@ObservedObject var action: Action
@Binding var defaultAction: DefaultAction
var associatedPluginList: PluginList?
var body: some View {
Button {
if let actionListId = action.listId?.uuidString {
defaultAction = .custom(name: action.name, listId: actionListId)
} else {
logManager.error(
"Default action: This action doesn't have a corresponding plugin list! Please uninstall the action"
)
}
} label: {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(action.name)
Group {
if let associatedPluginList {
Text("List: \(associatedPluginList.name)")
Text(associatedPluginList.id.uuidString)
.font(.caption)
} else {
Text("No plugin list found")
.font(.caption)
}
}
.foregroundColor(.secondary)
.lineLimit(1)
}
Spacer()
if
case let .custom(name, listId) = defaultAction,
action.listId?.uuidString == listId,
action.name == name
{
Image(systemName: "checkmark")
.foregroundColor(.blue)
}
}
}
.tint(.primary)
}
}
private struct DefaultChoiceButton: View {
@Binding var defaultAction: DefaultAction
let selectedOption: DefaultAction
@ -107,7 +125,7 @@ private struct DefaultChoiceButton: View {
}
}
}
.backport.tint(.primary)
.tint(.primary)
}
func fetchButtonName() -> String {

View file

@ -8,7 +8,7 @@
import SwiftUI
struct KodiEditorView: View {
@Environment(\.presentationMode) var presentationMode
@Environment(\.dismiss) var dismiss
@EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var pluginManager: PluginManager
@ -56,7 +56,7 @@ struct KodiEditorView: View {
.autocapitalization(.none)
.id(loadedSelectedServer)
}
.backport.onAppear {
.onAppear {
if let selectedKodiServer = navModel.selectedKodiServer {
serverUrl = selectedKodiServer.urlString
friendlyName = selectedKodiServer.name
@ -66,17 +66,17 @@ struct KodiEditorView: View {
loadedSelectedServer.toggle()
}
}
.backport.alert(
isPresented: $showErrorAlert,
title: "Error",
message: errorAlertText
)
.alert("Error", isPresented: $showErrorAlert) {
Button("OK", role: .cancel) {}
} message: {
Text(errorAlertText)
}
.navigationTitle("Editing Kodi Server")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
presentationMode.wrappedValue.dismiss()
dismiss()
}
}
@ -91,7 +91,7 @@ struct KodiEditorView: View {
existingServer: navModel.selectedKodiServer
)
presentationMode.wrappedValue.dismiss()
dismiss()
} catch {
logManager.error("Editing Kodi server: \(error)", showToast: false)
errorAlertText = error.localizedDescription

View file

@ -15,7 +15,6 @@ struct KodiServerView: View {
@State private var isActive = false
@State private var pingInProgress = false
@State private var viewTask: Task<Void, Never>?
var body: some View {
HStack {
@ -30,23 +29,18 @@ struct KodiServerView: View {
.frame(width: 10, height: 10)
}
}
.backport.onAppear {
viewTask = Task {
pingInProgress = true
.task {
pingInProgress = true
do {
try await pluginManager.kodi.ping(server: server)
isActive = true
} catch {
logManager.error("Kodi server \(server.name): \(error)", showToast: false)
isActive = false
}
pingInProgress = false
do {
try await pluginManager.kodi.ping(server: server)
isActive = true
} catch {
logManager.error("Kodi server \(server.name): \(error)", showToast: false)
isActive = false
}
}
.onDisappear {
viewTask?.cancel()
pingInProgress = false
}
}
}

View file

@ -12,11 +12,7 @@ struct SettingsKodiView: View {
@EnvironmentObject var navModel: NavigationViewModel
// TODO: Change to var in v0.7
@FetchRequest(
entity: KodiServer.entity(),
sortDescriptors: []
) var kodiServers: FetchedResults<KodiServer>
var kodiServers: FetchedResults<KodiServer>
@State private var presentEditSheet = false
@ -48,20 +44,11 @@ struct SettingsKodiView: View {
Image(systemName: "pencil")
}
if #available(iOS 15.0, *) {
Button(role: .destructive) {
PersistenceController.shared.delete(server, context: backgroundContext)
} label: {
Text("Remove")
Image(systemName: "trash")
}
} else {
Button {
PersistenceController.shared.delete(server, context: backgroundContext)
} label: {
Text("Remove")
Image(systemName: "trash")
}
Button(role: .destructive) {
PersistenceController.shared.delete(server, context: backgroundContext)
} label: {
Text("Remove")
Image(systemName: "trash")
}
}
}
@ -78,7 +65,6 @@ struct SettingsKodiView: View {
.listStyle(.insetGrouped)
.sheet(isPresented: $presentEditSheet) {
KodiEditorView()
.environmentObject(navModel)
}
.navigationTitle("Kodi")
.navigationBarTitleDisplayMode(.inline)

View file

@ -8,7 +8,7 @@
import SwiftUI
struct PluginListEditorView: View {
@Environment(\.presentationMode) var presentationMode
@Environment(\.dismiss) var dismiss
@EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var pluginManager: PluginManager
@ -33,23 +33,23 @@ struct PluginListEditorView: View {
.autocapitalization(.none)
.id(loadedSelectedList)
}
.backport.onAppear {
.onAppear {
if let selectedList = navModel.selectedPluginList {
pluginListUrl = selectedList.urlString
loadedSelectedList.toggle()
}
}
.backport.alert(
isPresented: $showUrlErrorAlert,
title: "Error",
message: urlErrorAlertText
)
.alert("Error", isPresented: $showUrlErrorAlert) {
Button("OK", role: .cancel) {}
} message: {
Text(urlErrorAlertText)
}
.navigationTitle("Editing Plugin List")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
presentationMode.wrappedValue.dismiss()
dismiss()
}
}
@ -62,7 +62,7 @@ struct PluginListEditorView: View {
existingPluginList: navModel.selectedPluginList
)
presentationMode.wrappedValue.dismiss()
dismiss()
} catch {
logManager.error("Editing plugin list: \(error)", showToast: false)
urlErrorAlertText = error.localizedDescription

View file

@ -48,20 +48,11 @@ struct SettingsPluginListView: View {
Image(systemName: "pencil")
}
if #available(iOS 15.0, *) {
Button(role: .destructive) {
PersistenceController.shared.delete(pluginList, context: backgroundContext)
} label: {
Text("Remove")
Image(systemName: "trash")
}
} else {
Button {
PersistenceController.shared.delete(pluginList, context: backgroundContext)
} label: {
Text("Remove")
Image(systemName: "trash")
}
Button(role: .destructive) {
PersistenceController.shared.delete(pluginList, context: backgroundContext)
} label: {
Text("Remove")
Image(systemName: "trash")
}
}
}
@ -83,7 +74,6 @@ struct SettingsPluginListView: View {
.presentationDetents([.medium])
} else {
PluginListEditorView()
.environmentObject(navModel)
}
}
.navigationTitle("Plugin Lists")

View file

@ -10,7 +10,6 @@ import SwiftUI
struct SettingsAppVersionView: View {
@EnvironmentObject var logManager: LoggingManager
@State private var viewTask: Task<Void, Never>?
@State private var releases: [Github.Release] = []
@State private var loadedReleases = false
@ -30,25 +29,20 @@ struct SettingsAppVersionView: View {
.listStyle(.insetGrouped)
}
}
.backport.onAppear {
viewTask = Task {
do {
if let fetchedReleases = try await Github().fetchReleases() {
releases = fetchedReleases
} else {
logManager.error("Github: No releases found")
}
} catch {
logManager.error("Github: \(error)")
}
withAnimation {
loadedReleases = true
.task {
do {
if let fetchedReleases = try await Github().fetchReleases() {
releases = fetchedReleases
} else {
logManager.error("Github: No releases found")
}
} catch {
logManager.error("Github: \(error)")
}
withAnimation {
loadedReleases = true
}
}
.onDisappear {
viewTask?.cancel()
}
.navigationTitle("Version History")
.navigationBarTitleDisplayMode(.inline)

View file

@ -23,11 +23,11 @@ struct SettingsLogView: View {
}
}
.listStyle(.plain)
.backport.alert(
isPresented: $logManager.showLogExportedAlert,
title: "Success",
message: "Log successfully exported in Ferrite's logs folder"
)
.alert("Success", isPresented: $logManager.showLogExportedAlert) {
Button("OK", role: .cancel) {}
} message: {
Text("Log successfully exported in Ferrite's logs folder")
}
.navigationTitle("Logs")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
@ -39,18 +39,10 @@ struct SettingsLogView: View {
Label("Export", systemImage: "square.and.arrow.up")
}
if #available(iOS 15, *) {
Button(role: .destructive) {
logManager.messageArray = []
} label: {
Label("Clear session logs", systemImage: "trash")
}
} else {
Button {
logManager.messageArray = []
} label: {
Label("Clear session logs", systemImage: "trash")
}
Button(role: .destructive) {
logManager.messageArray = []
} label: {
Label("Clear session logs", systemImage: "trash")
}
} label: {
Image(systemName: "ellipsis")

View file

@ -6,7 +6,6 @@
//
import SwiftUI
import SwiftUIX
struct ContentView: View {
@EnvironmentObject var scrapingModel: ScrapingViewModel
@ -16,133 +15,41 @@ struct ContentView: View {
@EnvironmentObject var logManager: LoggingManager
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch: Bool = false
@AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText: Bool = false
@State private var isEditingSearch = false
@State private var isSearching = false
@State private var searchText: String = ""
@State private var lastSearchTextIndex: Int = -1
@State private var searchBarText: String = "Search"
let searchBarTextArray: [String] = [
"What's on your mind?",
"Discover something interesting",
"Find an engaging show",
"Feeling adventurous?",
"Look for something new",
"The classics are a good idea"
]
@State private var searchPrompt: String = "Search"
var body: some View {
NavView {
List {
ForEach(scrapingModel.searchResults, id: \.self) { result in
if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil {
SearchResultButtonView(result: result)
}
}
SearchResultsView(searchText: $searchText, searchPrompt: $searchPrompt)
}
.listStyle(.insetGrouped)
.inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 20 : -20)
.overlay {
if
scrapingModel.searchResults.isEmpty,
isSearching,
scrapingModel.runningSearchTask == nil
{
Text("No results found")
}
}
.onChange(of: searchText) { newText in
if newText.isEmpty, isSearching {
searchBarText = getSearchBarText()
}
}
.onChange(of: scrapingModel.searchResults) { _ in
// Cleans up any leftover search results in the event of an abrupt cancellation
if !isSearching {
scrapingModel.searchResults = []
}
}
.onChange(of: navModel.selectedTab) { tab in
// Cancel the search if tab is switched while search is in progress
if tab != .search, scrapingModel.runningSearchTask != nil {
scrapingModel.searchResults = []
scrapingModel.runningSearchTask?.cancel()
scrapingModel.runningSearchTask = nil
isSearching = false
searchText = ""
}
}
.inlinedList(inset: 20)
.navigationTitle("Search")
.navigationSearchBar {
SearchBar(
searchBarText,
text: $searchText,
isEditing: $isEditingSearch,
onCommit: {
if let runningSearchTask = scrapingModel.runningSearchTask, runningSearchTask.isCancelled {
scrapingModel.runningSearchTask = nil
return
}
scrapingModel.runningSearchTask = Task {
isSearching = true
let sources = pluginManager.fetchInstalledSources()
await scrapingModel.scanSources(
sources: sources,
searchText: searchText,
debridManager: debridManager
)
logManager.hideIndeterminateToast()
scrapingModel.runningSearchTask = nil
}
}
)
.showsCancelButton(isEditingSearch || isSearching)
.onCancel {
scrapingModel.searchResults = []
scrapingModel.runningSearchTask?.cancel()
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: Text(searchPrompt))
.onSubmit(of: .search) {
if let runningSearchTask = scrapingModel.runningSearchTask, runningSearchTask.isCancelled {
scrapingModel.runningSearchTask = nil
return
}
scrapingModel.runningSearchTask = Task {
let sources = pluginManager.fetchInstalledSources()
await scrapingModel.scanSources(
sources: sources,
searchText: searchText,
debridManager: debridManager
)
logManager.hideIndeterminateToast()
scrapingModel.runningSearchTask = nil
isSearching = false
searchText = ""
searchBarText = getSearchBarText()
}
}
.autocorrectionDisabled(!autocorrectSearch)
.backport.introspectSearchController { searchController in
// TODO: Replace with SwiftUI autocapitalization modifier
searchController.searchBar.autocapitalizationType = .none
}
.navigationSearchBarHiddenWhenScrolling(false)
.customScopeBar {
SearchFilterHeaderView()
.environmentObject(scrapingModel)
.environmentObject(debridManager)
}
}
.backport.onAppear {
searchBarText = getSearchBarText()
}
}
// Fetches random searchbar text if enabled, otherwise deinit the last case value
func getSearchBarText() -> String {
if usesRandomSearchText {
let num = Int.random(in: 0 ..< searchBarTextArray.count - 1)
if num == lastSearchTextIndex {
lastSearchTextIndex = num + 1
return searchBarTextArray[safe: num + 1] ?? "Search"
} else {
lastSearchTextIndex = num
return searchBarTextArray[safe: num] ?? "Search"
}
} else {
lastSearchTextIndex = -1
return "Search"
}
}
}

View file

@ -6,55 +6,64 @@
//
import SwiftUI
import SwiftUIX
struct LibraryView: View {
@EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var navModel: NavigationViewModel
@State private var bookmarksEmpty = false
@State private var historyEmpty = false
@FetchRequest(
entity: Bookmark.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Bookmark.orderNum, ascending: true)]
) var bookmarks: FetchedResults<Bookmark>
@FetchRequest(
entity: HistoryEntry.entity(),
sortDescriptors: []
) var allHistoryEntries: FetchedResults<HistoryEntry>
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
@State private var editMode: EditMode = .inactive
@State private var searchText: String = ""
@State private var isEditingSearch = false
// Bound to the isSearching environment var
@State private var isSearching = false
@State private var searchText: String = ""
var body: some View {
NavView {
ZStack {
switch navModel.libraryPickerSelection {
case .bookmarks:
BookmarksView(searchText: $searchText, bookmarksEmpty: $bookmarksEmpty)
BookmarksView(searchText: $searchText, bookmarks: bookmarks)
case .history:
HistoryView(searchText: $searchText, historyEmpty: $historyEmpty)
HistoryView(allHistoryEntries: allHistoryEntries, searchText: $searchText)
case .debridCloud:
DebridCloudView(searchText: $searchText)
}
}
.searchListener(isSearching: $isSearching)
.overlay {
switch navModel.libraryPickerSelection {
case .bookmarks:
if bookmarksEmpty {
EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results")
}
case .history:
if historyEmpty {
EmptyInstructionView(title: "No History", message: "Start watching to build history")
}
case .debridCloud:
if debridManager.selectedDebridType == nil {
EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service")
if !isSearching {
switch navModel.libraryPickerSelection {
case .bookmarks:
if bookmarks.isEmpty {
EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results")
}
case .history:
if allHistoryEntries.isEmpty {
EmptyInstructionView(title: "No History", message: "Start watching to build history")
}
case .debridCloud:
if debridManager.selectedDebridType == nil {
EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service")
}
}
}
}
.navigationTitle("Library")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack(spacing: Application.shared.osVersion.majorVersion > 14 ? 10 : 18) {
HStack {
Spacer()
EditButton()
@ -72,21 +81,9 @@ struct LibraryView: View {
}
}
}
.navigationSearchBar {
SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: {
isSearching = true
})
.showsCancelButton(isEditingSearch || isSearching)
.onCancel {
searchText = ""
isSearching = false
}
}
.navigationSearchBarHiddenWhenScrolling(false)
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.customScopeBar {
LibraryPickerView()
.environmentObject(debridManager)
.environmentObject(navModel)
}
.environment(\.editMode, $editMode)
}

View file

@ -8,7 +8,7 @@
import SwiftUI
struct LoginWebView: View {
@Environment(\.presentationMode) var presentationMode
@Environment(\.dismiss) var dismiss
var url: URL
var body: some View {
@ -19,7 +19,7 @@ struct LoginWebView: View {
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
presentationMode.wrappedValue.dismiss()
dismiss()
}
}
}

View file

@ -6,7 +6,6 @@
//
import SwiftUI
import SwiftUIX
struct MainView: View {
@EnvironmentObject var navModel: NavigationViewModel
@ -21,7 +20,6 @@ struct MainView: View {
@State private var showUpdateAlert = false
@State private var releaseVersionString: String = ""
@State private var releaseUrlString: String = ""
@State private var viewTask: Task<Void, Never>?
var body: some View {
TabView(selection: $navModel.selectedTab) {
@ -53,78 +51,67 @@ struct MainView: View {
switch item {
case .action:
ActionChoiceView()
.environmentObject(debridManager)
.environmentObject(scrapingModel)
.environmentObject(navModel)
.environmentObject(pluginManager)
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
case .batch:
BatchChoiceView()
.environmentObject(debridManager)
.environmentObject(scrapingModel)
.environmentObject(navModel)
case .activity:
EmptyView()
// TODO: Fix share sheet
if #available(iOS 16, *) {
AppActivityView(activityItems: navModel.activityItems)
ShareSheet(activityItems: navModel.activityItems)
.presentationDetents([.medium, .large])
} else {
AppActivityView(activityItems: navModel.activityItems)
ShareSheet(activityItems: navModel.activityItems)
}
}
}
.backport.onAppear {
.onAppear {
logManager.info("Ferrite started")
}
.task {
if
autoUpdateNotifs,
Application.shared.osVersion.toString() >= Application.shared.minVersion
{
// MARK: If scope bar duplication happens, this may be the problem
// Sleep for 2 seconds to allow for view layout and app init
try? await Task.sleep(seconds: 2)
logManager.info("Ferrite started")
viewTask = Task {
// Sleep for 2 seconds to allow for view layout and app init
try? await Task.sleep(seconds: 2)
do {
guard let latestRelease = try await Github().fetchLatestRelease() else {
logManager.error(
"Github: No releases found",
description: "Github error: No releases found"
)
return
}
let releaseVersion = String(latestRelease.tagName.dropFirst())
if releaseVersion > Application.shared.appVersion {
releaseVersionString = latestRelease.tagName
releaseUrlString = latestRelease.htmlUrl
logManager.info("Update available to \(releaseVersionString)")
showUpdateAlert.toggle()
}
} catch {
let error = error as NSError
if error.code == -1009 {
logManager.info(
"Github: The connection is offline",
description: "The connection is offline"
)
} else {
logManager.error(
"Github: \(error)",
description: "A Github error was logged"
)
}
do {
guard let latestRelease = try await Github().fetchLatestRelease() else {
logManager.error(
"Github: No releases found",
description: "Github error: No releases found"
)
return
}
logManager.info("Github release updates checked")
let releaseVersion = String(latestRelease.tagName.dropFirst())
if releaseVersion > Application.shared.appVersion {
releaseVersionString = latestRelease.tagName
releaseUrlString = latestRelease.htmlUrl
logManager.info("Update available to \(releaseVersionString)")
showUpdateAlert.toggle()
}
} catch {
let error = error as NSError
if error.code == -1009 {
logManager.info(
"Github: The connection is offline",
description: "The connection is offline"
)
} else {
logManager.error(
"Github: \(error)",
description: "A Github error was logged"
)
}
}
logManager.info("Github release updates checked")
}
}
.onDisappear {
viewTask?.cancel()
}
.onOpenURL { url in
if url.scheme == "file" {
// Attempt to copy to backups directory if backup doesn't exist
@ -134,54 +121,51 @@ struct MainView: View {
}
}
// Global alerts and dialogs for backups
.backport.confirmationDialog(
.confirmationDialog(
"Restore backup?",
isPresented: $backupManager.showRestoreAlert,
title: "Restore backup?",
message:
"Merge (preferred): Will merge your current data with the backup \n\n" +
"Overwrite: Will delete and replace all your data \n\n" +
"If Merge causes app instability, uninstall Ferrite and use the Overwrite option.",
buttons: [
.init("Merge", role: .destructive) {
Task {
await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: false)
}
},
.init("Overwrite", role: .destructive) {
Task {
await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: true)
}
titleVisibility: .visible
) {
Button("Merge", role: .destructive) {
Task {
await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: false)
}
]
)
.backport.alert(
isPresented: $backupManager.showRestoreCompletedAlert,
title: "Backup restored",
message: backupManager.restoreCompletedMessage.joined(separator: " \n\n"),
buttons: [
.init("OK") {
backupManager.restoreCompletedMessage = []
}
Button("Overwrite", role: .destructive) {
Task {
await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: true)
}
]
)
}
} message: {
Text(
"Merge (preferred): Will merge your current data with the backup \n\n" +
"Overwrite: Will delete and replace all your data \n\n" +
"If Merge causes app instability, uninstall Ferrite and use the Overwrite option."
)
}
.alert("Backup restored", isPresented: $backupManager.showRestoreCompletedAlert) {
Button("OK", role: .cancel) {
backupManager.restoreCompletedMessage = []
}
} message: {
Text(backupManager.restoreCompletedMessage.joined(separator: " \n\n"))
}
// Updater alert
.backport.alert(
isPresented: $showUpdateAlert,
title: "Update available",
message:
"Ferrite \(releaseVersionString) can be downloaded. \n\n" +
"This alert can be disabled in Settings.",
buttons: [
.init("Download") {
guard let releaseUrl = URL(string: releaseUrlString) else {
return
}
.alert("Update available", isPresented: $showUpdateAlert) {
Button("Download") {
guard let releaseUrl = URL(string: releaseUrlString) else {
return
}
UIApplication.shared.open(releaseUrl)
},
.init(role: .cancel)
]
)
UIApplication.shared.open(releaseUrl)
}
Button("Cancel", role: .cancel) {}
} message: {
Text(
"Ferrite \(releaseVersionString) can be downloaded. \n\n" +
"This alert can be disabled in Settings."
)
}
.overlay {
VStack {
Spacer()
@ -198,9 +182,7 @@ struct MainView: View {
}
.padding(12)
.font(.caption)
.background {
VisualEffectBlurView(blurStyle: .systemThinMaterial)
}
.background(.thinMaterial)
.cornerRadius(10)
}
@ -222,9 +204,7 @@ struct MainView: View {
}
.padding(12)
.font(.caption)
.background {
VisualEffectBlurView(blurStyle: .systemThinMaterial)
}
.background(.thinMaterial)
.cornerRadius(10)
.frame(width: 200)
}

View file

@ -6,7 +6,6 @@
//
import SwiftUI
import SwiftUIX
struct PluginsView: View {
@EnvironmentObject var pluginManager: PluginManager
@ -14,16 +13,22 @@ struct PluginsView: View {
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
@State private var installedSourcesEmpty = false
@State private var installedActionsEmpty = false
@FetchRequest(
entity: Source.entity(),
sortDescriptors: []
) var installedSources: FetchedResults<Source>
@FetchRequest(
entity: Action.entity(),
sortDescriptors: []
) var installedActions: FetchedResults<Action>
@State private var checkedForPlugins = false
@State private var isEditingSearch = false
// Bound to the isSearching environment var
@State private var isSearching = false
@State private var searchText: String = ""
@State private var viewTask: Task<Void, Never>?
var body: some View {
NavView {
ZStack {
@ -31,58 +36,47 @@ struct PluginsView: View {
switch navModel.pluginPickerSelection {
case .sources:
PluginAggregateView<Source, SourceJson>(
searchText: $searchText,
pluginsEmpty: $installedSourcesEmpty
installedPlugins: installedSources,
searchText: $searchText
)
case .actions:
PluginAggregateView<Action, ActionJson>(
searchText: $searchText,
pluginsEmpty: $installedActionsEmpty
installedPlugins: installedActions,
searchText: $searchText
)
}
}
}
.searchListener(isSearching: $isSearching)
.overlay {
if checkedForPlugins {
switch navModel.pluginPickerSelection {
case .sources:
if installedSourcesEmpty, pluginManager.availableSources.isEmpty {
EmptyInstructionView(title: "No Sources", message: "Add a plugin list in Settings")
}
case .actions:
if installedActionsEmpty, pluginManager.availableActions.isEmpty {
EmptyInstructionView(title: "No Actions", message: "Add a plugin list in Settings")
if !isSearching {
if checkedForPlugins {
switch navModel.pluginPickerSelection {
case .sources:
if installedSources.isEmpty, pluginManager.availableSources.isEmpty {
EmptyInstructionView(title: "No Sources", message: "Add a plugin list in Settings")
}
case .actions:
if installedActions.isEmpty, pluginManager.availableActions.isEmpty {
EmptyInstructionView(title: "No Actions", message: "Add a plugin list in Settings")
}
}
} else {
ProgressView()
}
} else {
ProgressView()
}
}
.backport.onAppear {
viewTask = Task {
await pluginManager.fetchPluginsFromUrl()
checkedForPlugins = true
}
.task {
await pluginManager.fetchPluginsFromUrl()
checkedForPlugins = true
}
.onDisappear {
viewTask?.cancel()
checkedForPlugins = false
}
.navigationTitle("Plugins")
.navigationSearchBar {
SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: {
isSearching = true
})
.showsCancelButton(isEditingSearch || isSearching)
.onCancel {
searchText = ""
isSearching = false
}
}
.navigationSearchBarHiddenWhenScrolling(false)
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.customScopeBar {
PluginPickerView()
.environmentObject(navModel)
}
}
}

View file

@ -0,0 +1,18 @@
//
// ShareSheet.swift
// Ferrite
//
// Created by Brian Dashore on 3/28/23.
//
import SwiftUI
struct ShareSheet: UIViewControllerRepresentable {
let activityItems: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}

View file

@ -1,43 +0,0 @@
//
// ViewDidAppearHandler.swift
// Ferrite
//
// Created by Brian Dashore on 2/8/23.
//
// UIKit onAppear hook to fix onAppear behavior in iOS 14
//
import SwiftUI
struct ViewDidAppearHandler: UIViewControllerRepresentable {
let callback: () -> Void
class Coordinator: UIViewController {
let callback: () -> Void
init(callback: @escaping () -> Void) {
self.callback = callback
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
callback()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(callback: callback)
}
func makeUIViewController(context: Context) -> UIViewController {
context.coordinator
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

View file

@ -53,7 +53,7 @@ struct SettingsView: View {
}
Section(header: InlineHeader("Playback services")) {
NavigationLink(destination: SettingsKodiView(), label: {
NavigationLink(destination: SettingsKodiView(kodiServers: kodiServers), label: {
HStack {
Text("Kodi")
Spacer()

View file

@ -6,10 +6,9 @@
//
import SwiftUI
import SwiftUIX
struct ActionChoiceView: View {
@Environment(\.presentationMode) var presentationMode
@Environment(\.dismiss) var dismiss
@EnvironmentObject var scrapingModel: ScrapingViewModel
@EnvironmentObject var debridManager: DebridManager
@ -32,7 +31,7 @@ struct ActionChoiceView: View {
var body: some View {
NavView {
Form {
Section(header: "Now Playing") {
Section(header: InlineHeader("Now Playing")) {
VStack(alignment: .leading, spacing: 5) {
Text(navModel.selectedTitle)
.font(.callout)
@ -47,7 +46,7 @@ struct ActionChoiceView: View {
}
if !debridManager.downloadUrl.isEmpty {
Section(header: "Debrid options") {
Section(header: InlineHeader("Debrid options")) {
ForEach(actions, id: \.id) { action in
if action.requires.contains(ActionRequirement.debrid.rawValue) {
ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") {
@ -66,22 +65,21 @@ struct ActionChoiceView: View {
} label: {
KodiServerView(server: server)
}
.backport.tint(.primary)
.tint(.primary)
}
}
.backport.tint(.secondary)
.tint(.secondary)
}
ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") {
UIPasteboard.general.string = debridManager.downloadUrl
showLinkCopyAlert.toggle()
}
.backport.alert(
isPresented: $showLinkCopyAlert,
title: "Copied",
message: "Download link copied successfully",
buttons: [AlertButton("OK")]
)
.alert("Copied", isPresented: $showLinkCopyAlert) {
Button("OK", role: .cancel) {}
} message: {
Text("Download link copied successfully")
}
ListRowButtonView("Share download URL", systemImage: "square.and.arrow.up.fill") {
if let url = URL(string: debridManager.downloadUrl) {
@ -93,7 +91,7 @@ struct ActionChoiceView: View {
}
if !navModel.resultFromCloud {
Section(header: "Magnet options") {
Section(header: InlineHeader("Magnet options")) {
ForEach(actions, id: \.id) { action in
if action.requires.contains(ActionRequirement.magnet.rawValue) {
ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") {
@ -106,12 +104,11 @@ struct ActionChoiceView: View {
UIPasteboard.general.string = navModel.selectedMagnet?.link
showMagnetCopyAlert.toggle()
}
.backport.alert(
isPresented: $showMagnetCopyAlert,
title: "Copied",
message: "Magnet link copied successfully",
buttons: [AlertButton("OK")]
)
.alert("Copied", isPresented: $showMagnetCopyAlert) {
Button("OK", role: .cancel) {}
} message: {
Text("Magnet link copied successfully")
}
ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") {
if let magnetLink = navModel.selectedMagnet?.link,
@ -124,25 +121,26 @@ struct ActionChoiceView: View {
}
}
}
.backport.tint(.primary)
.tint(.primary)
.sheet(isPresented: $navModel.showLocalActivitySheet) {
// TODO: Fix share sheet
if #available(iOS 16, *) {
AppActivityView(activityItems: navModel.activityItems)
ShareSheet(activityItems: navModel.activityItems)
.presentationDetents([.medium, .large])
} else {
AppActivityView(activityItems: navModel.activityItems)
ShareSheet(activityItems: navModel.activityItems)
}
}
.backport.alert(
isPresented: $pluginManager.showActionSuccessAlert,
title: "Action successful",
message: pluginManager.actionSuccessAlertMessage
)
.backport.alert(
isPresented: $pluginManager.showActionErrorAlert,
title: "Action error",
message: pluginManager.actionErrorAlertMessage
)
.alert("Action successful", isPresented: $pluginManager.showActionSuccessAlert) {
Button("OK", role: .cancel) {}
} message: {
Text(pluginManager.actionSuccessAlertMessage)
}
.alert("Action error", isPresented: $pluginManager.showActionErrorAlert) {
Button("OK", role: .cancel) {}
} message: {
Text(pluginManager.actionErrorAlertMessage)
}
.onDisappear {
debridManager.downloadUrl = ""
navModel.selectedTitle = ""
@ -158,7 +156,7 @@ struct ActionChoiceView: View {
navModel.selectedTitle = ""
navModel.selectedBatchTitle = ""
presentationMode.wrappedValue.dismiss()
dismiss()
}
}
}

View file

@ -48,7 +48,7 @@ struct BatchChoiceView: View {
EmptyView()
}
}
.backport.tint(.primary)
.tint(.primary)
.listStyle(.insetGrouped)
.inlinedList(inset: -20)
.navigationTitle("Select a file")