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:
parent
8f9f522846
commit
3828ffa539
57 changed files with 738 additions and 1081 deletions
|
|
@ -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" */;
|
||||
|
|
|
|||
35
Ferrite/Extensions/Color.swift
Normal file
35
Ferrite/Extensions/Color.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
15
Ferrite/Extensions/UIApplication.swift
Normal file
15
Ferrite/Extensions/UIApplication.swift
Normal 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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ struct EmptyInstructionView: View {
|
|||
.padding(.horizontal, 50)
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.secondaryLabel)
|
||||
.foregroundColor(.init(uiColor: .secondaryLabel))
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
Ferrite/Views/CommonViews/Modifiers/SearchListener.swift
Normal file
26
Ferrite/Views/CommonViews/Modifiers/SearchListener.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ?? ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ struct SourceSettingsBaseUrlView: View {
|
|||
.keyboardType(.URL)
|
||||
.autocorrectionDisabled(true)
|
||||
.autocapitalization(.none)
|
||||
.backport.onAppear {
|
||||
.onAppear {
|
||||
tempBaseUrl = selectedSource.baseUrl ?? ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,6 @@ struct SourceSettingsMethodView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
.tint(.primary)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
Ferrite/Views/RepresentableViews/ShareSheet.swift
Normal file
18
Ferrite/Views/RepresentableViews/ShareSheet.swift
Normal 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) {}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ struct BatchChoiceView: View {
|
|||
EmptyView()
|
||||
}
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
.tint(.primary)
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList(inset: -20)
|
||||
.navigationTitle("Select a file")
|
||||
|
|
|
|||
Loading…
Reference in a new issue