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 */; };
|
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; };
|
||||||
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; };
|
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; };
|
||||||
0C2D9653299316CC00A504B6 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2D9652299316CC00A504B6 /* Tag.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 */; };
|
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 */; };
|
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; };
|
||||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; };
|
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; };
|
||||||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.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 */; };
|
0C3DD43F29B6968D006429DB /* KodiEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3DD43E29B6968D006429DB /* KodiEditorView.swift */; };
|
||||||
0C3DD44229B6ACD9006429DB /* KodiServer+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3DD44029B6ACD9006429DB /* KodiServer+CoreDataClass.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 */; };
|
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 */; };
|
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2A728D4DDDC007711AE /* Application.swift */; };
|
||||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AC28D51C63007711AE /* BackupManager.swift */; };
|
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AC28D51C63007711AE /* BackupManager.swift */; };
|
||||||
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AE28D52E8A007711AE /* BackupsView.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 */; };
|
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; };
|
||||||
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; };
|
||||||
0C5708EB29B8F89300BE07F9 /* SettingsLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5708EA29B8F89300BE07F9 /* SettingsLogView.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 */; };
|
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; };
|
||||||
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; };
|
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; };
|
||||||
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
|
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
|
||||||
|
|
@ -71,6 +67,8 @@
|
||||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.swift */; };
|
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.swift */; };
|
||||||
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */; };
|
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */; };
|
||||||
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9C29315292002DF910 /* AllDebridModels.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 */; };
|
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */; };
|
||||||
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */; };
|
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */; };
|
||||||
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.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 */; };
|
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA429F728C5098D000D0610 /* DateFormatter.swift */; };
|
||||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
|
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
|
||||||
0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAF9318296399190050812A /* PremiumizeCloudView.swift */; };
|
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 */; };
|
0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB0AB5E29BD2A200015422C /* KodiServerView.swift */; };
|
||||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; };
|
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; };
|
||||||
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.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 */; };
|
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; };
|
||||||
0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */ = {isa = PBXBuildFile; productRef = 0CDDDE042935235E006810B1 /* BetterSafariView */; };
|
0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */ = {isa = PBXBuildFile; productRef = 0CDDDE042935235E006810B1 /* BetterSafariView */; };
|
||||||
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1C4172981E8D700418F20 /* Plugin.swift */; };
|
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 */; };
|
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */; };
|
||||||
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
|
@ -170,8 +169,6 @@
|
||||||
0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
|
@ -297,7 +296,6 @@
|
||||||
0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */,
|
0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */,
|
||||||
0C448BE929A135F100F4E266 /* Introspect-Static in Frameworks */,
|
0C448BE929A135F100F4E266 /* Introspect-Static in Frameworks */,
|
||||||
0C64A4B4288903680079976D /* Base32 in Frameworks */,
|
0C64A4B4288903680079976D /* Base32 in Frameworks */,
|
||||||
0C2DD4CF29A6D47400293FC3 /* SwiftUIX in Frameworks */,
|
|
||||||
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */,
|
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */,
|
||||||
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */,
|
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */,
|
||||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */,
|
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */,
|
||||||
|
|
@ -448,7 +446,7 @@
|
||||||
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */,
|
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */,
|
||||||
0CB6516428C5A5D700DCA721 /* InlinedList.swift */,
|
0CB6516428C5A5D700DCA721 /* InlinedList.swift */,
|
||||||
0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */,
|
0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */,
|
||||||
0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */,
|
0C45E6A429D4B2FE00F047D2 /* SearchListener.swift */,
|
||||||
);
|
);
|
||||||
path = Modifiers;
|
path = Modifiers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -468,6 +466,7 @@
|
||||||
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */,
|
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */,
|
||||||
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */,
|
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */,
|
||||||
0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */,
|
0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */,
|
||||||
|
0CB0115A29D36D9E009AFEDE /* SearchResultsView.swift */,
|
||||||
);
|
);
|
||||||
path = SearchResult;
|
path = SearchResult;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -529,9 +528,6 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
0C44E2A928D4DFC4007711AE /* Modifiers */,
|
0C44E2A928D4DFC4007711AE /* Modifiers */,
|
||||||
0CE66B3928E640D200F69346 /* Backport.swift */,
|
|
||||||
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */,
|
|
||||||
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */,
|
|
||||||
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */,
|
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */,
|
||||||
0C871BDE29994D9D005279AC /* FilterLabelView.swift */,
|
0C871BDE29994D9D005279AC /* FilterLabelView.swift */,
|
||||||
0CA148C1288903F000DE2211 /* NavView.swift */,
|
0CA148C1288903F000DE2211 /* NavView.swift */,
|
||||||
|
|
@ -558,6 +554,7 @@
|
||||||
0CD72E16293D9928001A7EA4 /* Array.swift */,
|
0CD72E16293D9928001A7EA4 /* Array.swift */,
|
||||||
0C445C61293F9A0B0060744D /* Bundle.swift */,
|
0C445C61293F9A0B0060744D /* Bundle.swift */,
|
||||||
0CA148C9288903F000DE2211 /* Collection.swift */,
|
0CA148C9288903F000DE2211 /* Collection.swift */,
|
||||||
|
0C7075E329D374C50093DB2D /* Color.swift */,
|
||||||
0CA148CA288903F000DE2211 /* Data.swift */,
|
0CA148CA288903F000DE2211 /* Data.swift */,
|
||||||
0CA429F728C5098D000D0610 /* DateFormatter.swift */,
|
0CA429F728C5098D000D0610 /* DateFormatter.swift */,
|
||||||
0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */,
|
0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */,
|
||||||
|
|
@ -569,6 +566,7 @@
|
||||||
0C42B5972932F6DD008057A0 /* Set.swift */,
|
0C42B5972932F6DD008057A0 /* Set.swift */,
|
||||||
0C7C128528DAA3CD00381CD1 /* URL.swift */,
|
0C7C128528DAA3CD00381CD1 /* URL.swift */,
|
||||||
0C50B7CF299DF63C00A9FA3C /* UIDevice.swift */,
|
0C50B7CF299DF63C00A9FA3C /* UIDevice.swift */,
|
||||||
|
0CF2C0A429D1EBD400E716DD /* UIApplication.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -608,7 +606,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
0CA148CE288903F000DE2211 /* WebView.swift */,
|
0CA148CE288903F000DE2211 /* WebView.swift */,
|
||||||
0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */,
|
0C7075E529D3845D0093DB2D /* ShareSheet.swift */,
|
||||||
);
|
);
|
||||||
path = RepresentableViews;
|
path = RepresentableViews;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -690,7 +688,6 @@
|
||||||
0C7506D628B1AC9A008BEE38 /* SwiftyJSON */,
|
0C7506D628B1AC9A008BEE38 /* SwiftyJSON */,
|
||||||
0CDDDE042935235E006810B1 /* BetterSafariView */,
|
0CDDDE042935235E006810B1 /* BetterSafariView */,
|
||||||
0C448BE829A135F100F4E266 /* Introspect-Static */,
|
0C448BE829A135F100F4E266 /* Introspect-Static */,
|
||||||
0C2DD4CE29A6D47400293FC3 /* SwiftUIX */,
|
|
||||||
);
|
);
|
||||||
productName = Torrenter;
|
productName = Torrenter;
|
||||||
productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */;
|
productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */;
|
||||||
|
|
@ -728,7 +725,6 @@
|
||||||
0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
|
0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
|
||||||
0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */,
|
0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */,
|
||||||
0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||||
0C2DD4CD29A6D47400293FC3 /* XCRemoteSwiftPackageReference "SwiftUIX" */,
|
|
||||||
);
|
);
|
||||||
productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */;
|
productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
|
@ -803,11 +799,10 @@
|
||||||
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */,
|
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */,
|
||||||
0CA0545B288EEA4E00850554 /* PluginListEditorView.swift in Sources */,
|
0CA0545B288EEA4E00850554 /* PluginListEditorView.swift in Sources */,
|
||||||
0C3E00D8296F5B9A00ECECB2 /* PluginModels.swift in Sources */,
|
0C3E00D8296F5B9A00ECECB2 /* PluginModels.swift in Sources */,
|
||||||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */,
|
|
||||||
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */,
|
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */,
|
||||||
0C5005522992B6750064606A /* PluginTagsView.swift in Sources */,
|
0C5005522992B6750064606A /* PluginTagsView.swift in Sources */,
|
||||||
0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */,
|
0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */,
|
||||||
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */,
|
0CF2C0A529D1EBD400E716DD /* UIApplication.swift in Sources */,
|
||||||
0C42B5962932F2D5008057A0 /* DebridPickerView.swift in Sources */,
|
0C42B5962932F2D5008057A0 /* DebridPickerView.swift in Sources */,
|
||||||
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */,
|
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */,
|
||||||
0C3DD44229B6ACD9006429DB /* KodiServer+CoreDataClass.swift in Sources */,
|
0C3DD44229B6ACD9006429DB /* KodiServer+CoreDataClass.swift in Sources */,
|
||||||
|
|
@ -831,7 +826,6 @@
|
||||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
||||||
0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */,
|
0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */,
|
||||||
0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */,
|
0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */,
|
||||||
0C572D4C2993FC2A003EEC05 /* ViewDidAppearHandler.swift in Sources */,
|
|
||||||
0C95D8D828A55B03005E22B3 /* DefaultActionPickerView.swift in Sources */,
|
0C95D8D828A55B03005E22B3 /* DefaultActionPickerView.swift in Sources */,
|
||||||
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */,
|
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */,
|
||||||
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
||||||
|
|
@ -877,12 +871,15 @@
|
||||||
0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */,
|
0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */,
|
||||||
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */,
|
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */,
|
||||||
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */,
|
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */,
|
||||||
|
0C45E6A529D4B2FE00F047D2 /* SearchListener.swift in Sources */,
|
||||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
||||||
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */,
|
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */,
|
||||||
|
0C7075E629D3845D0093DB2D /* ShareSheet.swift in Sources */,
|
||||||
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */,
|
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */,
|
||||||
0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */,
|
0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */,
|
||||||
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */,
|
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */,
|
||||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
||||||
|
0C7075E429D374C50093DB2D /* Color.swift in Sources */,
|
||||||
0C8DC35229CE287E008A83AD /* PluginInfoView.swift in Sources */,
|
0C8DC35229CE287E008A83AD /* PluginInfoView.swift in Sources */,
|
||||||
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */,
|
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */,
|
||||||
0C8DC35629CE2ABF008A83AD /* SourceSettingsApiView.swift in Sources */,
|
0C8DC35629CE2ABF008A83AD /* SourceSettingsApiView.swift in Sources */,
|
||||||
|
|
@ -890,10 +887,10 @@
|
||||||
0C0974B029CCAAAF006DE7A3 /* OperatingSystemVersion.swift in Sources */,
|
0C0974B029CCAAAF006DE7A3 /* OperatingSystemVersion.swift in Sources */,
|
||||||
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
|
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
|
||||||
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
|
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
|
||||||
0C572D4E299403B7003EEC05 /* ViewDidAppear.swift in Sources */,
|
|
||||||
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
|
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
|
||||||
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */,
|
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */,
|
||||||
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */,
|
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */,
|
||||||
|
0CB0115B29D36D9E009AFEDE /* SearchResultsView.swift in Sources */,
|
||||||
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */,
|
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */,
|
||||||
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */,
|
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */,
|
||||||
0C6771F629B3B602005D38D2 /* SettingsKodiView.swift in Sources */,
|
0C6771F629B3B602005D38D2 /* SettingsKodiView.swift in Sources */,
|
||||||
|
|
@ -902,7 +899,6 @@
|
||||||
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */,
|
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */,
|
||||||
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */,
|
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */,
|
||||||
0C3DD43F29B6968D006429DB /* KodiEditorView.swift in Sources */,
|
0C3DD43F29B6968D006429DB /* KodiEditorView.swift in Sources */,
|
||||||
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */,
|
|
||||||
0C3DD44329B6ACD9006429DB /* KodiServer+CoreDataProperties.swift in Sources */,
|
0C3DD44329B6ACD9006429DB /* KodiServer+CoreDataProperties.swift in Sources */,
|
||||||
0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */,
|
0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */,
|
||||||
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */,
|
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */,
|
||||||
|
|
@ -1124,14 +1120,6 @@
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
0C2DD4CD29A6D47400293FC3 /* XCRemoteSwiftPackageReference "SwiftUIX" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/SwiftUIX/SwiftUIX";
|
|
||||||
requirement = {
|
|
||||||
branch = master;
|
|
||||||
kind = branch;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
|
0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect/";
|
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect/";
|
||||||
|
|
@ -1191,11 +1179,6 @@
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
0C2DD4CE29A6D47400293FC3 /* SwiftUIX */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = 0C2DD4CD29A6D47400293FC3 /* XCRemoteSwiftPackageReference "SwiftUIX" */;
|
|
||||||
productName = SwiftUIX;
|
|
||||||
};
|
|
||||||
0C448BE829A135F100F4E266 /* Introspect-Static */ = {
|
0C448BE829A135F100F4E266 /* Introspect-Static */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
|
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.
|
// Created by Brian Dashore on 2/16/23.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import UIKit
|
||||||
|
|
||||||
extension UIDevice {
|
extension UIDevice {
|
||||||
var hasNotch: Bool {
|
var hasNotch: Bool {
|
||||||
if #available(iOS 11.0, *) {
|
if #available(iOS 11.0, *) {
|
||||||
let keyWindow = UIApplication.shared.windows.filter(\.isKeyWindow).first
|
return UIApplication.shared.currentUIWindow?.safeAreaInsets.bottom ?? 0 > 0
|
||||||
return keyWindow?.safeAreaInsets.bottom ?? 0 > 0
|
} else {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,11 @@ extension View {
|
||||||
modifier(InlinedListModifier(inset: inset))
|
modifier(InlinedListModifier(inset: inset))
|
||||||
}
|
}
|
||||||
|
|
||||||
func viewDidAppear(_ callback: @escaping () -> Void) -> some View {
|
|
||||||
modifier(ViewDidAppearModifier(callback: callback))
|
|
||||||
}
|
|
||||||
|
|
||||||
func customScopeBar(_ content: @escaping () -> some View) -> some View {
|
func customScopeBar(_ content: @escaping () -> some View) -> some View {
|
||||||
modifier(CustomScopeBarModifier(scopeBarContent: content()))
|
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 {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
MainView()
|
MainView()
|
||||||
.backport.onAppear {
|
.onAppear {
|
||||||
scrapingModel.logManager = logManager
|
scrapingModel.logManager = logManager
|
||||||
debridManager.logManager = logManager
|
debridManager.logManager = logManager
|
||||||
pluginManager.logManager = logManager
|
pluginManager.logManager = logManager
|
||||||
|
|
|
||||||
|
|
@ -186,14 +186,7 @@ public class BackupManager: ObservableObject {
|
||||||
|
|
||||||
PersistenceController.shared.save(backgroundContext)
|
PersistenceController.shared.save(backgroundContext)
|
||||||
|
|
||||||
// if iOS 14 is available, sleep to prevent any issues with alerts
|
await toggleRestoreCompletedAlert()
|
||||||
if #available(iOS 15, *) {
|
|
||||||
await toggleRestoreCompletedAlert()
|
|
||||||
} else {
|
|
||||||
try? await Task.sleep(seconds: 0.1)
|
|
||||||
|
|
||||||
await toggleRestoreCompletedAlert()
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
await logManager?.error(
|
await logManager?.error(
|
||||||
"Backup restore: \(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.")
|
Text("Ferrite is a free and open source application developed by kingbri under the GNU-GPLv3 license.")
|
||||||
.textCase(.none)
|
.textCase(.none)
|
||||||
.foregroundColor(.label)
|
.foregroundColor(.init(uiColor: .label))
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
.padding(.bottom, 20)
|
.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)
|
.padding(.horizontal, 50)
|
||||||
}
|
}
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.foregroundColor(.secondaryLabel)
|
.foregroundColor(.init(uiColor: .secondaryLabel))
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,14 @@ struct FilterLabelView: View {
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
Image(systemName: "chevron.down")
|
Image(systemName: "chevron.down")
|
||||||
.foregroundColor(.tertiaryLabel)
|
.foregroundColor(.init(uiColor: .tertiaryLabel))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 9)
|
.padding(.horizontal, 9)
|
||||||
.padding(.vertical, 7)
|
.padding(.vertical, 7)
|
||||||
.font(.caption, weight: .medium)
|
.font(
|
||||||
.background(Capsule().foregroundColor(.secondarySystemFill))
|
.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 * 0.6, y: 0)
|
||||||
.offset(x: reader.size.width * 1.2 * self.offset, y: 0)
|
.offset(x: reader.size.width * 1.2 * self.offset, y: 0)
|
||||||
.animation(.default.repeatForever().speed(0.5), value: self.offset)
|
.animation(.default.repeatForever().speed(0.5), value: self.offset)
|
||||||
.backport.onAppear {
|
.onAppear {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
self.offset = 1
|
self.offset = 1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,9 @@ struct InlineHeader: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16, *) {
|
if #available(iOS 16, *) {
|
||||||
Text(title)
|
Text(title)
|
||||||
} else if #available(iOS 15, *) {
|
|
||||||
Text(title)
|
|
||||||
.listRowInsets(EdgeInsets(top: 10, leading: 15, bottom: 0, trailing: 0))
|
|
||||||
} else {
|
} else {
|
||||||
Text(title)
|
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>?
|
@State private var hostingController: UIHostingController<V>?
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
if #available(iOS 15, *) {
|
content
|
||||||
content
|
.introspectSearchController { searchController in
|
||||||
.backport.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.hidesNavigationBarDuringPresentation = true
|
||||||
searchController.searchBar.showsScopeBar = true
|
searchController.searchBar.showsScopeBar = true
|
||||||
searchController.searchBar.scopeButtonTitles = [""]
|
searchController.searchBar.scopeButtonTitles = [""]
|
||||||
(searchController.searchBar.value(forKey: "_scopeBar") as? UIView)?.isHidden = true
|
(searchController.searchBar.value(forKey: "_scopeBar") as? UIView)?.isHidden = true
|
||||||
|
|
||||||
let hostingController = UIHostingController(rootView: scopeBarContent)
|
let hostingController = UIHostingController(rootView: scopeBarContent)
|
||||||
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
hostingController.view.backgroundColor = .clear
|
hostingController.view.backgroundColor = .clear
|
||||||
|
|
||||||
guard let containerView = searchController.searchBar.value(forKey: "_scopeBarContainerView") as? UIView else {
|
guard let containerView = searchController.searchBar.value(forKey: "_scopeBarContainerView") as? UIView else {
|
||||||
return
|
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
|
|
||||||
}
|
}
|
||||||
.introspectNavigationController { navigationController in
|
containerView.addSubview(hostingController.view)
|
||||||
if #available(iOS 16, *) {
|
|
||||||
navigationController.viewControllers.first?.navigationItem.preferredSearchBarPlacement = .stacked
|
|
||||||
}
|
|
||||||
|
|
||||||
navigationController.navigationBar.prefersLargeTitles = true
|
NSLayoutConstraint.activate([
|
||||||
navigationController.navigationBar.sizeToFit()
|
hostingController.view.widthAnchor.constraint(equalTo: containerView.widthAnchor),
|
||||||
}
|
hostingController.view.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||||
} else {
|
hostingController.view.heightAnchor.constraint(equalTo: containerView.heightAnchor)
|
||||||
VStack {
|
])
|
||||||
scopeBarContent
|
|
||||||
content
|
self.hostingController = hostingController
|
||||||
Spacer()
|
}
|
||||||
|
.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)
|
.padding(.vertical, verticalPadding)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 5)
|
RoundedRectangle(cornerRadius: 5)
|
||||||
.foregroundColor(color.map { $0 } ?? .tertiaryLabel)
|
.foregroundColor(color.map { $0 } ?? .init(uiColor: .tertiaryLabel))
|
||||||
.opacity(0.3)
|
.opacity(0.3)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,84 +8,65 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct BookmarksView: View {
|
struct BookmarksView: View {
|
||||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
|
||||||
|
|
||||||
@EnvironmentObject var navModel: NavigationViewModel
|
@EnvironmentObject var navModel: NavigationViewModel
|
||||||
@EnvironmentObject var debridManager: DebridManager
|
@EnvironmentObject var debridManager: DebridManager
|
||||||
|
|
||||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||||
|
|
||||||
@Binding var searchText: String
|
@Binding var searchText: String
|
||||||
@Binding var bookmarksEmpty: Bool
|
|
||||||
|
|
||||||
@State private var viewTask: Task<Void, Never>?
|
var bookmarks: FetchedResults<Bookmark>
|
||||||
@State private var bookmarkPredicate: NSPredicate?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
DynamicFetchRequest(
|
List {
|
||||||
predicate: bookmarkPredicate,
|
if !bookmarks.isEmpty {
|
||||||
sortDescriptors: [NSSortDescriptor(keyPath: \Bookmark.orderNum, ascending: true)]
|
ForEach(bookmarks, id: \.self) { bookmark in
|
||||||
) { (bookmarks: FetchedResults<Bookmark>) in
|
SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark)
|
||||||
List {
|
}
|
||||||
if !bookmarks.isEmpty {
|
.onDelete { offsets in
|
||||||
ForEach(bookmarks, id: \.self) { bookmark in
|
for index in offsets {
|
||||||
SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark)
|
if let bookmark = bookmarks[safe: index] {
|
||||||
}
|
PersistenceController.shared.delete(bookmark, context: backgroundContext)
|
||||||
.onDelete { offsets in
|
NotificationCenter.default.post(name: .didDeleteBookmark, object: bookmark)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
.onMove { source, destination in
|
||||||
.listStyle(.insetGrouped)
|
var changedBookmarks = bookmarks.map { $0 }
|
||||||
.inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 15 : -25)
|
|
||||||
.backport.onAppear {
|
|
||||||
bookmarksEmpty = bookmarks.isEmpty
|
|
||||||
|
|
||||||
if debridManager.enabledDebrids.count > 0 {
|
changedBookmarks.move(fromOffsets: source, toOffset: destination)
|
||||||
viewTask = Task {
|
|
||||||
let magnets = bookmarks.compactMap {
|
for reverseIndex in stride(from: changedBookmarks.count - 1, through: 0, by: -1) {
|
||||||
if let magnetHash = $0.magnetHash {
|
changedBookmarks[reverseIndex].orderNum = Int16(reverseIndex)
|
||||||
return Magnet(hash: magnetHash, link: $0.magnetLink)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await debridManager.populateDebridIA(magnets)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PersistenceController.shared.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onDisappear {
|
|
||||||
viewTask?.cancel()
|
|
||||||
}
|
|
||||||
.onChange(of: bookmarks.count) { newCount in
|
|
||||||
bookmarksEmpty = newCount == 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.backport.onAppear {
|
.onAppear {
|
||||||
applyPredicate()
|
fetchPredicate()
|
||||||
}
|
}
|
||||||
.onChange(of: searchText) { _ in
|
.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() {
|
func fetchPredicate() {
|
||||||
bookmarkPredicate = searchText.isEmpty ? nil : NSPredicate(format: "title CONTAINS[cd] %@", searchText)
|
bookmarks.nsPredicate = searchText.isEmpty ? nil : NSPredicate(format: "title CONTAINS[cd] %@", searchText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@ struct AllDebridCloudView: View {
|
||||||
|
|
||||||
@Binding var searchText: String
|
@Binding var searchText: String
|
||||||
|
|
||||||
@State private var viewTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
DisclosureGroup("Magnets") {
|
DisclosureGroup("Magnets") {
|
||||||
ForEach(debridManager.allDebridCloudMagnets.filter {
|
ForEach(debridManager.allDebridCloudMagnets.filter {
|
||||||
|
|
@ -72,7 +70,7 @@ struct AllDebridCloudView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||||
.backport.tint(.black)
|
.tint(.black)
|
||||||
}
|
}
|
||||||
.onDelete { offsets in
|
.onDelete { offsets in
|
||||||
for index in offsets {
|
for index in offsets {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftUIX
|
|
||||||
|
|
||||||
struct PremiumizeCloudView: View {
|
struct PremiumizeCloudView: View {
|
||||||
@EnvironmentObject var debridManager: DebridManager
|
@EnvironmentObject var debridManager: DebridManager
|
||||||
|
|
@ -15,8 +14,6 @@ struct PremiumizeCloudView: View {
|
||||||
|
|
||||||
@Binding var searchText: String
|
@Binding var searchText: String
|
||||||
|
|
||||||
@State private var viewTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
DisclosureGroup("Items") {
|
DisclosureGroup("Items") {
|
||||||
ForEach(debridManager.premiumizeCloudItems.filter {
|
ForEach(debridManager.premiumizeCloudItems.filter {
|
||||||
|
|
@ -47,7 +44,7 @@ struct PremiumizeCloudView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||||
.backport.tint(.black)
|
.tint(.black)
|
||||||
}
|
}
|
||||||
.onDelete { offsets in
|
.onDelete { offsets in
|
||||||
for index in offsets {
|
for index in offsets {
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@ struct RealDebridCloudView: View {
|
||||||
|
|
||||||
@Binding var searchText: String
|
@Binding var searchText: String
|
||||||
|
|
||||||
@State private var viewTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
DisclosureGroup("Downloads") {
|
DisclosureGroup("Downloads") {
|
||||||
|
|
@ -41,7 +39,7 @@ struct RealDebridCloudView: View {
|
||||||
navModel: navModel
|
navModel: navModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.backport.tint(.primary)
|
.tint(.primary)
|
||||||
}
|
}
|
||||||
.onDelete { offsets in
|
.onDelete { offsets in
|
||||||
for index in offsets {
|
for index in offsets {
|
||||||
|
|
@ -111,7 +109,7 @@ struct RealDebridCloudView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||||
.backport.tint(.primary)
|
.tint(.primary)
|
||||||
}
|
}
|
||||||
.onDelete { offsets in
|
.onDelete { offsets in
|
||||||
for index in offsets {
|
for index in offsets {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@ struct DebridCloudView: View {
|
||||||
|
|
||||||
@Binding var searchText: String
|
@Binding var searchText: String
|
||||||
|
|
||||||
@State private var viewTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
switch debridManager.selectedDebridType {
|
switch debridManager.selectedDebridType {
|
||||||
|
|
@ -28,19 +26,12 @@ struct DebridCloudView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.backport.onAppear {
|
.task {
|
||||||
viewTask = Task {
|
await debridManager.fetchDebridCloud()
|
||||||
await debridManager.fetchDebridCloud()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
viewTask?.cancel()
|
|
||||||
}
|
}
|
||||||
.onChange(of: debridManager.selectedDebridType) { newType in
|
.onChange(of: debridManager.selectedDebridType) { newType in
|
||||||
viewTask?.cancel()
|
|
||||||
|
|
||||||
if newType != nil {
|
if newType != nil {
|
||||||
viewTask = Task {
|
Task {
|
||||||
await debridManager.fetchDebridCloud()
|
await debridManager.fetchDebridCloud()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,26 +16,27 @@ struct HistoryActionsView: View {
|
||||||
Button("Clear") {
|
Button("Clear") {
|
||||||
showActionSheet.toggle()
|
showActionSheet.toggle()
|
||||||
}
|
}
|
||||||
.backport.tint(.red)
|
.tint(.red)
|
||||||
.backport.confirmationDialog(
|
.confirmationDialog(
|
||||||
|
"Clear watch history",
|
||||||
isPresented: $showActionSheet,
|
isPresented: $showActionSheet,
|
||||||
title: "Clear watch history",
|
titleVisibility: .visible
|
||||||
message: "This is an irreversible action!",
|
) {
|
||||||
buttons: [
|
Button("Past day", role: .destructive) {
|
||||||
AlertButton("Past day", role: .destructive) {
|
deleteHistory(.day)
|
||||||
deleteHistory(.day)
|
}
|
||||||
},
|
Button("Past week", role: .destructive) {
|
||||||
AlertButton("Past week", role: .destructive) {
|
deleteHistory(.week)
|
||||||
deleteHistory(.week)
|
}
|
||||||
},
|
Button("Past month", role: .destructive) {
|
||||||
AlertButton("Past month", role: .destructive) {
|
deleteHistory(.month)
|
||||||
deleteHistory(.month)
|
}
|
||||||
},
|
Button("All time", role: .destructive) {
|
||||||
AlertButton("All time", role: .destructive) {
|
deleteHistory(.allTime)
|
||||||
deleteHistory(.allTime)
|
}
|
||||||
}
|
} message: {
|
||||||
]
|
Text("This is an irreversible action!")
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteHistory(_ deleteRange: HistoryDeleteRange) {
|
func deleteHistory(_ deleteRange: HistoryDeleteRange) {
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ struct HistoryButtonView: View {
|
||||||
}
|
}
|
||||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
.backport.tint(.primary)
|
.tint(.primary)
|
||||||
.disableInteraction(navModel.currentChoiceSheet != nil)
|
.disableInteraction(navModel.currentChoiceSheet != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,29 +17,24 @@ struct HistoryView: View {
|
||||||
]
|
]
|
||||||
) var history: FetchedResults<History>
|
) var history: FetchedResults<History>
|
||||||
|
|
||||||
|
var allHistoryEntries: FetchedResults<HistoryEntry>
|
||||||
|
|
||||||
@Binding var searchText: String
|
@Binding var searchText: String
|
||||||
@Binding var historyEmpty: Bool
|
|
||||||
|
|
||||||
@State private var historyPredicate: NSPredicate?
|
@State private var historyPredicate: NSPredicate?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
DynamicFetchRequest(predicate: historyPredicate) { (allEntries: FetchedResults<HistoryEntry>) in
|
List {
|
||||||
List {
|
if !history.isEmpty {
|
||||||
if !history.isEmpty {
|
ForEach(groupedHistory(history), id: \.self) { historyGroup in
|
||||||
ForEach(groupedHistory(history), id: \.self) { historyGroup in
|
HistorySectionView(allEntries: allHistoryEntries, historyGroup: historyGroup)
|
||||||
HistorySectionView(allEntries: allEntries, historyGroup: historyGroup)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
|
||||||
}
|
}
|
||||||
.backport.onAppear {
|
.listStyle(.insetGrouped)
|
||||||
historyEmpty = history.isEmpty
|
.onAppear {
|
||||||
applyPredicate()
|
applyPredicate()
|
||||||
}
|
}
|
||||||
.onChange(of: history.count) { newCount in
|
|
||||||
historyEmpty = newCount == 0
|
|
||||||
}
|
|
||||||
.onChange(of: searchText) { _ in
|
.onChange(of: searchText) { _ in
|
||||||
applyPredicate()
|
applyPredicate()
|
||||||
}
|
}
|
||||||
|
|
@ -47,11 +42,11 @@ struct HistoryView: View {
|
||||||
|
|
||||||
func applyPredicate() {
|
func applyPredicate() {
|
||||||
if searchText.isEmpty {
|
if searchText.isEmpty {
|
||||||
historyPredicate = nil
|
allHistoryEntries.nsPredicate = nil
|
||||||
} else {
|
} else {
|
||||||
let namePredicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText.lowercased())
|
let namePredicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText.lowercased())
|
||||||
let subNamePredicate = NSPredicate(format: "subName 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")
|
Image(systemName: "gear")
|
||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 15.0, *) {
|
Button(role: .destructive) {
|
||||||
Button(role: .destructive) {
|
PersistenceController.shared.delete(installedPlugin, context: backgroundContext)
|
||||||
PersistenceController.shared.delete(installedPlugin, context: backgroundContext)
|
} label: {
|
||||||
} label: {
|
Text("Remove")
|
||||||
Text("Remove")
|
Image(systemName: "trash")
|
||||||
Image(systemName: "trash")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button {
|
|
||||||
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(.horizontal, 7)
|
||||||
.padding(.vertical, 5)
|
.padding(.vertical, 5)
|
||||||
.background(.tertiarySystemBackground)
|
.background(Color.init(uiColor: .tertiarySystemBackground))
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,11 @@ struct PluginAggregateView<P: Plugin, PJ: PluginJson>: View {
|
||||||
sortDescriptors: []
|
sortDescriptors: []
|
||||||
) var pluginLists: FetchedResults<PluginList>
|
) var pluginLists: FetchedResults<PluginList>
|
||||||
|
|
||||||
|
var installedPlugins: FetchedResults<P>
|
||||||
|
|
||||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||||
|
|
||||||
@Binding var searchText: String
|
@Binding var searchText: String
|
||||||
@Binding var pluginsEmpty: Bool
|
|
||||||
|
|
||||||
@State private var isEditingSearch = false
|
@State private var isEditingSearch = false
|
||||||
@State private var isSearching = false
|
@State private var isSearching = false
|
||||||
|
|
@ -31,67 +32,63 @@ struct PluginAggregateView<P: Plugin, PJ: PluginJson>: View {
|
||||||
@State private var selectedPlugin: P?
|
@State private var selectedPlugin: P?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
List {
|
||||||
DynamicFetchRequest(predicate: sourcePredicate) { (installedPlugins: FetchedResults<P>) in
|
if
|
||||||
List {
|
let filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(
|
||||||
if
|
forType: PJ.self,
|
||||||
let filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(
|
installedPlugins: installedPlugins,
|
||||||
forType: PJ.self,
|
searchText: searchText
|
||||||
installedPlugins: installedPlugins,
|
),
|
||||||
searchText: searchText
|
!filteredUpdatedPlugins.isEmpty
|
||||||
),
|
{
|
||||||
!filteredUpdatedPlugins.isEmpty
|
Section(header: InlineHeader("Updates")) {
|
||||||
{
|
ForEach(filteredUpdatedPlugins, id: \.self) { (updatedPlugin: PJ) in
|
||||||
Section(header: InlineHeader("Updates")) {
|
PluginCatalogButtonView(availablePlugin: updatedPlugin, needsUpdate: true)
|
||||||
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) {
|
.sheet(isPresented: $showPluginOptions) {
|
||||||
PluginInfoView(selectedPlugin: $selectedPlugin)
|
PluginInfoView(selectedPlugin: $selectedPlugin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchPredicate() {
|
||||||
|
installedPlugins.nsPredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PluginInfoView<P: Plugin>: View {
|
struct PluginInfoView<P: Plugin>: View {
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
@Binding var selectedPlugin: P?
|
@Binding var selectedPlugin: P?
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ struct PluginInfoView<P: Plugin>: View {
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("Done") {
|
Button("Done") {
|
||||||
presentationMode.wrappedValue.dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ struct PluginTagsView: View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack {
|
HStack {
|
||||||
ForEach(tags, id: \.self) { tag in
|
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)
|
.autocorrectionDisabled(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.backport.onAppear {
|
.onAppear {
|
||||||
tempClientId = clientId.value ?? ""
|
tempClientId = clientId.value ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +45,7 @@ struct SourceSettingsApiView: View {
|
||||||
})
|
})
|
||||||
.autocorrectionDisabled(true)
|
.autocorrectionDisabled(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.backport.onAppear {
|
.onAppear {
|
||||||
tempClientSecret = clientSecret.value ?? ""
|
tempClientSecret = clientSecret.value ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ struct SourceSettingsBaseUrlView: View {
|
||||||
.keyboardType(.URL)
|
.keyboardType(.URL)
|
||||||
.autocorrectionDisabled(true)
|
.autocorrectionDisabled(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.backport.onAppear {
|
.onAppear {
|
||||||
tempBaseUrl = selectedSource.baseUrl ?? ""
|
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)
|
.id(debridManager.selectedDebridType)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, verticalSizeClass == .compact ? (Application.shared.osVersion.majorVersion > 14 ? 65 : 18) : 18)
|
.padding(.horizontal, verticalSizeClass == .compact ? 65 : 18)
|
||||||
.padding(.top, Application.shared.osVersion.majorVersion > 14 ? 0 : 10)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ struct SearchResultButtonView: View {
|
||||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
.disableInteraction(navModel.currentChoiceSheet != nil)
|
.disableInteraction(navModel.currentChoiceSheet != nil)
|
||||||
.backport.tint(.primary)
|
.tint(.primary)
|
||||||
.conditionalContextMenu(id: existingBookmark) {
|
.conditionalContextMenu(id: existingBookmark) {
|
||||||
ZStack {
|
ZStack {
|
||||||
if let bookmark = existingBookmark {
|
if let bookmark = existingBookmark {
|
||||||
|
|
@ -123,19 +123,19 @@ struct SearchResultButtonView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.alert(
|
.alert("Caching file", isPresented: $debridManager.showDeleteAlert) {
|
||||||
isPresented: $debridManager.showDeleteAlert,
|
Button("Yes", role: .destructive) {
|
||||||
title: "Caching file",
|
Task {
|
||||||
message: "RealDebrid is currently caching this file. Would you like to delete it? \n\nProgress can be checked on the RealDebrid website.",
|
await debridManager.deleteRdTorrent()
|
||||||
buttons: [
|
}
|
||||||
AlertButton("Yes", role: .destructive) {
|
}
|
||||||
Task {
|
Button("Cancel", role: .cancel) {}
|
||||||
await debridManager.deleteRdTorrent()
|
} message: {
|
||||||
}
|
Text(
|
||||||
},
|
"RealDebrid is currently caching this file. Would you like to delete it? \n\n" +
|
||||||
AlertButton(role: .cancel)
|
"Progress can be checked on the RealDebrid website."
|
||||||
]
|
)
|
||||||
)
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .didDeleteBookmark)) { notification in
|
.onReceive(NotificationCenter.default.publisher(for: .didDeleteBookmark)) { notification in
|
||||||
// If the instance contains the deleted bookmark, remove it.
|
// If the instance contains the deleted bookmark, remove it.
|
||||||
if let deletedBookmark = notification.object as? Bookmark,
|
if let deletedBookmark = notification.object as? Bookmark,
|
||||||
|
|
@ -145,7 +145,7 @@ struct SearchResultButtonView: View {
|
||||||
existingBookmark = nil
|
existingBookmark = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.onAppear {
|
.onAppear {
|
||||||
// Only run a exists request if a bookmark isn't passed to the view
|
// Only run a exists request if a bookmark isn't passed to the view
|
||||||
if existingBookmark == nil, !runOnce {
|
if existingBookmark == nil, !runOnce {
|
||||||
let bookmarkRequest = Bookmark.fetchRequest()
|
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")
|
Label("Export", systemImage: "square.and.arrow.up")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.tint(.primary)
|
.tint(.primary)
|
||||||
}
|
}
|
||||||
.onDelete { offsets in
|
.onDelete { offsets in
|
||||||
for index in offsets {
|
for index in offsets {
|
||||||
|
|
@ -48,7 +48,7 @@ struct BackupsView: View {
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.onAppear {
|
.onAppear {
|
||||||
backupManager.backupUrls = FileManager.default.appDirectory
|
backupManager.backupUrls = FileManager.default.appDirectory
|
||||||
.appendingPathComponent("Backups", isDirectory: true).contentsByDateAdded
|
.appendingPathComponent("Backups", isDirectory: true).contentsByDateAdded
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,46 +40,11 @@ struct DefaultActionPickerView: View {
|
||||||
|
|
||||||
// Handle custom here
|
// Handle custom here
|
||||||
ForEach(actions.filter { $0.requires.contains(actionRequirement.rawValue) }, id: \.id) { action in
|
ForEach(actions.filter { $0.requires.contains(actionRequirement.rawValue) }, id: \.id) { action in
|
||||||
Button {
|
CustomChoiceButton(
|
||||||
if let actionListId = action.listId?.uuidString {
|
action: action,
|
||||||
defaultAction = .custom(name: action.name, listId: actionListId)
|
defaultAction: $defaultAction,
|
||||||
} else {
|
associatedPluginList: pluginLists.first(where: { $0.id == action.listId })
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.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 {
|
private struct DefaultChoiceButton: View {
|
||||||
@Binding var defaultAction: DefaultAction
|
@Binding var defaultAction: DefaultAction
|
||||||
let selectedOption: DefaultAction
|
let selectedOption: DefaultAction
|
||||||
|
|
@ -107,7 +125,7 @@ private struct DefaultChoiceButton: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.tint(.primary)
|
.tint(.primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchButtonName() -> String {
|
func fetchButtonName() -> String {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct KodiEditorView: View {
|
struct KodiEditorView: View {
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
@EnvironmentObject var navModel: NavigationViewModel
|
@EnvironmentObject var navModel: NavigationViewModel
|
||||||
@EnvironmentObject var pluginManager: PluginManager
|
@EnvironmentObject var pluginManager: PluginManager
|
||||||
|
|
@ -56,7 +56,7 @@ struct KodiEditorView: View {
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.id(loadedSelectedServer)
|
.id(loadedSelectedServer)
|
||||||
}
|
}
|
||||||
.backport.onAppear {
|
.onAppear {
|
||||||
if let selectedKodiServer = navModel.selectedKodiServer {
|
if let selectedKodiServer = navModel.selectedKodiServer {
|
||||||
serverUrl = selectedKodiServer.urlString
|
serverUrl = selectedKodiServer.urlString
|
||||||
friendlyName = selectedKodiServer.name
|
friendlyName = selectedKodiServer.name
|
||||||
|
|
@ -66,17 +66,17 @@ struct KodiEditorView: View {
|
||||||
loadedSelectedServer.toggle()
|
loadedSelectedServer.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.alert(
|
.alert("Error", isPresented: $showErrorAlert) {
|
||||||
isPresented: $showErrorAlert,
|
Button("OK", role: .cancel) {}
|
||||||
title: "Error",
|
} message: {
|
||||||
message: errorAlertText
|
Text(errorAlertText)
|
||||||
)
|
}
|
||||||
.navigationTitle("Editing Kodi Server")
|
.navigationTitle("Editing Kodi Server")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
presentationMode.wrappedValue.dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,7 +91,7 @@ struct KodiEditorView: View {
|
||||||
existingServer: navModel.selectedKodiServer
|
existingServer: navModel.selectedKodiServer
|
||||||
)
|
)
|
||||||
|
|
||||||
presentationMode.wrappedValue.dismiss()
|
dismiss()
|
||||||
} catch {
|
} catch {
|
||||||
logManager.error("Editing Kodi server: \(error)", showToast: false)
|
logManager.error("Editing Kodi server: \(error)", showToast: false)
|
||||||
errorAlertText = error.localizedDescription
|
errorAlertText = error.localizedDescription
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ struct KodiServerView: View {
|
||||||
|
|
||||||
@State private var isActive = false
|
@State private var isActive = false
|
||||||
@State private var pingInProgress = false
|
@State private var pingInProgress = false
|
||||||
@State private var viewTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
@ -30,23 +29,18 @@ struct KodiServerView: View {
|
||||||
.frame(width: 10, height: 10)
|
.frame(width: 10, height: 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.onAppear {
|
.task {
|
||||||
viewTask = Task {
|
pingInProgress = true
|
||||||
pingInProgress = true
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await pluginManager.kodi.ping(server: server)
|
try await pluginManager.kodi.ping(server: server)
|
||||||
isActive = true
|
isActive = true
|
||||||
} catch {
|
} catch {
|
||||||
logManager.error("Kodi server \(server.name): \(error)", showToast: false)
|
logManager.error("Kodi server \(server.name): \(error)", showToast: false)
|
||||||
isActive = false
|
isActive = false
|
||||||
}
|
|
||||||
|
|
||||||
pingInProgress = false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.onDisappear {
|
pingInProgress = false
|
||||||
viewTask?.cancel()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,7 @@ struct SettingsKodiView: View {
|
||||||
|
|
||||||
@EnvironmentObject var navModel: NavigationViewModel
|
@EnvironmentObject var navModel: NavigationViewModel
|
||||||
|
|
||||||
// TODO: Change to var in v0.7
|
var kodiServers: FetchedResults<KodiServer>
|
||||||
@FetchRequest(
|
|
||||||
entity: KodiServer.entity(),
|
|
||||||
sortDescriptors: []
|
|
||||||
) var kodiServers: FetchedResults<KodiServer>
|
|
||||||
|
|
||||||
@State private var presentEditSheet = false
|
@State private var presentEditSheet = false
|
||||||
|
|
||||||
|
|
@ -48,20 +44,11 @@ struct SettingsKodiView: View {
|
||||||
Image(systemName: "pencil")
|
Image(systemName: "pencil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 15.0, *) {
|
Button(role: .destructive) {
|
||||||
Button(role: .destructive) {
|
PersistenceController.shared.delete(server, context: backgroundContext)
|
||||||
PersistenceController.shared.delete(server, context: backgroundContext)
|
} label: {
|
||||||
} label: {
|
Text("Remove")
|
||||||
Text("Remove")
|
Image(systemName: "trash")
|
||||||
Image(systemName: "trash")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button {
|
|
||||||
PersistenceController.shared.delete(server, context: backgroundContext)
|
|
||||||
} label: {
|
|
||||||
Text("Remove")
|
|
||||||
Image(systemName: "trash")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +65,6 @@ struct SettingsKodiView: View {
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.sheet(isPresented: $presentEditSheet) {
|
.sheet(isPresented: $presentEditSheet) {
|
||||||
KodiEditorView()
|
KodiEditorView()
|
||||||
.environmentObject(navModel)
|
|
||||||
}
|
}
|
||||||
.navigationTitle("Kodi")
|
.navigationTitle("Kodi")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PluginListEditorView: View {
|
struct PluginListEditorView: View {
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
@EnvironmentObject var navModel: NavigationViewModel
|
@EnvironmentObject var navModel: NavigationViewModel
|
||||||
@EnvironmentObject var pluginManager: PluginManager
|
@EnvironmentObject var pluginManager: PluginManager
|
||||||
|
|
@ -33,23 +33,23 @@ struct PluginListEditorView: View {
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.id(loadedSelectedList)
|
.id(loadedSelectedList)
|
||||||
}
|
}
|
||||||
.backport.onAppear {
|
.onAppear {
|
||||||
if let selectedList = navModel.selectedPluginList {
|
if let selectedList = navModel.selectedPluginList {
|
||||||
pluginListUrl = selectedList.urlString
|
pluginListUrl = selectedList.urlString
|
||||||
loadedSelectedList.toggle()
|
loadedSelectedList.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.alert(
|
.alert("Error", isPresented: $showUrlErrorAlert) {
|
||||||
isPresented: $showUrlErrorAlert,
|
Button("OK", role: .cancel) {}
|
||||||
title: "Error",
|
} message: {
|
||||||
message: urlErrorAlertText
|
Text(urlErrorAlertText)
|
||||||
)
|
}
|
||||||
.navigationTitle("Editing Plugin List")
|
.navigationTitle("Editing Plugin List")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
presentationMode.wrappedValue.dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +62,7 @@ struct PluginListEditorView: View {
|
||||||
existingPluginList: navModel.selectedPluginList
|
existingPluginList: navModel.selectedPluginList
|
||||||
)
|
)
|
||||||
|
|
||||||
presentationMode.wrappedValue.dismiss()
|
dismiss()
|
||||||
} catch {
|
} catch {
|
||||||
logManager.error("Editing plugin list: \(error)", showToast: false)
|
logManager.error("Editing plugin list: \(error)", showToast: false)
|
||||||
urlErrorAlertText = error.localizedDescription
|
urlErrorAlertText = error.localizedDescription
|
||||||
|
|
|
||||||
|
|
@ -48,20 +48,11 @@ struct SettingsPluginListView: View {
|
||||||
Image(systemName: "pencil")
|
Image(systemName: "pencil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 15.0, *) {
|
Button(role: .destructive) {
|
||||||
Button(role: .destructive) {
|
PersistenceController.shared.delete(pluginList, context: backgroundContext)
|
||||||
PersistenceController.shared.delete(pluginList, context: backgroundContext)
|
} label: {
|
||||||
} label: {
|
Text("Remove")
|
||||||
Text("Remove")
|
Image(systemName: "trash")
|
||||||
Image(systemName: "trash")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button {
|
|
||||||
PersistenceController.shared.delete(pluginList, context: backgroundContext)
|
|
||||||
} label: {
|
|
||||||
Text("Remove")
|
|
||||||
Image(systemName: "trash")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +74,6 @@ struct SettingsPluginListView: View {
|
||||||
.presentationDetents([.medium])
|
.presentationDetents([.medium])
|
||||||
} else {
|
} else {
|
||||||
PluginListEditorView()
|
PluginListEditorView()
|
||||||
.environmentObject(navModel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Plugin Lists")
|
.navigationTitle("Plugin Lists")
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import SwiftUI
|
||||||
struct SettingsAppVersionView: View {
|
struct SettingsAppVersionView: View {
|
||||||
@EnvironmentObject var logManager: LoggingManager
|
@EnvironmentObject var logManager: LoggingManager
|
||||||
|
|
||||||
@State private var viewTask: Task<Void, Never>?
|
|
||||||
@State private var releases: [Github.Release] = []
|
@State private var releases: [Github.Release] = []
|
||||||
|
|
||||||
@State private var loadedReleases = false
|
@State private var loadedReleases = false
|
||||||
|
|
@ -30,25 +29,20 @@ struct SettingsAppVersionView: View {
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.onAppear {
|
.task {
|
||||||
viewTask = Task {
|
do {
|
||||||
do {
|
if let fetchedReleases = try await Github().fetchReleases() {
|
||||||
if let fetchedReleases = try await Github().fetchReleases() {
|
releases = fetchedReleases
|
||||||
releases = fetchedReleases
|
} else {
|
||||||
} else {
|
logManager.error("Github: No releases found")
|
||||||
logManager.error("Github: No releases found")
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
logManager.error("Github: \(error)")
|
|
||||||
}
|
|
||||||
|
|
||||||
withAnimation {
|
|
||||||
loadedReleases = true
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
logManager.error("Github: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
withAnimation {
|
||||||
|
loadedReleases = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
viewTask?.cancel()
|
|
||||||
}
|
}
|
||||||
.navigationTitle("Version History")
|
.navigationTitle("Version History")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,11 @@ struct SettingsLogView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.backport.alert(
|
.alert("Success", isPresented: $logManager.showLogExportedAlert) {
|
||||||
isPresented: $logManager.showLogExportedAlert,
|
Button("OK", role: .cancel) {}
|
||||||
title: "Success",
|
} message: {
|
||||||
message: "Log successfully exported in Ferrite's logs folder"
|
Text("Log successfully exported in Ferrite's logs folder")
|
||||||
)
|
}
|
||||||
.navigationTitle("Logs")
|
.navigationTitle("Logs")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
|
@ -39,18 +39,10 @@ struct SettingsLogView: View {
|
||||||
Label("Export", systemImage: "square.and.arrow.up")
|
Label("Export", systemImage: "square.and.arrow.up")
|
||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 15, *) {
|
Button(role: .destructive) {
|
||||||
Button(role: .destructive) {
|
logManager.messageArray = []
|
||||||
logManager.messageArray = []
|
} label: {
|
||||||
} label: {
|
Label("Clear session logs", systemImage: "trash")
|
||||||
Label("Clear session logs", systemImage: "trash")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button {
|
|
||||||
logManager.messageArray = []
|
|
||||||
} label: {
|
|
||||||
Label("Clear session logs", systemImage: "trash")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "ellipsis")
|
Image(systemName: "ellipsis")
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftUIX
|
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||||
|
|
@ -16,133 +15,41 @@ struct ContentView: View {
|
||||||
@EnvironmentObject var logManager: LoggingManager
|
@EnvironmentObject var logManager: LoggingManager
|
||||||
|
|
||||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch: Bool = false
|
@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 searchText: String = ""
|
||||||
|
@State private var searchPrompt: String = "Search"
|
||||||
@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"
|
|
||||||
]
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavView {
|
NavView {
|
||||||
List {
|
List {
|
||||||
ForEach(scrapingModel.searchResults, id: \.self) { result in
|
SearchResultsView(searchText: $searchText, searchPrompt: $searchPrompt)
|
||||||
if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil {
|
|
||||||
SearchResultButtonView(result: result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 20 : -20)
|
.inlinedList(inset: 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 = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("Search")
|
.navigationTitle("Search")
|
||||||
.navigationSearchBar {
|
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: Text(searchPrompt))
|
||||||
SearchBar(
|
.onSubmit(of: .search) {
|
||||||
searchBarText,
|
if let runningSearchTask = scrapingModel.runningSearchTask, runningSearchTask.isCancelled {
|
||||||
text: $searchText,
|
scrapingModel.runningSearchTask = nil
|
||||||
isEditing: $isEditingSearch,
|
return
|
||||||
onCommit: {
|
}
|
||||||
if let runningSearchTask = scrapingModel.runningSearchTask, runningSearchTask.isCancelled {
|
|
||||||
scrapingModel.runningSearchTask = nil
|
scrapingModel.runningSearchTask = Task {
|
||||||
return
|
let sources = pluginManager.fetchInstalledSources()
|
||||||
}
|
await scrapingModel.scanSources(
|
||||||
|
sources: sources,
|
||||||
scrapingModel.runningSearchTask = Task {
|
searchText: searchText,
|
||||||
isSearching = true
|
debridManager: debridManager
|
||||||
|
)
|
||||||
let sources = pluginManager.fetchInstalledSources()
|
|
||||||
await scrapingModel.scanSources(
|
logManager.hideIndeterminateToast()
|
||||||
sources: sources,
|
|
||||||
searchText: searchText,
|
|
||||||
debridManager: debridManager
|
|
||||||
)
|
|
||||||
|
|
||||||
logManager.hideIndeterminateToast()
|
|
||||||
scrapingModel.runningSearchTask = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.showsCancelButton(isEditingSearch || isSearching)
|
|
||||||
.onCancel {
|
|
||||||
scrapingModel.searchResults = []
|
|
||||||
scrapingModel.runningSearchTask?.cancel()
|
|
||||||
scrapingModel.runningSearchTask = nil
|
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 {
|
.customScopeBar {
|
||||||
SearchFilterHeaderView()
|
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 SwiftUI
|
||||||
import SwiftUIX
|
|
||||||
|
|
||||||
struct LibraryView: View {
|
struct LibraryView: View {
|
||||||
@EnvironmentObject var debridManager: DebridManager
|
@EnvironmentObject var debridManager: DebridManager
|
||||||
@EnvironmentObject var navModel: NavigationViewModel
|
@EnvironmentObject var navModel: NavigationViewModel
|
||||||
|
|
||||||
@State private var bookmarksEmpty = false
|
@FetchRequest(
|
||||||
@State private var historyEmpty = false
|
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
|
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||||
|
|
||||||
@State private var editMode: EditMode = .inactive
|
@State private var editMode: EditMode = .inactive
|
||||||
|
|
||||||
@State private var searchText: String = ""
|
// Bound to the isSearching environment var
|
||||||
@State private var isEditingSearch = false
|
|
||||||
@State private var isSearching = false
|
@State private var isSearching = false
|
||||||
|
@State private var searchText: String = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavView {
|
NavView {
|
||||||
ZStack {
|
ZStack {
|
||||||
switch navModel.libraryPickerSelection {
|
switch navModel.libraryPickerSelection {
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
BookmarksView(searchText: $searchText, bookmarksEmpty: $bookmarksEmpty)
|
BookmarksView(searchText: $searchText, bookmarks: bookmarks)
|
||||||
case .history:
|
case .history:
|
||||||
HistoryView(searchText: $searchText, historyEmpty: $historyEmpty)
|
HistoryView(allHistoryEntries: allHistoryEntries, searchText: $searchText)
|
||||||
case .debridCloud:
|
case .debridCloud:
|
||||||
DebridCloudView(searchText: $searchText)
|
DebridCloudView(searchText: $searchText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.searchListener(isSearching: $isSearching)
|
||||||
.overlay {
|
.overlay {
|
||||||
switch navModel.libraryPickerSelection {
|
if !isSearching {
|
||||||
case .bookmarks:
|
switch navModel.libraryPickerSelection {
|
||||||
if bookmarksEmpty {
|
case .bookmarks:
|
||||||
EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results")
|
if bookmarks.isEmpty {
|
||||||
}
|
EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results")
|
||||||
case .history:
|
}
|
||||||
if historyEmpty {
|
case .history:
|
||||||
EmptyInstructionView(title: "No History", message: "Start watching to build history")
|
if allHistoryEntries.isEmpty {
|
||||||
}
|
EmptyInstructionView(title: "No History", message: "Start watching to build history")
|
||||||
case .debridCloud:
|
}
|
||||||
if debridManager.selectedDebridType == nil {
|
case .debridCloud:
|
||||||
EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service")
|
if debridManager.selectedDebridType == nil {
|
||||||
|
EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Library")
|
.navigationTitle("Library")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
HStack(spacing: Application.shared.osVersion.majorVersion > 14 ? 10 : 18) {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
EditButton()
|
EditButton()
|
||||||
|
|
||||||
|
|
@ -72,21 +81,9 @@ struct LibraryView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationSearchBar {
|
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
|
||||||
SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: {
|
|
||||||
isSearching = true
|
|
||||||
})
|
|
||||||
.showsCancelButton(isEditingSearch || isSearching)
|
|
||||||
.onCancel {
|
|
||||||
searchText = ""
|
|
||||||
isSearching = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationSearchBarHiddenWhenScrolling(false)
|
|
||||||
.customScopeBar {
|
.customScopeBar {
|
||||||
LibraryPickerView()
|
LibraryPickerView()
|
||||||
.environmentObject(debridManager)
|
|
||||||
.environmentObject(navModel)
|
|
||||||
}
|
}
|
||||||
.environment(\.editMode, $editMode)
|
.environment(\.editMode, $editMode)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LoginWebView: View {
|
struct LoginWebView: View {
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.dismiss) var dismiss
|
||||||
var url: URL
|
var url: URL
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
@ -19,7 +19,7 @@ struct LoginWebView: View {
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("Done") {
|
Button("Done") {
|
||||||
presentationMode.wrappedValue.dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftUIX
|
|
||||||
|
|
||||||
struct MainView: View {
|
struct MainView: View {
|
||||||
@EnvironmentObject var navModel: NavigationViewModel
|
@EnvironmentObject var navModel: NavigationViewModel
|
||||||
|
|
@ -21,7 +20,6 @@ struct MainView: View {
|
||||||
@State private var showUpdateAlert = false
|
@State private var showUpdateAlert = false
|
||||||
@State private var releaseVersionString: String = ""
|
@State private var releaseVersionString: String = ""
|
||||||
@State private var releaseUrlString: String = ""
|
@State private var releaseUrlString: String = ""
|
||||||
@State private var viewTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $navModel.selectedTab) {
|
TabView(selection: $navModel.selectedTab) {
|
||||||
|
|
@ -53,78 +51,67 @@ struct MainView: View {
|
||||||
switch item {
|
switch item {
|
||||||
case .action:
|
case .action:
|
||||||
ActionChoiceView()
|
ActionChoiceView()
|
||||||
.environmentObject(debridManager)
|
|
||||||
.environmentObject(scrapingModel)
|
|
||||||
.environmentObject(navModel)
|
|
||||||
.environmentObject(pluginManager)
|
|
||||||
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
|
|
||||||
case .batch:
|
case .batch:
|
||||||
BatchChoiceView()
|
BatchChoiceView()
|
||||||
.environmentObject(debridManager)
|
|
||||||
.environmentObject(scrapingModel)
|
|
||||||
.environmentObject(navModel)
|
|
||||||
case .activity:
|
case .activity:
|
||||||
|
EmptyView()
|
||||||
|
// TODO: Fix share sheet
|
||||||
if #available(iOS 16, *) {
|
if #available(iOS 16, *) {
|
||||||
AppActivityView(activityItems: navModel.activityItems)
|
ShareSheet(activityItems: navModel.activityItems)
|
||||||
.presentationDetents([.medium, .large])
|
.presentationDetents([.medium, .large])
|
||||||
} else {
|
} else {
|
||||||
AppActivityView(activityItems: navModel.activityItems)
|
ShareSheet(activityItems: navModel.activityItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.onAppear {
|
.onAppear {
|
||||||
|
logManager.info("Ferrite started")
|
||||||
|
}
|
||||||
|
.task {
|
||||||
if
|
if
|
||||||
autoUpdateNotifs,
|
autoUpdateNotifs,
|
||||||
Application.shared.osVersion.toString() >= Application.shared.minVersion
|
Application.shared.osVersion.toString() >= Application.shared.minVersion
|
||||||
{
|
{
|
||||||
// MARK: If scope bar duplication happens, this may be the problem
|
// 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")
|
do {
|
||||||
|
guard let latestRelease = try await Github().fetchLatestRelease() else {
|
||||||
viewTask = Task {
|
logManager.error(
|
||||||
// Sleep for 2 seconds to allow for view layout and app init
|
"Github: No releases found",
|
||||||
try? await Task.sleep(seconds: 2)
|
description: "Github error: No releases found"
|
||||||
|
)
|
||||||
do {
|
return
|
||||||
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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
.onOpenURL { url in
|
||||||
if url.scheme == "file" {
|
if url.scheme == "file" {
|
||||||
// Attempt to copy to backups directory if backup doesn't exist
|
// Attempt to copy to backups directory if backup doesn't exist
|
||||||
|
|
@ -134,54 +121,51 @@ struct MainView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Global alerts and dialogs for backups
|
// Global alerts and dialogs for backups
|
||||||
.backport.confirmationDialog(
|
.confirmationDialog(
|
||||||
|
"Restore backup?",
|
||||||
isPresented: $backupManager.showRestoreAlert,
|
isPresented: $backupManager.showRestoreAlert,
|
||||||
title: "Restore backup?",
|
titleVisibility: .visible
|
||||||
message:
|
) {
|
||||||
"Merge (preferred): Will merge your current data with the backup \n\n" +
|
Button("Merge", role: .destructive) {
|
||||||
"Overwrite: Will delete and replace all your data \n\n" +
|
Task {
|
||||||
"If Merge causes app instability, uninstall Ferrite and use the Overwrite option.",
|
await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: false)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
)
|
Button("Overwrite", role: .destructive) {
|
||||||
.backport.alert(
|
Task {
|
||||||
isPresented: $backupManager.showRestoreCompletedAlert,
|
await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: true)
|
||||||
title: "Backup restored",
|
|
||||||
message: backupManager.restoreCompletedMessage.joined(separator: " \n\n"),
|
|
||||||
buttons: [
|
|
||||||
.init("OK") {
|
|
||||||
backupManager.restoreCompletedMessage = []
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
)
|
} 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
|
// Updater alert
|
||||||
.backport.alert(
|
.alert("Update available", isPresented: $showUpdateAlert) {
|
||||||
isPresented: $showUpdateAlert,
|
Button("Download") {
|
||||||
title: "Update available",
|
guard let releaseUrl = URL(string: releaseUrlString) else {
|
||||||
message:
|
return
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
UIApplication.shared.open(releaseUrl)
|
UIApplication.shared.open(releaseUrl)
|
||||||
},
|
}
|
||||||
.init(role: .cancel)
|
Button("Cancel", role: .cancel) {}
|
||||||
]
|
} message: {
|
||||||
)
|
Text(
|
||||||
|
"Ferrite \(releaseVersionString) can be downloaded. \n\n" +
|
||||||
|
"This alert can be disabled in Settings."
|
||||||
|
)
|
||||||
|
}
|
||||||
.overlay {
|
.overlay {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
@ -198,9 +182,7 @@ struct MainView: View {
|
||||||
}
|
}
|
||||||
.padding(12)
|
.padding(12)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.background {
|
.background(.thinMaterial)
|
||||||
VisualEffectBlurView(blurStyle: .systemThinMaterial)
|
|
||||||
}
|
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,9 +204,7 @@ struct MainView: View {
|
||||||
}
|
}
|
||||||
.padding(12)
|
.padding(12)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.background {
|
.background(.thinMaterial)
|
||||||
VisualEffectBlurView(blurStyle: .systemThinMaterial)
|
|
||||||
}
|
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.frame(width: 200)
|
.frame(width: 200)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftUIX
|
|
||||||
|
|
||||||
struct PluginsView: View {
|
struct PluginsView: View {
|
||||||
@EnvironmentObject var pluginManager: PluginManager
|
@EnvironmentObject var pluginManager: PluginManager
|
||||||
|
|
@ -14,16 +13,22 @@ struct PluginsView: View {
|
||||||
|
|
||||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||||
|
|
||||||
@State private var installedSourcesEmpty = false
|
@FetchRequest(
|
||||||
@State private var installedActionsEmpty = false
|
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 checkedForPlugins = false
|
||||||
|
|
||||||
@State private var isEditingSearch = false
|
// Bound to the isSearching environment var
|
||||||
@State private var isSearching = false
|
@State private var isSearching = false
|
||||||
@State private var searchText: String = ""
|
@State private var searchText: String = ""
|
||||||
|
|
||||||
@State private var viewTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavView {
|
NavView {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
@ -31,58 +36,47 @@ struct PluginsView: View {
|
||||||
switch navModel.pluginPickerSelection {
|
switch navModel.pluginPickerSelection {
|
||||||
case .sources:
|
case .sources:
|
||||||
PluginAggregateView<Source, SourceJson>(
|
PluginAggregateView<Source, SourceJson>(
|
||||||
searchText: $searchText,
|
installedPlugins: installedSources,
|
||||||
pluginsEmpty: $installedSourcesEmpty
|
searchText: $searchText
|
||||||
)
|
)
|
||||||
case .actions:
|
case .actions:
|
||||||
PluginAggregateView<Action, ActionJson>(
|
PluginAggregateView<Action, ActionJson>(
|
||||||
searchText: $searchText,
|
installedPlugins: installedActions,
|
||||||
pluginsEmpty: $installedActionsEmpty
|
searchText: $searchText
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.searchListener(isSearching: $isSearching)
|
||||||
.overlay {
|
.overlay {
|
||||||
if checkedForPlugins {
|
if !isSearching {
|
||||||
switch navModel.pluginPickerSelection {
|
if checkedForPlugins {
|
||||||
case .sources:
|
switch navModel.pluginPickerSelection {
|
||||||
if installedSourcesEmpty, pluginManager.availableSources.isEmpty {
|
case .sources:
|
||||||
EmptyInstructionView(title: "No Sources", message: "Add a plugin list in Settings")
|
if installedSources.isEmpty, pluginManager.availableSources.isEmpty {
|
||||||
}
|
EmptyInstructionView(title: "No Sources", message: "Add a plugin list in Settings")
|
||||||
case .actions:
|
}
|
||||||
if installedActionsEmpty, pluginManager.availableActions.isEmpty {
|
case .actions:
|
||||||
EmptyInstructionView(title: "No Actions", message: "Add a plugin list in Settings")
|
if installedActions.isEmpty, pluginManager.availableActions.isEmpty {
|
||||||
|
EmptyInstructionView(title: "No Actions", message: "Add a plugin list in Settings")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ProgressView()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ProgressView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.onAppear {
|
.task {
|
||||||
viewTask = Task {
|
await pluginManager.fetchPluginsFromUrl()
|
||||||
await pluginManager.fetchPluginsFromUrl()
|
checkedForPlugins = true
|
||||||
checkedForPlugins = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
viewTask?.cancel()
|
|
||||||
checkedForPlugins = false
|
checkedForPlugins = false
|
||||||
}
|
}
|
||||||
.navigationTitle("Plugins")
|
.navigationTitle("Plugins")
|
||||||
.navigationSearchBar {
|
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
|
||||||
SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: {
|
|
||||||
isSearching = true
|
|
||||||
})
|
|
||||||
.showsCancelButton(isEditingSearch || isSearching)
|
|
||||||
.onCancel {
|
|
||||||
searchText = ""
|
|
||||||
isSearching = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationSearchBarHiddenWhenScrolling(false)
|
|
||||||
.customScopeBar {
|
.customScopeBar {
|
||||||
PluginPickerView()
|
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")) {
|
Section(header: InlineHeader("Playback services")) {
|
||||||
NavigationLink(destination: SettingsKodiView(), label: {
|
NavigationLink(destination: SettingsKodiView(kodiServers: kodiServers), label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Kodi")
|
Text("Kodi")
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,9 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftUIX
|
|
||||||
|
|
||||||
struct ActionChoiceView: View {
|
struct ActionChoiceView: View {
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||||
@EnvironmentObject var debridManager: DebridManager
|
@EnvironmentObject var debridManager: DebridManager
|
||||||
|
|
@ -32,7 +31,7 @@ struct ActionChoiceView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavView {
|
NavView {
|
||||||
Form {
|
Form {
|
||||||
Section(header: "Now Playing") {
|
Section(header: InlineHeader("Now Playing")) {
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text(navModel.selectedTitle)
|
Text(navModel.selectedTitle)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
|
|
@ -47,7 +46,7 @@ struct ActionChoiceView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !debridManager.downloadUrl.isEmpty {
|
if !debridManager.downloadUrl.isEmpty {
|
||||||
Section(header: "Debrid options") {
|
Section(header: InlineHeader("Debrid options")) {
|
||||||
ForEach(actions, id: \.id) { action in
|
ForEach(actions, id: \.id) { action in
|
||||||
if action.requires.contains(ActionRequirement.debrid.rawValue) {
|
if action.requires.contains(ActionRequirement.debrid.rawValue) {
|
||||||
ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") {
|
ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") {
|
||||||
|
|
@ -66,22 +65,21 @@ struct ActionChoiceView: View {
|
||||||
} label: {
|
} label: {
|
||||||
KodiServerView(server: server)
|
KodiServerView(server: server)
|
||||||
}
|
}
|
||||||
.backport.tint(.primary)
|
.tint(.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.tint(.secondary)
|
.tint(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") {
|
ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") {
|
||||||
UIPasteboard.general.string = debridManager.downloadUrl
|
UIPasteboard.general.string = debridManager.downloadUrl
|
||||||
showLinkCopyAlert.toggle()
|
showLinkCopyAlert.toggle()
|
||||||
}
|
}
|
||||||
.backport.alert(
|
.alert("Copied", isPresented: $showLinkCopyAlert) {
|
||||||
isPresented: $showLinkCopyAlert,
|
Button("OK", role: .cancel) {}
|
||||||
title: "Copied",
|
} message: {
|
||||||
message: "Download link copied successfully",
|
Text("Download link copied successfully")
|
||||||
buttons: [AlertButton("OK")]
|
}
|
||||||
)
|
|
||||||
|
|
||||||
ListRowButtonView("Share download URL", systemImage: "square.and.arrow.up.fill") {
|
ListRowButtonView("Share download URL", systemImage: "square.and.arrow.up.fill") {
|
||||||
if let url = URL(string: debridManager.downloadUrl) {
|
if let url = URL(string: debridManager.downloadUrl) {
|
||||||
|
|
@ -93,7 +91,7 @@ struct ActionChoiceView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !navModel.resultFromCloud {
|
if !navModel.resultFromCloud {
|
||||||
Section(header: "Magnet options") {
|
Section(header: InlineHeader("Magnet options")) {
|
||||||
ForEach(actions, id: \.id) { action in
|
ForEach(actions, id: \.id) { action in
|
||||||
if action.requires.contains(ActionRequirement.magnet.rawValue) {
|
if action.requires.contains(ActionRequirement.magnet.rawValue) {
|
||||||
ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") {
|
ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") {
|
||||||
|
|
@ -106,12 +104,11 @@ struct ActionChoiceView: View {
|
||||||
UIPasteboard.general.string = navModel.selectedMagnet?.link
|
UIPasteboard.general.string = navModel.selectedMagnet?.link
|
||||||
showMagnetCopyAlert.toggle()
|
showMagnetCopyAlert.toggle()
|
||||||
}
|
}
|
||||||
.backport.alert(
|
.alert("Copied", isPresented: $showMagnetCopyAlert) {
|
||||||
isPresented: $showMagnetCopyAlert,
|
Button("OK", role: .cancel) {}
|
||||||
title: "Copied",
|
} message: {
|
||||||
message: "Magnet link copied successfully",
|
Text("Magnet link copied successfully")
|
||||||
buttons: [AlertButton("OK")]
|
}
|
||||||
)
|
|
||||||
|
|
||||||
ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") {
|
ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") {
|
||||||
if let magnetLink = navModel.selectedMagnet?.link,
|
if let magnetLink = navModel.selectedMagnet?.link,
|
||||||
|
|
@ -124,25 +121,26 @@ struct ActionChoiceView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.tint(.primary)
|
.tint(.primary)
|
||||||
.sheet(isPresented: $navModel.showLocalActivitySheet) {
|
.sheet(isPresented: $navModel.showLocalActivitySheet) {
|
||||||
|
// TODO: Fix share sheet
|
||||||
if #available(iOS 16, *) {
|
if #available(iOS 16, *) {
|
||||||
AppActivityView(activityItems: navModel.activityItems)
|
ShareSheet(activityItems: navModel.activityItems)
|
||||||
.presentationDetents([.medium, .large])
|
.presentationDetents([.medium, .large])
|
||||||
} else {
|
} else {
|
||||||
AppActivityView(activityItems: navModel.activityItems)
|
ShareSheet(activityItems: navModel.activityItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.alert(
|
.alert("Action successful", isPresented: $pluginManager.showActionSuccessAlert) {
|
||||||
isPresented: $pluginManager.showActionSuccessAlert,
|
Button("OK", role: .cancel) {}
|
||||||
title: "Action successful",
|
} message: {
|
||||||
message: pluginManager.actionSuccessAlertMessage
|
Text(pluginManager.actionSuccessAlertMessage)
|
||||||
)
|
}
|
||||||
.backport.alert(
|
.alert("Action error", isPresented: $pluginManager.showActionErrorAlert) {
|
||||||
isPresented: $pluginManager.showActionErrorAlert,
|
Button("OK", role: .cancel) {}
|
||||||
title: "Action error",
|
} message: {
|
||||||
message: pluginManager.actionErrorAlertMessage
|
Text(pluginManager.actionErrorAlertMessage)
|
||||||
)
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
debridManager.downloadUrl = ""
|
debridManager.downloadUrl = ""
|
||||||
navModel.selectedTitle = ""
|
navModel.selectedTitle = ""
|
||||||
|
|
@ -158,7 +156,7 @@ struct ActionChoiceView: View {
|
||||||
navModel.selectedTitle = ""
|
navModel.selectedTitle = ""
|
||||||
navModel.selectedBatchTitle = ""
|
navModel.selectedBatchTitle = ""
|
||||||
|
|
||||||
presentationMode.wrappedValue.dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ struct BatchChoiceView: View {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.tint(.primary)
|
.tint(.primary)
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.inlinedList(inset: -20)
|
.inlinedList(inset: -20)
|
||||||
.navigationTitle("Select a file")
|
.navigationTitle("Select a file")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue