v0.6.1 #22
91 changed files with 3071 additions and 1629 deletions
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
|
|
@ -8,7 +8,7 @@ jobs:
|
|||
build:
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
cp -r build/Ferrite.xcarchive/Products/Applications/Ferrite.app Payload
|
||||
zip -r Ferrite-iOS_nightly-${{ env.sha_short }}.ipa Payload
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Ferrite-iOS_nightly-${{ env.sha_short }}.ipa
|
||||
path: Ferrite-iOS_nightly-${{ env.sha_short }}.ipa
|
||||
|
|
|
|||
36
.github/workflows/release.yml
vendored
Normal file
36
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
name: Build and upload release ipa
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: latest
|
||||
- name: Build
|
||||
run: xcodebuild -scheme Ferrite -configuration Release archive -archivePath build/Ferrite.xcarchive CODE_SIGN_IDENTITY= CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO
|
||||
env:
|
||||
IS_NIGHTLY: NO
|
||||
- name: Get app version
|
||||
run: |
|
||||
echo "app_version=$(/usr/libexec/plistbuddy -c Print:CFBundleShortVersionString: build/Ferrite.xcarchive/Products/Applications/Ferrite.app/Info.plist)" >> $GITHUB_ENV
|
||||
- name: Package ipa
|
||||
run: |
|
||||
mkdir Payload
|
||||
cp -r build/Ferrite.xcarchive/Products/Applications/Ferrite.app Payload
|
||||
zip -r Ferrite-iOS_v${{ env.app_version }}.ipa Payload
|
||||
- name: Create ipa zip
|
||||
run: |
|
||||
zip -j Ferrite-iOS_v${{ env.app_version }}.ipa.zip Ferrite-iOS_v${{ env.app_version }}.ipa
|
||||
- name: Upload release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: Ferrite-iOS_v${{ env.app_version }}.ipa.zip
|
||||
|
|
@ -8,35 +8,49 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0167DB29293FA900B65783 /* RealDebridModels.swift */; };
|
||||
0C03EB71296F619900162E9A /* PluginList+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03EB6F296F619900162E9A /* PluginList+CoreDataClass.swift */; };
|
||||
0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03EB70296F619900162E9A /* PluginList+CoreDataProperties.swift */; };
|
||||
0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C5293424A200ECA142 /* DebridLabelView.swift */; };
|
||||
0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */; };
|
||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; };
|
||||
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; };
|
||||
0C0D50E7288DFF850035ECC8 /* PluginListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* PluginListView.swift */; };
|
||||
0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; };
|
||||
0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; };
|
||||
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; };
|
||||
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; };
|
||||
0C2D9653299316CC00A504B6 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2D9652299316CC00A504B6 /* Tag.swift */; };
|
||||
0C2DD4CF29A6D47400293FC3 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 0C2DD4CE29A6D47400293FC3 /* SwiftUIX */; };
|
||||
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; };
|
||||
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; };
|
||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; };
|
||||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; };
|
||||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; };
|
||||
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */; };
|
||||
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E00CF296F4DB200ECECB2 /* ActionModels.swift */; };
|
||||
0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */; };
|
||||
0C3E00D8296F5B9A00ECECB2 /* PluginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */; };
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */; };
|
||||
0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */; };
|
||||
0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */; };
|
||||
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C422E7F293542F300486D65 /* PremiumizeModels.swift */; };
|
||||
0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */; };
|
||||
0C42B5962932F2D5008057A0 /* DebridPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5952932F2D5008057A0 /* DebridPickerView.swift */; };
|
||||
0C42B5982932F6DD008057A0 /* Set.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5972932F6DD008057A0 /* Set.swift */; };
|
||||
0C445C62293F9A0B0060744D /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C445C61293F9A0B0060744D /* Bundle.swift */; };
|
||||
0C448BE929A135F100F4E266 /* Introspect-Static in Frameworks */ = {isa = PBXBuildFile; productRef = 0C448BE829A135F100F4E266 /* Introspect-Static */; };
|
||||
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2A728D4DDDC007711AE /* Application.swift */; };
|
||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AC28D51C63007711AE /* BackupManager.swift */; };
|
||||
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AE28D52E8A007711AE /* BackupsView.swift */; };
|
||||
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; };
|
||||
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; };
|
||||
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */; };
|
||||
0C5005522992B6750064606A /* PluginTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5005512992B6750064606A /* PluginTagsView.swift */; };
|
||||
0C50055A2992BA6A0064606A /* PluginTag+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5005582992BA6A0064606A /* PluginTag+CoreDataClass.swift */; };
|
||||
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */; };
|
||||
0C50B7D0299DF63C00A9FA3C /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C50B7CF299DF63C00A9FA3C /* UIDevice.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 */; };
|
||||
0C572D4C2993FC2A003EEC05 /* ViewDidAppearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */; };
|
||||
0C572D4E299403B7003EEC05 /* ViewDidAppear.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */; };
|
||||
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; };
|
||||
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; };
|
||||
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
|
||||
|
|
@ -48,14 +62,12 @@
|
|||
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */; };
|
||||
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */; };
|
||||
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; };
|
||||
0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7376EF28A97D1400D60918 /* SwiftUIX */; };
|
||||
0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */; };
|
||||
0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */; };
|
||||
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */; };
|
||||
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C78041C28BFB3EA001E8CA3 /* String.swift */; };
|
||||
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */; };
|
||||
0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */; };
|
||||
0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */; };
|
||||
0C794B69289DACC800DD1CC8 /* InstalledPluginButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledPluginButtonView.swift */; };
|
||||
0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* PluginCatalogButtonView.swift */; };
|
||||
0C794B6D289EFA2E00DD1CC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */; };
|
||||
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */; };
|
||||
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */; };
|
||||
|
|
@ -68,16 +80,14 @@
|
|||
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */; };
|
||||
0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */; };
|
||||
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */; };
|
||||
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */; };
|
||||
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */; };
|
||||
0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */; };
|
||||
0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */; };
|
||||
0CA05457288EE58200850554 /* SettingsSourceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsSourceListView.swift */; };
|
||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05458288EE9E600850554 /* SourceManager.swift */; };
|
||||
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */; };
|
||||
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C871BDE29994D9D005279AC /* FilterLabelView.swift */; };
|
||||
0C95D8D828A55B03005E22B3 /* DefaultActionPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */; };
|
||||
0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsPluginListView.swift */; };
|
||||
0CA05459288EE9E600850554 /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05458288EE9E600850554 /* PluginManager.swift */; };
|
||||
0CA0545B288EEA4E00850554 /* PluginListEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */; };
|
||||
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BB288903F000DE2211 /* SettingsView.swift */; };
|
||||
0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BC288903F000DE2211 /* LoginWebView.swift */; };
|
||||
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */; };
|
||||
0CA148D8288903F000DE2211 /* ActionChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BD288903F000DE2211 /* ActionChoiceView.swift */; };
|
||||
0CA148DB288903F000DE2211 /* NavView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C1288903F000DE2211 /* NavView.swift */; };
|
||||
0CA148DC288903F000DE2211 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CA148C2288903F000DE2211 /* Assets.xcassets */; };
|
||||
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */; };
|
||||
|
|
@ -91,7 +101,6 @@
|
|||
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148CF288903F000DE2211 /* ToastViewModel.swift */; };
|
||||
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */; };
|
||||
0CA148E9288903F000DE2211 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D1288903F000DE2211 /* MainView.swift */; };
|
||||
0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D3288903F000DE2211 /* SearchResultsView.swift */; };
|
||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D4288903F000DE2211 /* ContentView.swift */; };
|
||||
0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23328C2658700616D3A /* LibraryView.swift */; };
|
||||
0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23628C2660700616D3A /* HistoryView.swift */; };
|
||||
|
|
@ -104,41 +113,54 @@
|
|||
0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAF9318296399190050812A /* PremiumizeCloudView.swift */; };
|
||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; };
|
||||
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; };
|
||||
0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 0CB6516728C5A5EC00DCA721 /* Introspect */; };
|
||||
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; };
|
||||
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */; };
|
||||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
|
||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
|
||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; };
|
||||
0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389512970AD900066D06F /* Action+CoreDataClass.swift */; };
|
||||
0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */; };
|
||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; };
|
||||
0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */; };
|
||||
0CD5F1FB299BEFBE00476DDB /* CustomScopeBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */; };
|
||||
0CD5F1FD299C083B00476DDB /* PluginPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */; };
|
||||
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD72E16293D9928001A7EA4 /* Array.swift */; };
|
||||
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; };
|
||||
0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */ = {isa = PBXBuildFile; productRef = 0CDDDE042935235E006810B1 /* BetterSafariView */; };
|
||||
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1C4172981E8D700418F20 /* Plugin.swift */; };
|
||||
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE66B3928E640D200F69346 /* Backport.swift */; };
|
||||
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */; };
|
||||
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0C0167DB29293FA900B65783 /* RealDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = "<group>"; };
|
||||
0C03EB6F296F619900162E9A /* PluginList+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginList+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C03EB70296F619900162E9A /* PluginList+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginList+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C0755C5293424A200ECA142 /* DebridLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridLabelView.swift; sourceTree = "<group>"; };
|
||||
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridManagerModels.swift; sourceTree = "<group>"; };
|
||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = "<group>"; };
|
||||
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = "<group>"; };
|
||||
0C0D50E6288DFF850035ECC8 /* PluginListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListView.swift; sourceTree = "<group>"; };
|
||||
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = "<group>"; };
|
||||
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = "<group>"; };
|
||||
0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = "<group>"; };
|
||||
0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridCloudView.swift; sourceTree = "<group>"; };
|
||||
0C2D9652299316CC00A504B6 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.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>"; };
|
||||
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>"; };
|
||||
0C3E00CF296F4DB200ECECB2 /* ActionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionModels.swift; sourceTree = "<group>"; };
|
||||
0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginsView.swift; sourceTree = "<group>"; };
|
||||
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginModels.swift; sourceTree = "<group>"; };
|
||||
0C3E00D9296F5E4F00ECECB2 /* FerriteDB_v2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB_v2.xcdatamodel; sourceTree = "<group>"; };
|
||||
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultButtonView.swift; sourceTree = "<group>"; };
|
||||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.swift; sourceTree = "<group>"; };
|
||||
0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeWrapper.swift; sourceTree = "<group>"; };
|
||||
0C422E7F293542F300486D65 /* PremiumizeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeModels.swift; sourceTree = "<group>"; };
|
||||
0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridChoiceView.swift; sourceTree = "<group>"; };
|
||||
0C42B5952932F2D5008057A0 /* DebridPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridPickerView.swift; sourceTree = "<group>"; };
|
||||
0C42B5972932F6DD008057A0 /* Set.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Set.swift; sourceTree = "<group>"; };
|
||||
0C445C61293F9A0B0060744D /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
|
||||
0C44E2A728D4DDDC007711AE /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -146,8 +168,14 @@
|
|||
0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.swift; sourceTree = "<group>"; };
|
||||
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C5005512992B6750064606A /* PluginTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginTagsView.swift; sourceTree = "<group>"; };
|
||||
0C5005582992BA6A0064606A /* PluginTag+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginTag+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginTag+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C50B7CF299DF63C00A9FA3C /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.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>"; };
|
||||
0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppearHandler.swift; sourceTree = "<group>"; };
|
||||
0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppear.swift; sourceTree = "<group>"; };
|
||||
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; };
|
||||
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = "<group>"; };
|
||||
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -160,9 +188,8 @@
|
|||
0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C78041C28BFB3EA001E8CA3 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
|
||||
0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceUpdateButtonView.swift; sourceTree = "<group>"; };
|
||||
0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledSourceButtonView.swift; sourceTree = "<group>"; };
|
||||
0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCatalogButtonView.swift; sourceTree = "<group>"; };
|
||||
0C794B68289DACC800DD1CC8 /* InstalledPluginButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledPluginButtonView.swift; sourceTree = "<group>"; };
|
||||
0C794B6A289DACF100DD1CC8 /* PluginCatalogButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginCatalogButtonView.swift; sourceTree = "<group>"; };
|
||||
0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -175,16 +202,14 @@
|
|||
0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceList+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceList+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultActionsPickerViews.swift; sourceTree = "<group>"; };
|
||||
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = "<group>"; };
|
||||
0CA05456288EE58200850554 /* SettingsSourceListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSourceListView.swift; sourceTree = "<group>"; };
|
||||
0CA05458288EE9E600850554 /* SourceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceManager.swift; sourceTree = "<group>"; };
|
||||
0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListEditorView.swift; sourceTree = "<group>"; };
|
||||
0C871BDE29994D9D005279AC /* FilterLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterLabelView.swift; sourceTree = "<group>"; };
|
||||
0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultActionPickerView.swift; sourceTree = "<group>"; };
|
||||
0CA05456288EE58200850554 /* SettingsPluginListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPluginListView.swift; sourceTree = "<group>"; };
|
||||
0CA05458288EE9E600850554 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = "<group>"; };
|
||||
0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListEditorView.swift; sourceTree = "<group>"; };
|
||||
0CA148BB288903F000DE2211 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
0CA148BC288903F000DE2211 /* LoginWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginWebView.swift; sourceTree = "<group>"; };
|
||||
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MagnetChoiceView.swift; sourceTree = "<group>"; };
|
||||
0CA148BD288903F000DE2211 /* ActionChoiceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionChoiceView.swift; sourceTree = "<group>"; };
|
||||
0CA148C1288903F000DE2211 /* NavView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavView.swift; sourceTree = "<group>"; };
|
||||
0CA148C2288903F000DE2211 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapingViewModel.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -198,7 +223,6 @@
|
|||
0CA148CF288903F000DE2211 /* ToastViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = "<group>"; };
|
||||
0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealDebridWrapper.swift; sourceTree = "<group>"; };
|
||||
0CA148D1288903F000DE2211 /* MainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
|
||||
0CA148D3288903F000DE2211 /* SearchResultsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
|
||||
0CA148D4288903F000DE2211 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
0CA3B23328C2658700616D3A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
|
||||
0CA3B23628C2660700616D3A /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -216,12 +240,19 @@
|
|||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
|
||||
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
|
||||
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||
0CC389512970AD900066D06F /* Action+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = "<group>"; };
|
||||
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = "<group>"; };
|
||||
0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScopeBar.swift; sourceTree = "<group>"; };
|
||||
0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginPickerView.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>"; };
|
||||
0CE1C4172981E8D700418F20 /* Plugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plugin.swift; sourceTree = "<group>"; };
|
||||
0CE66B3928E640D200F69346 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||
0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFilterHeaderView.swift; sourceTree = "<group>"; };
|
||||
0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryPickerView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -230,10 +261,10 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */,
|
||||
0C448BE929A135F100F4E266 /* Introspect-Static in Frameworks */,
|
||||
0C64A4B4288903680079976D /* Base32 in Frameworks */,
|
||||
0C2DD4CF29A6D47400293FC3 /* SwiftUIX in Frameworks */,
|
||||
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */,
|
||||
0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */,
|
||||
0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */,
|
||||
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */,
|
||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */,
|
||||
0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */,
|
||||
|
|
@ -246,7 +277,7 @@
|
|||
0C0755C22934241F00ECA142 /* SheetViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
|
||||
0CA148BD288903F000DE2211 /* ActionChoiceView.swift */,
|
||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
|
||||
);
|
||||
path = SheetViews;
|
||||
|
|
@ -255,11 +286,11 @@
|
|||
0C0755C32934244500ECA142 /* ComponentViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C3E00D4296F560800ECECB2 /* Plugin */,
|
||||
0C0755C42934245800ECA142 /* Debrid */,
|
||||
0CA3B23528C265FD00616D3A /* Library */,
|
||||
0C44E2AB28D4E126007711AE /* SearchResult */,
|
||||
0CA0545C288F7CB200850554 /* Settings */,
|
||||
0C794B65289DAC9F00DD1CC8 /* Source */,
|
||||
);
|
||||
path = ComponentViews;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -267,7 +298,7 @@
|
|||
0C0755C42934245800ECA142 /* Debrid */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */,
|
||||
0C42B5952932F2D5008057A0 /* DebridPickerView.swift */,
|
||||
0C0755C5293424A200ECA142 /* DebridLabelView.swift */,
|
||||
);
|
||||
path = Debrid;
|
||||
|
|
@ -276,6 +307,12 @@
|
|||
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C5005582992BA6A0064606A /* PluginTag+CoreDataClass.swift */,
|
||||
0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */,
|
||||
0CC389512970AD900066D06F /* Action+CoreDataClass.swift */,
|
||||
0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */,
|
||||
0C03EB6F296F619900162E9A /* PluginList+CoreDataClass.swift */,
|
||||
0C03EB70296F619900162E9A /* PluginList+CoreDataProperties.swift */,
|
||||
0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */,
|
||||
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */,
|
||||
0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */,
|
||||
|
|
@ -292,8 +329,6 @@
|
|||
0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */,
|
||||
0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */,
|
||||
0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */,
|
||||
0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */,
|
||||
0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */,
|
||||
);
|
||||
path = Classes;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -301,6 +336,7 @@
|
|||
0C0D50E3288DFE6E0035ECC8 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C3E00CF296F4DB200ECECB2 /* ActionModels.swift */,
|
||||
0C6C7C9C29315292002DF910 /* AllDebridModels.swift */,
|
||||
0C7ED14028D61BBA009E29AD /* BackupModels.swift */,
|
||||
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */,
|
||||
|
|
@ -308,8 +344,8 @@
|
|||
0C422E7F293542F300486D65 /* PremiumizeModels.swift */,
|
||||
0C0167DB29293FA900B65783 /* RealDebridModels.swift */,
|
||||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
|
||||
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */,
|
||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
|
||||
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -324,6 +360,18 @@
|
|||
path = Cloud;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C3E00D4296F560800ECECB2 /* Plugin */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AA28D4E09B007711AE /* Buttons */,
|
||||
0C794B65289DAC9F00DD1CC8 /* Source */,
|
||||
0C0D50E6288DFF850035ECC8 /* PluginListView.swift */,
|
||||
0C5005512992B6750064606A /* PluginTagsView.swift */,
|
||||
0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */,
|
||||
);
|
||||
path = Plugin;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C44E2A628D4DDC6007711AE /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -340,6 +388,8 @@
|
|||
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */,
|
||||
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */,
|
||||
0CB6516428C5A5D700DCA721 /* InlinedList.swift */,
|
||||
0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */,
|
||||
0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */,
|
||||
);
|
||||
path = Modifiers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -347,9 +397,8 @@
|
|||
0C44E2AA28D4E09B007711AE /* Buttons */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */,
|
||||
0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */,
|
||||
0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */,
|
||||
0C794B68289DACC800DD1CC8 /* InstalledPluginButtonView.swift */,
|
||||
0C794B6A289DACF100DD1CC8 /* PluginCatalogButtonView.swift */,
|
||||
);
|
||||
path = Buttons;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -359,14 +408,22 @@
|
|||
children = (
|
||||
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */,
|
||||
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */,
|
||||
0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */,
|
||||
);
|
||||
path = SearchResult;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C5005552992B9C20064606A /* Protocols */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CE1C4172981E8D700418F20 /* Plugin.swift */,
|
||||
);
|
||||
path = Protocols;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C794B65289DAC9F00DD1CC8 /* Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AA28D4E09B007711AE /* Buttons */,
|
||||
0C733286289C4C820058D1FE /* SourceSettingsView.swift */,
|
||||
);
|
||||
path = Source;
|
||||
|
|
@ -376,9 +433,9 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AE28D52E8A007711AE /* BackupsView.swift */,
|
||||
0CA05456288EE58200850554 /* SettingsSourceListView.swift */,
|
||||
0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */,
|
||||
0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */,
|
||||
0CA05456288EE58200850554 /* SettingsPluginListView.swift */,
|
||||
0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */,
|
||||
0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */,
|
||||
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
|
|
@ -394,6 +451,7 @@
|
|||
0CA148EF2889061600DE2211 /* ViewModels */,
|
||||
0CA148EE2889061200DE2211 /* Views */,
|
||||
0C44E2A628D4DDC6007711AE /* Classes */,
|
||||
0C5005552992B9C20064606A /* Protocols */,
|
||||
0CA148C8288903F000DE2211 /* Extensions */,
|
||||
0CA148C5288903F000DE2211 /* Preview Content */,
|
||||
0CA148C7288903F000DE2211 /* FerriteApp.swift */,
|
||||
|
|
@ -411,10 +469,12 @@
|
|||
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */,
|
||||
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */,
|
||||
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */,
|
||||
0C871BDE29994D9D005279AC /* FilterLabelView.swift */,
|
||||
0CA148C1288903F000DE2211 /* NavView.swift */,
|
||||
0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */,
|
||||
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */,
|
||||
0C32FB562890D1F2002BD219 /* ListRowViews.swift */,
|
||||
0C2D9652299316CC00A504B6 /* Tag.swift */,
|
||||
);
|
||||
path = CommonViews;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -442,6 +502,7 @@
|
|||
0C7ED14228D65518009E29AD /* FileManager.swift */,
|
||||
0C42B5972932F6DD008057A0 /* Set.swift */,
|
||||
0C7C128528DAA3CD00381CD1 /* URL.swift */,
|
||||
0C50B7CF299DF63C00A9FA3C /* UIDevice.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -455,9 +516,8 @@
|
|||
0C0755C22934241F00ECA142 /* SheetViews */,
|
||||
0CA148D1288903F000DE2211 /* MainView.swift */,
|
||||
0CA148D4288903F000DE2211 /* ContentView.swift */,
|
||||
0CA148D3288903F000DE2211 /* SearchResultsView.swift */,
|
||||
0CA3B23328C2658700616D3A /* LibraryView.swift */,
|
||||
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */,
|
||||
0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */,
|
||||
0CA148BB288903F000DE2211 /* SettingsView.swift */,
|
||||
0C32FB522890D19D002BD219 /* AboutView.swift */,
|
||||
0CA148BC288903F000DE2211 /* LoginWebView.swift */,
|
||||
|
|
@ -472,7 +532,7 @@
|
|||
0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */,
|
||||
0CA148CF288903F000DE2211 /* ToastViewModel.swift */,
|
||||
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */,
|
||||
0CA05458288EE9E600850554 /* SourceManager.swift */,
|
||||
0CA05458288EE9E600850554 /* PluginManager.swift */,
|
||||
0C44E2AC28D51C63007711AE /* BackupManager.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
|
|
@ -482,6 +542,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA148CE288903F000DE2211 /* WebView.swift */,
|
||||
0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */,
|
||||
);
|
||||
path = RepresentableViews;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -506,6 +567,7 @@
|
|||
0CA3B23628C2660700616D3A /* HistoryView.swift */,
|
||||
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */,
|
||||
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */,
|
||||
0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */,
|
||||
);
|
||||
path = Library;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -558,10 +620,10 @@
|
|||
0C64A4B3288903680079976D /* Base32 */,
|
||||
0C64A4B6288903880079976D /* KeychainSwift */,
|
||||
0C4CFC452897030D00AD9FAD /* Regex */,
|
||||
0C7376EF28A97D1400D60918 /* SwiftUIX */,
|
||||
0C7506D628B1AC9A008BEE38 /* SwiftyJSON */,
|
||||
0CB6516728C5A5EC00DCA721 /* Introspect */,
|
||||
0CDDDE042935235E006810B1 /* BetterSafariView */,
|
||||
0C448BE829A135F100F4E266 /* Introspect-Static */,
|
||||
0C2DD4CE29A6D47400293FC3 /* SwiftUIX */,
|
||||
);
|
||||
productName = Torrenter;
|
||||
productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */;
|
||||
|
|
@ -596,10 +658,10 @@
|
|||
0C64A4B2288903680079976D /* XCRemoteSwiftPackageReference "Base32" */,
|
||||
0C64A4B5288903880079976D /* XCRemoteSwiftPackageReference "keychain-swift" */,
|
||||
0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */,
|
||||
0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */,
|
||||
0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
|
||||
0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||
0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */,
|
||||
0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||
0C2DD4CD29A6D47400293FC3 /* XCRemoteSwiftPackageReference "SwiftUIX" */,
|
||||
);
|
||||
productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -652,46 +714,57 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0C7ED14328D65518009E29AD /* FileManager.swift in Sources */,
|
||||
0C03EB71296F619900162E9A /* PluginList+CoreDataClass.swift in Sources */,
|
||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */,
|
||||
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */,
|
||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
|
||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */,
|
||||
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */,
|
||||
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
|
||||
0C2D9653299316CC00A504B6 /* Tag.swift in Sources */,
|
||||
0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */,
|
||||
0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
|
||||
0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */,
|
||||
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
|
||||
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */,
|
||||
0C50055A2992BA6A0064606A /* PluginTag+CoreDataClass.swift in Sources */,
|
||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
|
||||
0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */,
|
||||
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */,
|
||||
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */,
|
||||
0CA0545B288EEA4E00850554 /* PluginListEditorView.swift in Sources */,
|
||||
0C3E00D8296F5B9A00ECECB2 /* PluginModels.swift in Sources */,
|
||||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */,
|
||||
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */,
|
||||
0C5005522992B6750064606A /* PluginTagsView.swift in Sources */,
|
||||
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */,
|
||||
0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */,
|
||||
0C42B5962932F2D5008057A0 /* DebridPickerView.swift in Sources */,
|
||||
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */,
|
||||
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */,
|
||||
0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */,
|
||||
0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */,
|
||||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */,
|
||||
0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
|
||||
0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */,
|
||||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
|
||||
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */,
|
||||
0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */,
|
||||
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */,
|
||||
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */,
|
||||
0C50B7D0299DF63C00A9FA3C /* UIDevice.swift in Sources */,
|
||||
0C0D50E7288DFF850035ECC8 /* PluginListView.swift in Sources */,
|
||||
0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */,
|
||||
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */,
|
||||
0CD5F1FB299BEFBE00476DDB /* CustomScopeBar.swift in Sources */,
|
||||
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */,
|
||||
0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */,
|
||||
0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */,
|
||||
0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */,
|
||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
||||
0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */,
|
||||
0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */,
|
||||
0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */,
|
||||
0C572D4C2993FC2A003EEC05 /* ViewDidAppearHandler.swift in Sources */,
|
||||
0C95D8D828A55B03005E22B3 /* DefaultActionPickerView.swift in Sources */,
|
||||
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */,
|
||||
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
||||
0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */,
|
||||
0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */,
|
||||
0C794B69289DACC800DD1CC8 /* InstalledPluginButtonView.swift in Sources */,
|
||||
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */,
|
||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */,
|
||||
0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */,
|
||||
|
|
@ -699,11 +772,9 @@
|
|||
0C445C62293F9A0B0060744D /* Bundle.swift in Sources */,
|
||||
0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */,
|
||||
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */,
|
||||
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */,
|
||||
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */,
|
||||
0CA148D8288903F000DE2211 /* ActionChoiceView.swift in Sources */,
|
||||
0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */,
|
||||
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */,
|
||||
0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */,
|
||||
0CA148E3288903F000DE2211 /* Task.swift in Sources */,
|
||||
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */,
|
||||
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */,
|
||||
|
|
@ -712,7 +783,6 @@
|
|||
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */,
|
||||
0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */,
|
||||
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
|
||||
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */,
|
||||
0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
|
||||
0CA3B23D28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift in Sources */,
|
||||
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */,
|
||||
|
|
@ -722,31 +792,38 @@
|
|||
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */,
|
||||
0C42B5982932F6DD008057A0 /* Set.swift in Sources */,
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */,
|
||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
|
||||
0CA05459288EE9E600850554 /* PluginManager.swift in Sources */,
|
||||
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
|
||||
0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */,
|
||||
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */,
|
||||
0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */,
|
||||
0CA05457288EE58200850554 /* SettingsSourceListView.swift in Sources */,
|
||||
0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */,
|
||||
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */,
|
||||
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */,
|
||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
||||
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */,
|
||||
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */,
|
||||
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */,
|
||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
||||
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */,
|
||||
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */,
|
||||
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
|
||||
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
|
||||
0C572D4E299403B7003EEC05 /* ViewDidAppear.swift in Sources */,
|
||||
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
|
||||
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */,
|
||||
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */,
|
||||
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */,
|
||||
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */,
|
||||
0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */,
|
||||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */,
|
||||
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */,
|
||||
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */,
|
||||
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */,
|
||||
0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */,
|
||||
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */,
|
||||
0CD5F1FD299C083B00476DDB /* PluginPickerView.swift in Sources */,
|
||||
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */,
|
||||
0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */,
|
||||
0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */,
|
||||
0CA148E0288903F000DE2211 /* FerriteApp.swift in Sources */,
|
||||
);
|
||||
|
|
@ -894,7 +971,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.6.0;
|
||||
MARKETING_VERSION = 0.6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
|
@ -929,7 +1006,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.6.0;
|
||||
MARKETING_VERSION = 0.6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
|
@ -963,6 +1040,22 @@
|
|||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
0C2DD4CD29A6D47400293FC3 /* XCRemoteSwiftPackageReference "SwiftUIX" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SwiftUIX/SwiftUIX";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.1.3;
|
||||
};
|
||||
};
|
||||
0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect/";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.2.2;
|
||||
};
|
||||
};
|
||||
0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/sindresorhus/Regex";
|
||||
|
|
@ -987,14 +1080,6 @@
|
|||
kind = branch;
|
||||
};
|
||||
};
|
||||
0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SwiftUIX/SwiftUIX";
|
||||
requirement = {
|
||||
branch = master;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON";
|
||||
|
|
@ -1011,14 +1096,6 @@
|
|||
minimumVersion = 2.0.0;
|
||||
};
|
||||
};
|
||||
0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect";
|
||||
requirement = {
|
||||
branch = master;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/stleamist/BetterSafariView";
|
||||
|
|
@ -1030,6 +1107,16 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
0C2DD4CE29A6D47400293FC3 /* SwiftUIX */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 0C2DD4CD29A6D47400293FC3 /* XCRemoteSwiftPackageReference "SwiftUIX" */;
|
||||
productName = SwiftUIX;
|
||||
};
|
||||
0C448BE829A135F100F4E266 /* Introspect-Static */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
|
||||
productName = "Introspect-Static";
|
||||
};
|
||||
0C4CFC452897030D00AD9FAD /* Regex */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */;
|
||||
|
|
@ -1045,11 +1132,6 @@
|
|||
package = 0C64A4B5288903880079976D /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
||||
productName = KeychainSwift;
|
||||
};
|
||||
0C7376EF28A97D1400D60918 /* SwiftUIX */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */;
|
||||
productName = SwiftUIX;
|
||||
};
|
||||
0C7506D628B1AC9A008BEE38 /* SwiftyJSON */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */;
|
||||
|
|
@ -1060,11 +1142,6 @@
|
|||
package = 0CAF1C79286F5C8600296F86 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||
productName = SwiftSoup;
|
||||
};
|
||||
0CB6516728C5A5EC00DCA721 /* Introspect */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
|
||||
productName = Introspect;
|
||||
};
|
||||
0CDDDE042935235E006810B1 /* BetterSafariView */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */;
|
||||
|
|
@ -1076,9 +1153,10 @@
|
|||
0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
0C3E00D9296F5E4F00ECECB2 /* FerriteDB_v2.xcdatamodel */,
|
||||
0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */,
|
||||
);
|
||||
currentVersion = 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */;
|
||||
currentVersion = 0C3E00D9296F5E4F00ECECB2 /* FerriteDB_v2.xcdatamodel */;
|
||||
path = FerriteDB.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,16 @@ public class RealDebrid {
|
|||
|
||||
var authTask: Task<Void, Error>?
|
||||
|
||||
@MainActor
|
||||
func setUserDefaultsValue(_ value: Any, forKey: String) {
|
||||
UserDefaults.standard.set(value, forKey: forKey)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func removeUserDefaultsValue(forKey: String) {
|
||||
UserDefaults.standard.removeObject(forKey: forKey)
|
||||
}
|
||||
|
||||
// Fetches the device code from RD
|
||||
public func getVerificationInfo() async throws -> DeviceCodeResponse {
|
||||
var urlComponents = URLComponents(string: "\(baseAuthUrl)/device/code")!
|
||||
|
|
@ -72,7 +82,7 @@ public class RealDebrid {
|
|||
|
||||
// If there's a client ID from the response, end the task successfully
|
||||
if let clientId = rawResponse?.clientID, let clientSecret = rawResponse?.clientSecret {
|
||||
UserDefaults.standard.set(clientId, forKey: "RealDebrid.ClientId")
|
||||
await setUserDefaultsValue(clientId, forKey: "RealDebrid.ClientId")
|
||||
keychain.set(clientSecret, forKey: "RealDebrid.ClientSecret")
|
||||
|
||||
try await getTokens(deviceCode: deviceCode)
|
||||
|
|
@ -124,7 +134,7 @@ public class RealDebrid {
|
|||
keychain.set(rawResponse.refreshToken, forKey: "RealDebrid.RefreshToken")
|
||||
|
||||
let accessTimestamp = Date().timeIntervalSince1970 + Double(rawResponse.expiresIn)
|
||||
UserDefaults.standard.set(accessTimestamp, forKey: "RealDebrid.AccessTokenStamp")
|
||||
await setUserDefaultsValue(accessTimestamp, forKey: "RealDebrid.AccessTokenStamp")
|
||||
}
|
||||
|
||||
public func fetchToken() async -> String? {
|
||||
|
|
@ -147,8 +157,8 @@ public class RealDebrid {
|
|||
public func deleteTokens() async throws {
|
||||
keychain.delete("RealDebrid.RefreshToken")
|
||||
keychain.delete("RealDebrid.ClientSecret")
|
||||
UserDefaults.standard.removeObject(forKey: "RealDebrid.ClientId")
|
||||
UserDefaults.standard.removeObject(forKey: "RealDebrid.AccessTokenStamp")
|
||||
await removeUserDefaultsValue(forKey: "RealDebrid.ClientId")
|
||||
await removeUserDefaultsValue(forKey: "RealDebrid.AccessTokenStamp")
|
||||
|
||||
// Run the request, doesn't matter if it fails
|
||||
if let token = keychain.get("RealDebrid.AccessToken") {
|
||||
|
|
|
|||
13
Ferrite/DataManagement/Classes/Action+CoreDataClass.swift
Normal file
13
Ferrite/DataManagement/Classes/Action+CoreDataClass.swift
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// Action+CoreDataClass.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 1/12/23.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(Action)
|
||||
public class Action: NSManagedObject, Plugin {}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// Action+CoreDataProperties.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/6/23.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
|
||||
extension Action {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<Action> {
|
||||
return NSFetchRequest<Action>(entityName: "Action")
|
||||
}
|
||||
|
||||
@NSManaged public var id: UUID
|
||||
@NSManaged public var listId: UUID?
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var deeplink: String?
|
||||
@NSManaged public var version: Int16
|
||||
@NSManaged public var requires: [String]
|
||||
@NSManaged public var author: String
|
||||
@NSManaged public var enabled: Bool
|
||||
@NSManaged public var tags: NSOrderedSet?
|
||||
|
||||
public func getTags() -> [PluginTagJson] {
|
||||
return requires.map { PluginTagJson(name: $0, colorHex: nil) } + tagArray.map { $0.toJson() }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Generated accessors for tags
|
||||
extension Action {
|
||||
|
||||
@objc(insertObject:inTagsAtIndex:)
|
||||
@NSManaged public func insertIntoTags(_ value: PluginTag, at idx: Int)
|
||||
|
||||
@objc(removeObjectFromTagsAtIndex:)
|
||||
@NSManaged public func removeFromTags(at idx: Int)
|
||||
|
||||
@objc(insertTags:atIndexes:)
|
||||
@NSManaged public func insertIntoTags(_ values: [PluginTag], at indexes: NSIndexSet)
|
||||
|
||||
@objc(removeTagsAtIndexes:)
|
||||
@NSManaged public func removeFromTags(at indexes: NSIndexSet)
|
||||
|
||||
@objc(replaceObjectInTagsAtIndex:withObject:)
|
||||
@NSManaged public func replaceTags(at idx: Int, with value: PluginTag)
|
||||
|
||||
@objc(replaceTagsAtIndexes:withTags:)
|
||||
@NSManaged public func replaceTags(at indexes: NSIndexSet, with values: [PluginTag])
|
||||
|
||||
@objc(addTagsObject:)
|
||||
@NSManaged public func addToTags(_ value: PluginTag)
|
||||
|
||||
@objc(removeTagsObject:)
|
||||
@NSManaged public func removeFromTags(_ value: PluginTag)
|
||||
|
||||
@objc(addTags:)
|
||||
@NSManaged public func addToTags(_ values: NSOrderedSet)
|
||||
|
||||
@objc(removeTags:)
|
||||
@NSManaged public func removeFromTags(_ values: NSOrderedSet)
|
||||
|
||||
}
|
||||
|
||||
extension Action : Identifiable {
|
||||
|
||||
}
|
||||
|
|
@ -10,15 +10,4 @@ import CoreData
|
|||
import Foundation
|
||||
|
||||
@objc(Bookmark)
|
||||
public class Bookmark: NSManagedObject {
|
||||
func toSearchResult() -> SearchResult {
|
||||
SearchResult(
|
||||
title: title,
|
||||
source: source,
|
||||
size: size,
|
||||
magnet: Magnet(hash: magnetHash, link: magnetLink),
|
||||
seeders: seeders,
|
||||
leechers: leechers
|
||||
)
|
||||
}
|
||||
}
|
||||
public class Bookmark: NSManagedObject {}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,17 @@ public extension Bookmark {
|
|||
@NSManaged var source: String
|
||||
@NSManaged var title: String?
|
||||
@NSManaged var orderNum: Int16
|
||||
|
||||
func toSearchResult() -> SearchResult {
|
||||
SearchResult(
|
||||
title: title,
|
||||
source: source,
|
||||
size: size,
|
||||
magnet: Magnet(hash: magnetHash, link: magnetLink),
|
||||
seeders: seeders,
|
||||
leechers: leechers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Bookmark: Identifiable {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// PluginList+CoreDataClass.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 1/11/23.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(PluginList)
|
||||
public class PluginList: NSManagedObject {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// PluginList+CoreDataProperties.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 1/11/23.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
|
||||
extension PluginList {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<PluginList> {
|
||||
return NSFetchRequest<PluginList>(entityName: "PluginList")
|
||||
}
|
||||
|
||||
@NSManaged public var author: String
|
||||
@NSManaged public var id: UUID
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var urlString: String
|
||||
|
||||
}
|
||||
|
||||
extension PluginList : Identifiable {
|
||||
|
||||
}
|
||||
14
Ferrite/DataManagement/Classes/PluginTag+CoreDataClass.swift
Normal file
14
Ferrite/DataManagement/Classes/PluginTag+CoreDataClass.swift
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// PluginTag+CoreDataClass.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/7/23.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(PluginTag)
|
||||
public class PluginTag: NSManagedObject {
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// PluginTag+CoreDataProperties.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/7/23.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
|
||||
extension PluginTag {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<PluginTag> {
|
||||
return NSFetchRequest<PluginTag>(entityName: "PluginTag")
|
||||
}
|
||||
|
||||
@NSManaged public var colorHex: String?
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var parentAction: Action?
|
||||
@NSManaged public var parentSource: Source?
|
||||
|
||||
func toJson() -> PluginTagJson {
|
||||
return PluginTagJson(name: name, colorHex: colorHex)
|
||||
}
|
||||
}
|
||||
|
||||
extension PluginTag : Identifiable {
|
||||
|
||||
}
|
||||
|
|
@ -10,4 +10,4 @@ import CoreData
|
|||
import Foundation
|
||||
|
||||
@objc(Source)
|
||||
public class Source: NSManagedObject {}
|
||||
public class Source: NSManagedObject, Plugin {}
|
||||
|
|
|
|||
|
|
@ -2,33 +2,77 @@
|
|||
// Source+CoreDataProperties.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/3/22.
|
||||
// Created by Brian Dashore on 2/6/23.
|
||||
//
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
public extension Source {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Source> {
|
||||
NSFetchRequest<Source>(entityName: "Source")
|
||||
|
||||
extension Source {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<Source> {
|
||||
return NSFetchRequest<Source>(entityName: "Source")
|
||||
}
|
||||
|
||||
@NSManaged var id: UUID
|
||||
@NSManaged var baseUrl: String?
|
||||
@NSManaged var fallbackUrls: [String]?
|
||||
@NSManaged var dynamicBaseUrl: Bool
|
||||
@NSManaged var enabled: Bool
|
||||
@NSManaged var name: String
|
||||
@NSManaged var author: String
|
||||
@NSManaged var listId: UUID?
|
||||
@NSManaged var preferredParser: Int16
|
||||
@NSManaged var version: Int16
|
||||
@NSManaged var htmlParser: SourceHtmlParser?
|
||||
@NSManaged var rssParser: SourceRssParser?
|
||||
@NSManaged var jsonParser: SourceJsonParser?
|
||||
@NSManaged var api: SourceApi?
|
||||
@NSManaged var trackers: [String]?
|
||||
@NSManaged public var id: UUID
|
||||
@NSManaged public var baseUrl: String?
|
||||
@NSManaged public var fallbackUrls: [String]?
|
||||
@NSManaged public var dynamicBaseUrl: Bool
|
||||
@NSManaged public var enabled: Bool
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var author: String
|
||||
@NSManaged public var listId: UUID?
|
||||
@NSManaged public var preferredParser: Int16
|
||||
@NSManaged public var version: Int16
|
||||
@NSManaged public var htmlParser: SourceHtmlParser?
|
||||
@NSManaged public var rssParser: SourceRssParser?
|
||||
@NSManaged public var jsonParser: SourceJsonParser?
|
||||
@NSManaged public var api: SourceApi?
|
||||
@NSManaged public var trackers: [String]?
|
||||
@NSManaged public var tags: NSOrderedSet?
|
||||
|
||||
public func getTags() -> [PluginTagJson] {
|
||||
return tagArray.map { $0.toJson() }
|
||||
}
|
||||
}
|
||||
|
||||
extension Source: Identifiable {}
|
||||
// MARK: Generated accessors for tags
|
||||
extension Source {
|
||||
|
||||
@objc(insertObject:inTagsAtIndex:)
|
||||
@NSManaged public func insertIntoTags(_ value: PluginTag, at idx: Int)
|
||||
|
||||
@objc(removeObjectFromTagsAtIndex:)
|
||||
@NSManaged public func removeFromTags(at idx: Int)
|
||||
|
||||
@objc(insertTags:atIndexes:)
|
||||
@NSManaged public func insertIntoTags(_ values: [PluginTag], at indexes: NSIndexSet)
|
||||
|
||||
@objc(removeTagsAtIndexes:)
|
||||
@NSManaged public func removeFromTags(at indexes: NSIndexSet)
|
||||
|
||||
@objc(replaceObjectInTagsAtIndex:withObject:)
|
||||
@NSManaged public func replaceTags(at idx: Int, with value: PluginTag)
|
||||
|
||||
@objc(replaceTagsAtIndexes:withTags:)
|
||||
@NSManaged public func replaceTags(at indexes: NSIndexSet, with values: [PluginTag])
|
||||
|
||||
@objc(addTagsObject:)
|
||||
@NSManaged public func addToTags(_ value: PluginTag)
|
||||
|
||||
@objc(removeTagsObject:)
|
||||
@NSManaged public func removeFromTags(_ value: PluginTag)
|
||||
|
||||
@objc(addTags:)
|
||||
@NSManaged public func addToTags(_ values: NSOrderedSet)
|
||||
|
||||
@objc(removeTags:)
|
||||
@NSManaged public func removeFromTags(_ values: NSOrderedSet)
|
||||
|
||||
}
|
||||
|
||||
extension Source : Identifiable {
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public extension SourceHtmlParser {
|
|||
@NSManaged var seedLeech: SourceSeedLeech?
|
||||
@NSManaged var size: SourceSize?
|
||||
@NSManaged var title: SourceTitle?
|
||||
@NSManaged var subName: SourceSubName?
|
||||
}
|
||||
|
||||
extension SourceHtmlParser: Identifiable {}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public extension SourceJsonParser {
|
|||
@NSManaged var seedLeech: SourceSeedLeech?
|
||||
@NSManaged var size: SourceSize?
|
||||
@NSManaged var title: SourceTitle?
|
||||
@NSManaged var subName: SourceSubName?
|
||||
}
|
||||
|
||||
extension SourceJsonParser: Identifiable {}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// SourceList+CoreDataClass.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/30/22.
|
||||
//
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(SourceList)
|
||||
public class SourceList: NSManagedObject {}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
//
|
||||
// SourceList+CoreDataProperties.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/30/22.
|
||||
//
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
public extension SourceList {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<SourceList> {
|
||||
NSFetchRequest<SourceList>(entityName: "SourceList")
|
||||
}
|
||||
|
||||
@NSManaged var id: UUID
|
||||
@NSManaged var author: String
|
||||
@NSManaged var name: String
|
||||
@NSManaged var urlString: String
|
||||
}
|
||||
|
||||
extension SourceList: Identifiable {}
|
||||
|
|
@ -23,6 +23,7 @@ public extension SourceRssParser {
|
|||
@NSManaged var seedLeech: SourceSeedLeech?
|
||||
@NSManaged var size: SourceSize?
|
||||
@NSManaged var title: SourceTitle?
|
||||
@NSManaged var subName: SourceSubName?
|
||||
}
|
||||
|
||||
extension SourceRssParser: Identifiable {}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>FerriteDB.xcdatamodel</string>
|
||||
<string>FerriteDB_v2.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Action" representedClassName="Action" syncable="YES">
|
||||
<attribute name="author" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="deeplink" optional="YES" attributeType="String"/>
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="listId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="requires" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
|
||||
<attribute name="version" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="PluginTag" inverseName="parentAction" inverseEntity="PluginTag"/>
|
||||
</entity>
|
||||
<entity name="Bookmark" representedClassName="Bookmark" syncable="YES">
|
||||
<attribute name="leechers" optional="YES" attributeType="String"/>
|
||||
<attribute name="magnetHash" optional="YES" attributeType="String"/>
|
||||
<attribute name="magnetLink" optional="YES" attributeType="String"/>
|
||||
<attribute name="orderNum" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="seeders" optional="YES" attributeType="String"/>
|
||||
<attribute name="size" optional="YES" attributeType="String"/>
|
||||
<attribute name="source" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="History" representedClassName="History" syncable="YES">
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="dateString" optional="YES" attributeType="String"/>
|
||||
<relationship name="entries" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="HistoryEntry" inverseName="parentHistory" inverseEntity="HistoryEntry"/>
|
||||
</entity>
|
||||
<entity name="HistoryEntry" representedClassName="HistoryEntry" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="source" optional="YES" attributeType="String"/>
|
||||
<attribute name="subName" optional="YES" attributeType="String"/>
|
||||
<attribute name="timeStamp" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="url" optional="YES" attributeType="String"/>
|
||||
<relationship name="parentHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="History" inverseName="entries" inverseEntity="History"/>
|
||||
</entity>
|
||||
<entity name="PluginList" representedClassName="PluginList" syncable="YES">
|
||||
<attribute name="author" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="urlString" attributeType="String" defaultValueString=""/>
|
||||
</entity>
|
||||
<entity name="PluginTag" representedClassName="PluginTag" syncable="YES">
|
||||
<attribute name="colorHex" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||
<relationship name="parentAction" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Action" inverseName="tags" inverseEntity="Action"/>
|
||||
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="tags" inverseEntity="Source"/>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="author" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="baseUrl" optional="YES" attributeType="String"/>
|
||||
<attribute name="dynamicBaseUrl" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="fallbackUrls" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
|
||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="listId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="preferredParser" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="trackers" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
|
||||
<attribute name="version" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="api" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceApi" inverseName="parentSource" inverseEntity="SourceApi"/>
|
||||
<relationship name="htmlParser" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceHtmlParser" inverseName="parentSource" inverseEntity="SourceHtmlParser"/>
|
||||
<relationship name="jsonParser" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceJsonParser" inverseName="parentSource" inverseEntity="SourceJsonParser"/>
|
||||
<relationship name="rssParser" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceRssParser" inverseName="parentSource" inverseEntity="SourceRssParser"/>
|
||||
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="PluginTag" inverseName="parentSource" inverseEntity="PluginTag"/>
|
||||
</entity>
|
||||
<entity name="SourceApi" representedClassName="SourceApi" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="apiUrl" optional="YES" attributeType="String"/>
|
||||
<relationship name="clientId" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceApiClientId" inverseName="parentApi" inverseEntity="SourceApiClientId"/>
|
||||
<relationship name="clientSecret" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceApiClientSecret" inverseName="parentApi" inverseEntity="SourceApiClientSecret"/>
|
||||
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="api" inverseEntity="Source"/>
|
||||
</entity>
|
||||
<entity name="SourceApiClientId" representedClassName="SourceApiClientId" parentEntity="SourceApiCredential" syncable="YES" codeGenerationType="class">
|
||||
<relationship name="parentApi" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceApi" inverseName="clientId" inverseEntity="SourceApi"/>
|
||||
</entity>
|
||||
<entity name="SourceApiClientSecret" representedClassName="SourceApiClientSecret" parentEntity="SourceApiCredential" syncable="YES" codeGenerationType="class">
|
||||
<relationship name="parentApi" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceApi" inverseName="clientSecret" inverseEntity="SourceApi"/>
|
||||
</entity>
|
||||
<entity name="SourceApiCredential" representedClassName="SourceApiCredential" isAbstract="YES" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="dynamic" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="expiryLength" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="query" optional="YES" attributeType="String"/>
|
||||
<attribute name="responseType" optional="YES" attributeType="String"/>
|
||||
<attribute name="timeStamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="urlString" optional="YES" attributeType="String"/>
|
||||
<attribute name="value" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="SourceComplexQuery" representedClassName="SourceComplexQuery" isAbstract="YES" syncable="YES">
|
||||
<attribute name="attribute" attributeType="String" defaultValueString="text"/>
|
||||
<attribute name="discriminator" optional="YES" attributeType="String"/>
|
||||
<attribute name="query" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="regex" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="SourceHtmlParser" representedClassName="SourceHtmlParser" syncable="YES">
|
||||
<attribute name="rows" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="searchUrl" attributeType="String" defaultValueString=""/>
|
||||
<relationship name="magnetHash" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetHash" inverseName="parentHtmlParser" inverseEntity="SourceMagnetHash"/>
|
||||
<relationship name="magnetLink" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetLink" inverseName="parentHtmlParser" inverseEntity="SourceMagnetLink"/>
|
||||
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="htmlParser" inverseEntity="Source"/>
|
||||
<relationship name="seedLeech" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSeedLeech" inverseName="parentHtmlParser" inverseEntity="SourceSeedLeech"/>
|
||||
<relationship name="size" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSize" inverseName="parentHtmlParser" inverseEntity="SourceSize"/>
|
||||
<relationship name="subName" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceSubName" inverseName="parentHtmlParser" inverseEntity="SourceSubName"/>
|
||||
<relationship name="title" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceTitle" inverseName="parentHtmlParser" inverseEntity="SourceTitle"/>
|
||||
</entity>
|
||||
<entity name="SourceJsonParser" representedClassName="SourceJsonParser" syncable="YES">
|
||||
<attribute name="results" optional="YES" attributeType="String" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
|
||||
<attribute name="searchUrl" optional="YES" attributeType="String"/>
|
||||
<attribute name="subResults" optional="YES" attributeType="String"/>
|
||||
<relationship name="magnetHash" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetHash" inverseName="parentJsonParser" inverseEntity="SourceMagnetHash"/>
|
||||
<relationship name="magnetLink" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetLink" inverseName="parentJsonParser" inverseEntity="SourceMagnetLink"/>
|
||||
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="jsonParser" inverseEntity="Source"/>
|
||||
<relationship name="seedLeech" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSeedLeech" inverseName="parentJsonParser" inverseEntity="SourceSeedLeech"/>
|
||||
<relationship name="size" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSize" inverseName="parentJsonParser" inverseEntity="SourceSize"/>
|
||||
<relationship name="subName" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceSubName" inverseName="parentJsonParser" inverseEntity="SourceSubName"/>
|
||||
<relationship name="title" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceTitle" inverseName="parentJsonParser" inverseEntity="SourceTitle"/>
|
||||
</entity>
|
||||
<entity name="SourceMagnetHash" representedClassName="SourceMagnetHash" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
|
||||
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="magnetHash" inverseEntity="SourceHtmlParser"/>
|
||||
<relationship name="parentJsonParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceJsonParser" inverseName="magnetHash" inverseEntity="SourceJsonParser"/>
|
||||
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="magnetHash" inverseEntity="SourceRssParser"/>
|
||||
</entity>
|
||||
<entity name="SourceMagnetLink" representedClassName="SourceMagnetLink" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="externalLinkQuery" optional="YES" attributeType="String"/>
|
||||
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="magnetLink" inverseEntity="SourceHtmlParser"/>
|
||||
<relationship name="parentJsonParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceJsonParser" inverseName="magnetLink" inverseEntity="SourceJsonParser"/>
|
||||
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="magnetLink" inverseEntity="SourceRssParser"/>
|
||||
</entity>
|
||||
<entity name="SourceRssParser" representedClassName="SourceRssParser" syncable="YES">
|
||||
<attribute name="items" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="rssUrl" optional="YES" attributeType="String"/>
|
||||
<attribute name="searchUrl" attributeType="String" defaultValueString=""/>
|
||||
<relationship name="magnetHash" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetHash" inverseName="parentRssParser" inverseEntity="SourceMagnetHash"/>
|
||||
<relationship name="magnetLink" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetLink" inverseName="parentRssParser" inverseEntity="SourceMagnetLink"/>
|
||||
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="rssParser" inverseEntity="Source"/>
|
||||
<relationship name="seedLeech" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSeedLeech" inverseName="parentRssParser" inverseEntity="SourceSeedLeech"/>
|
||||
<relationship name="size" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSize" inverseName="parentRssParser" inverseEntity="SourceSize"/>
|
||||
<relationship name="subName" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceSubName" inverseName="parentRssParser" inverseEntity="SourceSubName"/>
|
||||
<relationship name="title" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceTitle" inverseName="parentRssParser" inverseEntity="SourceTitle"/>
|
||||
</entity>
|
||||
<entity name="SourceSeedLeech" representedClassName="SourceSeedLeech" syncable="YES">
|
||||
<attribute name="attribute" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="combined" optional="YES" attributeType="String"/>
|
||||
<attribute name="discriminator" optional="YES" attributeType="String"/>
|
||||
<attribute name="leecherRegex" optional="YES" attributeType="String"/>
|
||||
<attribute name="leechers" optional="YES" attributeType="String"/>
|
||||
<attribute name="seederRegex" optional="YES" attributeType="String"/>
|
||||
<attribute name="seeders" optional="YES" attributeType="String"/>
|
||||
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="seedLeech" inverseEntity="SourceHtmlParser"/>
|
||||
<relationship name="parentJsonParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceJsonParser" inverseName="seedLeech" inverseEntity="SourceJsonParser"/>
|
||||
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="seedLeech" inverseEntity="SourceRssParser"/>
|
||||
</entity>
|
||||
<entity name="SourceSize" representedClassName="SourceSize" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
|
||||
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="size" inverseEntity="SourceHtmlParser"/>
|
||||
<relationship name="parentJsonParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceJsonParser" inverseName="size" inverseEntity="SourceJsonParser"/>
|
||||
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="size" inverseEntity="SourceRssParser"/>
|
||||
</entity>
|
||||
<entity name="SourceSubName" representedClassName="SourceSubName" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
|
||||
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="subName" inverseEntity="SourceHtmlParser"/>
|
||||
<relationship name="parentJsonParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceJsonParser" inverseName="subName" inverseEntity="SourceJsonParser"/>
|
||||
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="subName" inverseEntity="SourceRssParser"/>
|
||||
</entity>
|
||||
<entity name="SourceTitle" representedClassName="SourceTitle" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
|
||||
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="title" inverseEntity="SourceHtmlParser"/>
|
||||
<relationship name="parentJsonParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceJsonParser" inverseName="title" inverseEntity="SourceJsonParser"/>
|
||||
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="title" inverseEntity="SourceRssParser"/>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -91,7 +91,7 @@ struct PersistenceController {
|
|||
save()
|
||||
}
|
||||
|
||||
func createBookmark(_ bookmarkJson: BookmarkJson) {
|
||||
func createBookmark(_ bookmarkJson: BookmarkJson, performSave: Bool) {
|
||||
let bookmarkRequest = Bookmark.fetchRequest()
|
||||
bookmarkRequest.predicate = NSPredicate(
|
||||
format: "source == %@ AND title == %@ AND magnetLink == %@",
|
||||
|
|
@ -113,32 +113,31 @@ struct PersistenceController {
|
|||
newBookmark.seeders = bookmarkJson.seeders
|
||||
newBookmark.leechers = bookmarkJson.leechers
|
||||
|
||||
save(backgroundContext)
|
||||
if performSave {
|
||||
save(backgroundContext)
|
||||
}
|
||||
}
|
||||
|
||||
func createHistory(_ entryJson: HistoryEntryJson, date: Double? = nil) {
|
||||
func createHistory(_ entryJson: HistoryEntryJson, performSave: Bool, isBackup: Bool = false, date: Double? = nil) {
|
||||
let historyDate = date.map { Date(timeIntervalSince1970: $0) } ?? Date()
|
||||
let historyDateString = DateFormatter.historyDateFormatter.string(from: historyDate)
|
||||
|
||||
let newHistoryEntry = HistoryEntry(context: backgroundContext)
|
||||
|
||||
newHistoryEntry.source = entryJson.source
|
||||
newHistoryEntry.name = entryJson.name
|
||||
newHistoryEntry.url = entryJson.url
|
||||
newHistoryEntry.subName = entryJson.subName
|
||||
newHistoryEntry.timeStamp = entryJson.timeStamp ?? Date().timeIntervalSince1970
|
||||
|
||||
let historyRequest = History.fetchRequest()
|
||||
historyRequest.predicate = NSPredicate(format: "dateString = %@", historyDateString)
|
||||
var existingHistory: History?
|
||||
|
||||
// Safely add entries to a parent history if it exists
|
||||
if var histories = try? backgroundContext.fetch(historyRequest) {
|
||||
for (i, history) in histories.enumerated() {
|
||||
let existingEntries = history.entryArray.filter { $0.url == newHistoryEntry.url && $0.name == newHistoryEntry.name }
|
||||
let existingEntries = history.entryArray.filter { $0.url == entryJson.url && $0.name == entryJson.name }
|
||||
|
||||
// Maybe add !isBackup here
|
||||
if !existingEntries.isEmpty {
|
||||
for entry in existingEntries {
|
||||
PersistenceController.shared.delete(entry, context: backgroundContext)
|
||||
if isBackup {
|
||||
continue
|
||||
} else {
|
||||
for entry in existingEntries {
|
||||
PersistenceController.shared.delete(entry, context: backgroundContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,15 +147,24 @@ struct PersistenceController {
|
|||
}
|
||||
}
|
||||
|
||||
newHistoryEntry.parentHistory = histories.first ?? History(context: backgroundContext)
|
||||
} else {
|
||||
newHistoryEntry.parentHistory = History(context: backgroundContext)
|
||||
existingHistory = histories.first
|
||||
}
|
||||
|
||||
let newHistoryEntry = HistoryEntry(context: backgroundContext)
|
||||
|
||||
newHistoryEntry.source = entryJson.source
|
||||
newHistoryEntry.name = entryJson.name
|
||||
newHistoryEntry.url = entryJson.url
|
||||
newHistoryEntry.subName = entryJson.subName
|
||||
newHistoryEntry.timeStamp = entryJson.timeStamp ?? Date().timeIntervalSince1970
|
||||
|
||||
newHistoryEntry.parentHistory = existingHistory ?? History(context: backgroundContext)
|
||||
newHistoryEntry.parentHistory?.dateString = historyDateString
|
||||
newHistoryEntry.parentHistory?.date = historyDate
|
||||
|
||||
save(backgroundContext)
|
||||
if performSave {
|
||||
save(backgroundContext)
|
||||
}
|
||||
}
|
||||
|
||||
func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? {
|
||||
|
|
@ -200,8 +208,7 @@ struct PersistenceController {
|
|||
return predicate
|
||||
}
|
||||
|
||||
// Always use the background context to batch delete
|
||||
// Merge changes into both contexts to update views
|
||||
// Wrapper to batch delete history objects
|
||||
func batchDeleteHistory(range: HistoryDeleteRange) throws {
|
||||
let predicate = getHistoryPredicate(range: range)
|
||||
|
||||
|
|
@ -213,6 +220,13 @@ struct PersistenceController {
|
|||
throw HistoryDeleteError.noDate("No history date range was provided and you weren't trying to clear everything! Try relaunching the app?")
|
||||
}
|
||||
|
||||
try batchDelete("History", predicate: predicate)
|
||||
}
|
||||
|
||||
// Always use the background context to batch delete
|
||||
// Merge changes into both contexts to update views
|
||||
func batchDelete(_ entity: String, predicate: NSPredicate? = nil) throws {
|
||||
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entity)
|
||||
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
|
||||
batchDeleteRequest.resultType = .resultTypeObjectIDs
|
||||
|
|
|
|||
18
Ferrite/Extensions/UIDevice.swift
Normal file
18
Ferrite/Extensions/UIDevice.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// UIDevice.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/16/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension UIDevice {
|
||||
var hasNotch: Bool {
|
||||
if #available(iOS 11.0, *) {
|
||||
let keyWindow = UIApplication.shared.windows.filter(\.isKeyWindow).first
|
||||
return keyWindow?.safeAreaInsets.bottom ?? 0 > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -9,51 +9,39 @@ import Introspect
|
|||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
// MARK: Custom introspect functions
|
||||
|
||||
func introspectCollectionView(customize: @escaping (UICollectionView) -> Void) -> some View {
|
||||
inject(UIKitIntrospectionView(
|
||||
selector: { introspectionView in
|
||||
guard let viewHost = Introspect.findViewHost(from: introspectionView) else {
|
||||
return nil
|
||||
}
|
||||
return Introspect.previousSibling(containing: UICollectionView.self, from: viewHost)
|
||||
},
|
||||
customize: customize
|
||||
))
|
||||
}
|
||||
|
||||
// From https://github.com/siteline/SwiftUI-Introspect/pull/129
|
||||
public func introspectSearchController(customize: @escaping (UISearchController) -> Void) -> some View {
|
||||
introspectNavigationController { navigationController in
|
||||
let navigationBar = navigationController.navigationBar
|
||||
if let searchController = navigationBar.topItem?.searchController {
|
||||
customize(searchController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Modifiers
|
||||
|
||||
func conditionalContextMenu(id: some Hashable,
|
||||
@ViewBuilder _ internalContent: @escaping () -> some View) -> some View
|
||||
{
|
||||
modifier(ConditionalContextMenu(internalContent, id: id))
|
||||
modifier(ConditionalContextMenuModifier(internalContent, id: id))
|
||||
}
|
||||
|
||||
func conditionalId(_ id: some Hashable) -> some View {
|
||||
modifier(ConditionalId(id: id))
|
||||
modifier(ConditionalIdModifier(id: id))
|
||||
}
|
||||
|
||||
func disabledAppearance(_ disabled: Bool, dimmedOpacity: Double? = nil, animation: Animation? = nil) -> some View {
|
||||
modifier(DisabledAppearance(disabled: disabled, dimmedOpacity: dimmedOpacity, animation: animation))
|
||||
modifier(DisabledAppearanceModifier(disabled: disabled, dimmedOpacity: dimmedOpacity, animation: animation))
|
||||
}
|
||||
|
||||
func disableInteraction(_ disabled: Bool) -> some View {
|
||||
modifier(DisableInteraction(disabled: disabled))
|
||||
modifier(DisableInteractionModifier(disabled: disabled))
|
||||
}
|
||||
|
||||
func inlinedList() -> some View {
|
||||
modifier(InlinedList())
|
||||
func inlinedList(inset: CGFloat) -> some View {
|
||||
modifier(InlinedListModifier(inset: inset))
|
||||
}
|
||||
|
||||
func viewDidAppear(_ callback: @escaping () -> Void) -> some View {
|
||||
modifier(ViewDidAppearModifier(callback: callback))
|
||||
}
|
||||
|
||||
func customScopeBar<Content: View>(_ content: Content) -> some View {
|
||||
modifier(CustomScopeBarModifier(hostingContent: content))
|
||||
}
|
||||
|
||||
func customScopeBar<Content: View>(_ content: @escaping () -> Content) -> some View {
|
||||
modifier(CustomScopeBarModifier(hostingContent: content()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,16 +15,16 @@ struct FerriteApp: App {
|
|||
@StateObject var toastModel: ToastViewModel = .init()
|
||||
@StateObject var debridManager: DebridManager = .init()
|
||||
@StateObject var navModel: NavigationViewModel = .init()
|
||||
@StateObject var sourceManager: SourceManager = .init()
|
||||
@StateObject var pluginManager: PluginManager = .init()
|
||||
@StateObject var backupManager: BackupManager = .init()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
MainView()
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
scrapingModel.toastModel = toastModel
|
||||
debridManager.toastModel = toastModel
|
||||
sourceManager.toastModel = toastModel
|
||||
pluginManager.toastModel = toastModel
|
||||
backupManager.toastModel = toastModel
|
||||
navModel.toastModel = toastModel
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ struct FerriteApp: App {
|
|||
.environmentObject(scrapingModel)
|
||||
.environmentObject(toastModel)
|
||||
.environmentObject(navModel)
|
||||
.environmentObject(sourceManager)
|
||||
.environmentObject(pluginManager)
|
||||
.environmentObject(backupManager)
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
}
|
||||
|
|
|
|||
32
Ferrite/Models/ActionModels.swift
Normal file
32
Ferrite/Models/ActionModels.swift
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// ActionModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 1/11/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ActionJson: Codable, Hashable, PluginJson {
|
||||
public let name: String
|
||||
public let version: Int16
|
||||
let minVersion: String?
|
||||
let requires: [ActionRequirement]
|
||||
let deeplink: String?
|
||||
public var author: String?
|
||||
public var listId: UUID?
|
||||
public var tags: [PluginTagJson]?
|
||||
}
|
||||
|
||||
extension ActionJson {
|
||||
// Fetches all tags without optional requirement
|
||||
// Avoids the need for extra tag additions in DB
|
||||
public func getTags() -> [PluginTagJson] {
|
||||
return requires.map { PluginTagJson(name: $0.rawValue, colorHex: nil) } + (tags.map { $0 } ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
public enum ActionRequirement: String, Codable {
|
||||
case magnet
|
||||
case debrid
|
||||
}
|
||||
|
|
@ -7,12 +7,17 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
// Version is optional until v1 is phased out
|
||||
public struct Backup: Codable {
|
||||
let version: Int
|
||||
let version: Int?
|
||||
var bookmarks: [BookmarkJson]?
|
||||
var history: [HistoryJson]?
|
||||
var sourceNames: [String]?
|
||||
var sourceLists: [SourceListBackupJson]?
|
||||
var actionNames: [String]?
|
||||
var pluginListUrls: [String]?
|
||||
|
||||
// MARK: Remove once v1 backups are unsupported
|
||||
var sourceLists: [PluginListBackupJson]?
|
||||
}
|
||||
|
||||
// MARK: - CoreData translation
|
||||
|
|
@ -43,8 +48,8 @@ struct HistoryEntryJson: Codable {
|
|||
let source: String?
|
||||
}
|
||||
|
||||
// Differs from SourceListJson
|
||||
struct SourceListBackupJson: Codable {
|
||||
// Differs from PluginListJson
|
||||
struct PluginListBackupJson: Codable {
|
||||
let name: String
|
||||
let author: String
|
||||
let id: String
|
||||
|
|
|
|||
32
Ferrite/Models/PluginModels.swift
Normal file
32
Ferrite/Models/PluginModels.swift
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// PluginModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 1/11/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct PluginListJson: Codable {
|
||||
let name: String
|
||||
let author: String
|
||||
var sources: [SourceJson]?
|
||||
var actions: [ActionJson]?
|
||||
}
|
||||
|
||||
// Color: Hex value
|
||||
public struct PluginTagJson: Codable, Hashable, Sendable {
|
||||
public let name: String
|
||||
public let colorHex: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case colorHex = "color"
|
||||
}
|
||||
}
|
||||
|
||||
extension PluginManager {
|
||||
enum PluginManagerError: Error {
|
||||
case ListAddition(description: String)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
//
|
||||
// SettingsModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/11/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum DefaultMagnetActionType: Int, CaseIterable {
|
||||
// Let the user choose
|
||||
case none = 0
|
||||
|
||||
// Open in actions come first
|
||||
case webtor = 1
|
||||
|
||||
// Sharing actions come last
|
||||
case shareMagnet = 2
|
||||
}
|
||||
|
||||
public enum DefaultDebridActionType: Int, CaseIterable {
|
||||
// Let the user choose
|
||||
case none = 0
|
||||
|
||||
// Open in actions come first
|
||||
case outplayer = 1
|
||||
case vlc = 2
|
||||
case infuse = 3
|
||||
|
||||
// Sharing actions come last
|
||||
case shareDownload = 4
|
||||
}
|
||||
|
|
@ -12,26 +12,28 @@ public enum ApiCredentialResponseType: String, Codable, Hashable, Sendable {
|
|||
case text
|
||||
}
|
||||
|
||||
public struct SourceListJson: Codable, Sendable {
|
||||
let name: String
|
||||
let author: String
|
||||
var sources: [SourceJson]
|
||||
}
|
||||
|
||||
public struct SourceJson: Codable, Hashable, Sendable {
|
||||
let name: String
|
||||
let version: Int16
|
||||
public struct SourceJson: Codable, Hashable, Sendable, PluginJson {
|
||||
public let name: String
|
||||
public let version: Int16
|
||||
let minVersion: String?
|
||||
let baseUrl: String?
|
||||
let fallbackUrls: [String]?
|
||||
var dynamicBaseUrl: Bool?
|
||||
var author: String?
|
||||
var listId: UUID?
|
||||
let dynamicBaseUrl: Bool?
|
||||
let trackers: [String]?
|
||||
let api: SourceApiJson?
|
||||
let jsonParser: SourceJsonParserJson?
|
||||
let rssParser: SourceRssParserJson?
|
||||
let htmlParser: SourceHtmlParserJson?
|
||||
public var author: String?
|
||||
public var listId: UUID?
|
||||
public var tags: [PluginTagJson]?
|
||||
}
|
||||
|
||||
extension SourceJson {
|
||||
// Fetches all tags without optional requirement
|
||||
public func getTags() -> [PluginTagJson] {
|
||||
return tags ?? []
|
||||
}
|
||||
}
|
||||
|
||||
public enum SourcePreferredParser: Int16, CaseIterable, Sendable {
|
||||
|
|
@ -60,10 +62,11 @@ public struct SourceJsonParserJson: Codable, Hashable, Sendable {
|
|||
let searchUrl: String
|
||||
let results: String?
|
||||
let subResults: String?
|
||||
let magnetHash: SouceComplexQueryJson?
|
||||
let magnetLink: SouceComplexQueryJson?
|
||||
let title: SouceComplexQueryJson?
|
||||
let size: SouceComplexQueryJson?
|
||||
let magnetHash: SourceComplexQueryJson?
|
||||
let magnetLink: SourceComplexQueryJson?
|
||||
let subName: SourceComplexQueryJson?
|
||||
let title: SourceComplexQueryJson?
|
||||
let size: SourceComplexQueryJson?
|
||||
let sl: SourceSLJson?
|
||||
}
|
||||
|
||||
|
|
@ -71,10 +74,11 @@ public struct SourceRssParserJson: Codable, Hashable, Sendable {
|
|||
let rssUrl: String?
|
||||
let searchUrl: String
|
||||
let items: String
|
||||
let magnetHash: SouceComplexQueryJson?
|
||||
let magnetLink: SouceComplexQueryJson?
|
||||
let title: SouceComplexQueryJson?
|
||||
let size: SouceComplexQueryJson?
|
||||
let magnetHash: SourceComplexQueryJson?
|
||||
let magnetLink: SourceComplexQueryJson?
|
||||
let subName: SourceComplexQueryJson?
|
||||
let title: SourceComplexQueryJson?
|
||||
let size: SourceComplexQueryJson?
|
||||
let sl: SourceSLJson?
|
||||
}
|
||||
|
||||
|
|
@ -82,12 +86,13 @@ public struct SourceHtmlParserJson: Codable, Hashable, Sendable {
|
|||
let searchUrl: String
|
||||
let rows: String
|
||||
let magnet: SourceMagnetJson
|
||||
let title: SouceComplexQueryJson?
|
||||
let size: SouceComplexQueryJson?
|
||||
let subName: SourceComplexQueryJson?
|
||||
let title: SourceComplexQueryJson?
|
||||
let size: SourceComplexQueryJson?
|
||||
let sl: SourceSLJson?
|
||||
}
|
||||
|
||||
public struct SouceComplexQueryJson: Codable, Hashable, Sendable {
|
||||
public struct SourceComplexQueryJson: Codable, Hashable, Sendable {
|
||||
let query: String
|
||||
let discriminator: String?
|
||||
let attribute: String?
|
||||
|
|
|
|||
35
Ferrite/Protocols/Plugin.swift
Normal file
35
Ferrite/Protocols/Plugin.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// Plugin.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 1/25/23.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
public protocol Plugin: ObservableObject, NSManagedObject {
|
||||
var id: UUID { get set }
|
||||
var listId: UUID? { get set }
|
||||
var name: String { get set }
|
||||
var version: Int16 { get set }
|
||||
var author: String { get set }
|
||||
var enabled: Bool { get set }
|
||||
var tags: NSOrderedSet? { get set }
|
||||
func getTags() -> [PluginTagJson]
|
||||
}
|
||||
|
||||
extension Plugin {
|
||||
var tagArray: [PluginTag] {
|
||||
return self.tags?.array as? [PluginTag] ?? []
|
||||
}
|
||||
}
|
||||
|
||||
public protocol PluginJson: Hashable {
|
||||
var name: String { get }
|
||||
var version: Int16 { get }
|
||||
var author: String? { get set }
|
||||
var listId: UUID? { get set }
|
||||
var tags: [PluginTagJson]? { get set }
|
||||
func getTags() -> [PluginTagJson]
|
||||
}
|
||||
|
|
@ -9,18 +9,33 @@ import Foundation
|
|||
|
||||
public class BackupManager: ObservableObject {
|
||||
// Constant variable for backup versions
|
||||
let latestBackupVersion: Int = 1
|
||||
let latestBackupVersion: Int = 2
|
||||
|
||||
var toastModel: ToastViewModel?
|
||||
|
||||
@Published var showRestoreAlert = false
|
||||
@Published var showRestoreCompletedAlert = false
|
||||
@Published var restoreCompletedMessage: [String] = []
|
||||
|
||||
@Published var backupUrls: [URL] = []
|
||||
@Published var backupSourceNames: [String] = []
|
||||
@Published var selectedBackupUrl: URL?
|
||||
|
||||
func createBackup() {
|
||||
@MainActor
|
||||
func updateRestoreCompletedMessage(newString: String) {
|
||||
restoreCompletedMessage.append(newString)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func toggleRestoreCompletedAlert() {
|
||||
showRestoreCompletedAlert.toggle()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func updateBackupUrls(newUrl: URL) {
|
||||
backupUrls.append(newUrl)
|
||||
}
|
||||
|
||||
func createBackup() async {
|
||||
var backup = Backup(version: latestBackupVersion)
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
|
|
@ -71,16 +86,14 @@ public class BackupManager: ObservableObject {
|
|||
backup.sourceNames = sources.map(\.name)
|
||||
}
|
||||
|
||||
let sourceListRequest = SourceList.fetchRequest()
|
||||
if let sourceLists = try? backgroundContext.fetch(sourceListRequest) {
|
||||
backup.sourceLists = sourceLists.map {
|
||||
SourceListBackupJson(
|
||||
name: $0.name,
|
||||
author: $0.author,
|
||||
id: $0.id.uuidString,
|
||||
urlString: $0.urlString
|
||||
)
|
||||
}
|
||||
let actionRequest = Action.fetchRequest()
|
||||
if let actions = try? backgroundContext.fetch(actionRequest) {
|
||||
backup.actionNames = actions.map(\.name)
|
||||
}
|
||||
|
||||
let pluginListRequest = PluginList.fetchRequest()
|
||||
if let pluginLists = try? backgroundContext.fetch(pluginListRequest) {
|
||||
backup.pluginListUrls = pluginLists.map(\.urlString)
|
||||
}
|
||||
|
||||
do {
|
||||
|
|
@ -94,18 +107,20 @@ public class BackupManager: ObservableObject {
|
|||
let writeUrl = backupsPath.appendingPathComponent("Ferrite-backup-\(snapshot).feb")
|
||||
|
||||
try encodedJson.write(to: writeUrl)
|
||||
backupUrls.append(writeUrl)
|
||||
|
||||
await updateBackupUrls(newUrl: writeUrl)
|
||||
} catch {
|
||||
print(error)
|
||||
await toastModel?.updateToastDescription("Backup error: \(error)")
|
||||
print("Backup error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Backup is in local documents directory, so no need to restore it from the shared URL
|
||||
func restoreBackup() {
|
||||
// Pass the pluginManager reference since it's not used throughout the class like toastModel
|
||||
func restoreBackup(pluginManager: PluginManager, doOverwrite: Bool) async {
|
||||
guard let backupUrl = selectedBackupUrl else {
|
||||
Task {
|
||||
await toastModel?.updateToastDescription("Could not find the selected backup in the local directory.")
|
||||
}
|
||||
await toastModel?.updateToastDescription("Could not find the selected backup in the local directory.")
|
||||
print("Backup restore error: Could not find backup in app directory.")
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -113,64 +128,74 @@ public class BackupManager: ObservableObject {
|
|||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
do {
|
||||
// Delete all relevant entities to prevent issues with restoration if overwrite is selected
|
||||
if doOverwrite {
|
||||
try PersistenceController.shared.batchDelete("Bookmark")
|
||||
try PersistenceController.shared.batchDelete("History")
|
||||
try PersistenceController.shared.batchDelete("HistoryEntry")
|
||||
try PersistenceController.shared.batchDelete("PluginList")
|
||||
try PersistenceController.shared.batchDelete("Source")
|
||||
try PersistenceController.shared.batchDelete("Action")
|
||||
}
|
||||
|
||||
let file = try Data(contentsOf: backupUrl)
|
||||
|
||||
let backup = try JSONDecoder().decode(Backup.self, from: file)
|
||||
|
||||
if let bookmarks = backup.bookmarks {
|
||||
for bookmark in bookmarks {
|
||||
PersistenceController.shared.createBookmark(bookmark)
|
||||
PersistenceController.shared.createBookmark(bookmark, performSave: false)
|
||||
}
|
||||
}
|
||||
|
||||
if let storedHistories = backup.history {
|
||||
for storedHistory in storedHistories {
|
||||
for storedEntry in storedHistory.entries {
|
||||
PersistenceController.shared.createHistory(storedEntry, date: storedHistory.date)
|
||||
PersistenceController.shared.createHistory(
|
||||
storedEntry,
|
||||
performSave: false,
|
||||
isBackup: true,
|
||||
date: storedHistory.date
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let storedLists = backup.sourceLists {
|
||||
let version = backup.version ?? -1
|
||||
|
||||
if let storedLists = backup.sourceLists, version < 2 {
|
||||
// Only present in v1 or no version backups
|
||||
for list in storedLists {
|
||||
let sourceListRequest = SourceList.fetchRequest()
|
||||
let urlPredicate = NSPredicate(format: "urlString == %@", list.urlString)
|
||||
let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", list.author, list.name)
|
||||
sourceListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate])
|
||||
sourceListRequest.fetchLimit = 1
|
||||
|
||||
if (try? backgroundContext.fetch(sourceListRequest).first) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
let newSourceList = SourceList(context: backgroundContext)
|
||||
newSourceList.name = list.name
|
||||
newSourceList.urlString = list.urlString
|
||||
newSourceList.id = UUID(uuidString: list.id) ?? UUID()
|
||||
newSourceList.author = list.author
|
||||
try await pluginManager.addPluginList(list.urlString, existingPluginList: nil)
|
||||
}
|
||||
} else if let pluginListUrls = backup.pluginListUrls {
|
||||
// v2 and up
|
||||
for listUrl in pluginListUrls {
|
||||
try await pluginManager.addPluginList(listUrl, existingPluginList: nil)
|
||||
}
|
||||
}
|
||||
|
||||
backupSourceNames = backup.sourceNames ?? []
|
||||
if let sourceNames = backup.sourceNames {
|
||||
await updateRestoreCompletedMessage(newString: sourceNames.isEmpty ? "No sources need to be reinstalled" : "Reinstall sources: \(sourceNames.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
if let actionNames = backup.actionNames {
|
||||
await updateRestoreCompletedMessage(newString: actionNames.isEmpty ? "No actions need to be reinstalled" : "Reinstall actions: \(actionNames.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
PersistenceController.shared.save(backgroundContext)
|
||||
|
||||
// if iOS 14 is available, sleep to prevent any issues with alerts
|
||||
if #available(iOS 15, *) {
|
||||
showRestoreCompletedAlert.toggle()
|
||||
await toggleRestoreCompletedAlert()
|
||||
} else {
|
||||
Task {
|
||||
try? await Task.sleep(seconds: 0.1)
|
||||
try? await Task.sleep(seconds: 0.1)
|
||||
|
||||
Task { @MainActor in
|
||||
showRestoreCompletedAlert.toggle()
|
||||
}
|
||||
}
|
||||
await toggleRestoreCompletedAlert()
|
||||
}
|
||||
} catch {
|
||||
Task {
|
||||
await toastModel?.updateToastDescription("Backup restore: \(error)")
|
||||
}
|
||||
await toastModel?.updateToastDescription("Backup restore error: \(error)")
|
||||
print("Backup restore error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +212,8 @@ public class BackupManager: ObservableObject {
|
|||
}
|
||||
} catch {
|
||||
Task {
|
||||
await toastModel?.updateToastDescription("Backup removal: \(error)")
|
||||
await toastModel?.updateToastDescription("Backup removal error: \(error)")
|
||||
print("Backup removal error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ public class DebridManager: ObservableObject {
|
|||
// UI Variables
|
||||
@Published var showWebView: Bool = false
|
||||
@Published var showAuthSession: Bool = false
|
||||
@Published var showLoadingProgress: Bool = false
|
||||
|
||||
// Service agnostic variables
|
||||
@Published var enabledDebrids: Set<DebridType> = [] {
|
||||
|
|
@ -50,6 +49,7 @@ public class DebridManager: ObservableObject {
|
|||
var selectedRealDebridFile: RealDebrid.IAFile?
|
||||
var selectedRealDebridID: String?
|
||||
|
||||
// TODO: Maybe make these generic?
|
||||
// RealDebrid cloud variables
|
||||
@Published var realDebridCloudTorrents: [RealDebrid.UserTorrentsResponse] = []
|
||||
@Published var realDebridCloudDownloads: [RealDebrid.UserDownloadsResponse] = []
|
||||
|
|
@ -479,10 +479,13 @@ public class DebridManager: ObservableObject {
|
|||
public func fetchDebridDownload(magnet: Magnet?, cloudInfo: String? = nil) async {
|
||||
defer {
|
||||
currentDebridTask = nil
|
||||
showLoadingProgress = false
|
||||
toastModel?.hideIndeterminateToast()
|
||||
}
|
||||
|
||||
showLoadingProgress = true
|
||||
toastModel?.updateIndeterminateToast("Loading content", cancelAction: {
|
||||
self.currentDebridTask?.cancel()
|
||||
self.currentDebridTask = nil
|
||||
})
|
||||
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
|
|
@ -557,7 +560,7 @@ public class DebridManager: ObservableObject {
|
|||
await deleteRdTorrent(torrentID: selectedRealDebridID, presentError: false)
|
||||
}
|
||||
|
||||
showLoadingProgress = false
|
||||
toastModel?.hideIndeterminateToast()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,30 +7,38 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
enum ViewTab {
|
||||
case search
|
||||
case sources
|
||||
case settings
|
||||
case library
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class NavigationViewModel: ObservableObject {
|
||||
public class NavigationViewModel: ObservableObject {
|
||||
var toastModel: ToastViewModel?
|
||||
|
||||
// Used between SearchResultsView and MagnetChoiceView
|
||||
enum ChoiceSheetType: Identifiable {
|
||||
var id: Int {
|
||||
public enum ChoiceSheetType: Identifiable {
|
||||
public var id: Int {
|
||||
hashValue
|
||||
}
|
||||
|
||||
case magnet
|
||||
case action
|
||||
case batch
|
||||
case activity
|
||||
}
|
||||
|
||||
@Published var isEditingSearch: Bool = false
|
||||
@Published var isSearching: Bool = false
|
||||
enum ViewTab {
|
||||
case search
|
||||
case plugins
|
||||
case settings
|
||||
case library
|
||||
}
|
||||
|
||||
enum LibraryPickerSegment {
|
||||
case bookmarks
|
||||
case history
|
||||
case debridCloud
|
||||
}
|
||||
|
||||
enum PluginPickerSegment {
|
||||
case sources
|
||||
case actions
|
||||
}
|
||||
|
||||
@Published var selectedMagnet: Magnet?
|
||||
@Published var selectedHistoryInfo: HistoryEntryJson?
|
||||
|
|
@ -49,82 +57,13 @@ class NavigationViewModel: ObservableObject {
|
|||
@Published var showLocalActivitySheet = false
|
||||
|
||||
@Published var selectedTab: ViewTab = .search
|
||||
@Published var showSearchProgress: Bool = false
|
||||
|
||||
// Used between SourceListView and SourceSettingsView
|
||||
@Published var showSourceSettings: Bool = false
|
||||
var selectedSource: Source?
|
||||
|
||||
@Published var showSourceListEditor: Bool = false
|
||||
var selectedSourceList: SourceList?
|
||||
|
||||
@AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none
|
||||
@AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none
|
||||
|
||||
public func runDebridAction(urlString: String, _ action: DefaultDebridActionType? = nil) {
|
||||
let selectedAction = action ?? defaultDebridAction
|
||||
|
||||
switch selectedAction {
|
||||
case .none:
|
||||
currentChoiceSheet = .magnet
|
||||
case .outplayer:
|
||||
if let downloadUrl = URL(string: "outplayer://\(urlString)") {
|
||||
UIApplication.shared.open(downloadUrl)
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not create an Outplayer URL")
|
||||
}
|
||||
case .vlc:
|
||||
if let downloadUrl = URL(string: "vlc://\(urlString)") {
|
||||
UIApplication.shared.open(downloadUrl)
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not create a VLC URL")
|
||||
}
|
||||
case .infuse:
|
||||
if let downloadUrl = URL(string: "infuse://x-callback-url/play?url=\(urlString)") {
|
||||
UIApplication.shared.open(downloadUrl)
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not create a Infuse URL")
|
||||
}
|
||||
case .shareDownload:
|
||||
if let downloadUrl = URL(string: urlString), currentChoiceSheet == nil {
|
||||
activityItems = [downloadUrl]
|
||||
currentChoiceSheet = .activity
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not create object for sharing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func runMagnetAction(magnet: Magnet?, _ action: DefaultMagnetActionType? = nil) {
|
||||
// Fall back to selected magnet if the provided magnet is nil
|
||||
let magnet = magnet ?? selectedMagnet
|
||||
guard let magnetLink = magnet?.link else {
|
||||
toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.")
|
||||
print("Magnet action error: The magnet link is invalid.")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let selectedAction = action ?? defaultMagnetAction
|
||||
|
||||
switch selectedAction {
|
||||
case .none:
|
||||
currentChoiceSheet = .magnet
|
||||
case .webtor:
|
||||
if let url = URL(string: "https://webtor.io/#/show?magnet=\(magnetLink)") {
|
||||
UIApplication.shared.open(url)
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not create a WebTor URL")
|
||||
}
|
||||
case .shareMagnet:
|
||||
if let magnetUrl = URL(string: magnetLink),
|
||||
currentChoiceSheet == nil
|
||||
{
|
||||
activityItems = [magnetUrl]
|
||||
currentChoiceSheet = .activity
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not create object for sharing")
|
||||
}
|
||||
}
|
||||
}
|
||||
@Published var libraryPickerSelection: LibraryPickerSegment = .bookmarks
|
||||
@Published var pluginPickerSelection: PluginPickerSegment = .sources
|
||||
}
|
||||
|
|
|
|||
685
Ferrite/ViewModels/PluginManager.swift
Normal file
685
Ferrite/ViewModels/PluginManager.swift
Normal file
|
|
@ -0,0 +1,685 @@
|
|||
//
|
||||
// SourceManager.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/25/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public class PluginManager: ObservableObject {
|
||||
var toastModel: ToastViewModel?
|
||||
|
||||
@Published var availableSources: [SourceJson] = []
|
||||
@Published var availableActions: [ActionJson] = []
|
||||
|
||||
@Published var showBrokenDefaultActionAlert = false
|
||||
|
||||
@MainActor
|
||||
public func fetchPluginsFromUrl() async {
|
||||
let pluginListRequest = PluginList.fetchRequest()
|
||||
do {
|
||||
let pluginLists = try PersistenceController.shared.backgroundContext.fetch(pluginListRequest)
|
||||
|
||||
// Clean availablePlugin arrays for repopulation
|
||||
availableSources = []
|
||||
availableActions = []
|
||||
|
||||
for pluginList in pluginLists {
|
||||
guard let url = URL(string: pluginList.urlString) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Always get the up-to-date source list
|
||||
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
let pluginResponse = try JSONDecoder().decode(PluginListJson.self, from: data)
|
||||
|
||||
if let sources = pluginResponse.sources {
|
||||
// Faster and more performant to map instead of a for loop
|
||||
availableSources += sources.compactMap { inputJson in
|
||||
if checkAppVersion(minVersion: inputJson.minVersion) {
|
||||
return SourceJson(
|
||||
name: inputJson.name,
|
||||
version: inputJson.version,
|
||||
minVersion: inputJson.minVersion,
|
||||
baseUrl: inputJson.baseUrl,
|
||||
fallbackUrls: inputJson.fallbackUrls,
|
||||
dynamicBaseUrl: inputJson.dynamicBaseUrl,
|
||||
trackers: inputJson.trackers,
|
||||
api: inputJson.api,
|
||||
jsonParser: inputJson.jsonParser,
|
||||
rssParser: inputJson.rssParser,
|
||||
htmlParser: inputJson.htmlParser,
|
||||
author: pluginList.author,
|
||||
listId: pluginList.id,
|
||||
tags: inputJson.tags
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let actions = pluginResponse.actions {
|
||||
availableActions += actions.compactMap { inputJson in
|
||||
if checkAppVersion(minVersion: inputJson.minVersion) {
|
||||
return ActionJson(
|
||||
name: inputJson.name,
|
||||
version: inputJson.version,
|
||||
minVersion: inputJson.minVersion,
|
||||
requires: inputJson.requires,
|
||||
deeplink: inputJson.deeplink,
|
||||
author: pluginList.author,
|
||||
listId: pluginList.id,
|
||||
tags: inputJson.tags
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
let error = error as NSError
|
||||
if error.code != -999 {
|
||||
toastModel?.updateToastDescription("Plugin fetch error: \(error)")
|
||||
}
|
||||
|
||||
print("Plugin fetch error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// forType required to guide generic inferences
|
||||
func fetchFilteredPlugins<P: Plugin, PJ: PluginJson>(
|
||||
forType: PJ.Type,
|
||||
installedPlugins: FetchedResults<P>,
|
||||
searchText: String
|
||||
) -> [PJ] {
|
||||
let availablePlugins: [PJ] = fetchCastedPlugins(forType)
|
||||
|
||||
return availablePlugins
|
||||
.filter { availablePlugin in
|
||||
let pluginExists = installedPlugins.contains(where: {
|
||||
availablePlugin.name == $0.name &&
|
||||
availablePlugin.listId == $0.listId &&
|
||||
availablePlugin.author == $0.author
|
||||
})
|
||||
|
||||
if searchText.isEmpty {
|
||||
return !pluginExists
|
||||
} else {
|
||||
return !pluginExists && availablePlugin.name.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchUpdatedPlugins<P: Plugin, PJ: PluginJson>(
|
||||
forType: PJ.Type,
|
||||
installedPlugins: FetchedResults<P>,
|
||||
searchText: String
|
||||
) -> [PJ] {
|
||||
var updatedPlugins: [PJ] = []
|
||||
let availablePlugins: [PJ] = fetchCastedPlugins(forType)
|
||||
|
||||
for plugin in installedPlugins {
|
||||
if let availablePlugin = availablePlugins.first(where: {
|
||||
plugin.listId == $0.listId &&
|
||||
plugin.name == $0.name &&
|
||||
plugin.author == $0.author
|
||||
}),
|
||||
availablePlugin.version > plugin.version
|
||||
{
|
||||
updatedPlugins.append(availablePlugin)
|
||||
}
|
||||
}
|
||||
|
||||
return updatedPlugins
|
||||
.filter {
|
||||
searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
|
||||
func fetchCastedPlugins<PJ: PluginJson>(_ forType: PJ.Type) -> [PJ] {
|
||||
switch String(describing: PJ.self) {
|
||||
case "SourceJson":
|
||||
return availableSources as? [PJ] ?? []
|
||||
case "ActionJson":
|
||||
return availableActions as? [PJ] ?? []
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the current app version is supported by the source
|
||||
func checkAppVersion(minVersion: String?) -> Bool {
|
||||
// If there's no min version, assume that every version is supported
|
||||
guard let minVersion else {
|
||||
return true
|
||||
}
|
||||
|
||||
return Application.shared.appVersion >= minVersion
|
||||
}
|
||||
|
||||
// Fetches sources using the background context
|
||||
public func fetchInstalledSources() -> [Source] {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
if let sources = try? backgroundContext.fetch(Source.fetchRequest()) {
|
||||
return sources.compactMap { $0 }
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func runDebridAction(urlString: String?, currentChoiceSheet: inout NavigationViewModel.ChoiceSheetType?) {
|
||||
let context = PersistenceController.shared.backgroundContext
|
||||
|
||||
if
|
||||
let defaultDebridActionName = UserDefaults.standard.string(forKey: "Actions.DefaultDebridName"),
|
||||
let defaultDebridActionList = UserDefaults.standard.string(forKey: "Actions.DefaultDebridList")
|
||||
{
|
||||
let actionFetchRequest = Action.fetchRequest()
|
||||
actionFetchRequest.fetchLimit = 1
|
||||
actionFetchRequest.predicate = NSPredicate(format: "name == %@ AND listId == %@", defaultDebridActionName, defaultDebridActionList)
|
||||
|
||||
if let fetchedAction = try? context.fetch(actionFetchRequest).first {
|
||||
runDeeplinkAction(fetchedAction, urlString: urlString)
|
||||
} else {
|
||||
currentChoiceSheet = .action
|
||||
UserDefaults.standard.set(nil, forKey: "Actions.DefaultDebridName")
|
||||
UserDefaults.standard.set(nil, forKey: "Action.DefaultDebridList")
|
||||
|
||||
showBrokenDefaultActionAlert.toggle()
|
||||
}
|
||||
} else {
|
||||
currentChoiceSheet = .action
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func runMagnetAction(urlString: String?, currentChoiceSheet: inout NavigationViewModel.ChoiceSheetType?) {
|
||||
let context = PersistenceController.shared.backgroundContext
|
||||
|
||||
if
|
||||
let defaultMagnetActionName = UserDefaults.standard.string(forKey: "Actions.DefaultMagnetName"),
|
||||
let defaultMagnetActionList = UserDefaults.standard.string(forKey: "Actions.DefaultMagnetList")
|
||||
{
|
||||
let actionFetchRequest = Action.fetchRequest()
|
||||
actionFetchRequest.fetchLimit = 1
|
||||
actionFetchRequest.predicate = NSPredicate(format: "name == %@ AND listId == %@", defaultMagnetActionName, defaultMagnetActionList)
|
||||
|
||||
if let fetchedAction = try? context.fetch(actionFetchRequest).first {
|
||||
runDeeplinkAction(fetchedAction, urlString: urlString)
|
||||
} else {
|
||||
currentChoiceSheet = .action
|
||||
UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetName")
|
||||
UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetList")
|
||||
|
||||
showBrokenDefaultActionAlert.toggle()
|
||||
}
|
||||
} else {
|
||||
currentChoiceSheet = .action
|
||||
}
|
||||
}
|
||||
|
||||
// The iOS version of Ferrite only runs deeplink actions
|
||||
@MainActor
|
||||
public func runDeeplinkAction(_ action: Action, urlString: String?) {
|
||||
guard let deeplink = action.deeplink, let urlString else {
|
||||
toastModel?.updateToastDescription("Could not run action: \(action.name) since there is no deeplink to execute. Contact the action dev!")
|
||||
print("Could not run action: \(action.name) since there is no deeplink to execute.")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let playbackUrl = URL(string: deeplink.replacingOccurrences(of: "{link}", with: urlString))
|
||||
|
||||
if let playbackUrl {
|
||||
UIApplication.shared.open(playbackUrl)
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not run action: \(action.name) because the created deeplink was invalid. Contact the action dev!")
|
||||
print("Could not run action: \(action.name) because the created deeplink (\(String(describing: playbackUrl))) was invalid")
|
||||
}
|
||||
}
|
||||
|
||||
public func installAction(actionJson: ActionJson?, doUpsert: Bool = false) async {
|
||||
guard let actionJson else {
|
||||
await toastModel?.updateToastDescription("Action addition error: No action present. Contact the app dev!")
|
||||
return
|
||||
}
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
if actionJson.requires.count < 1 {
|
||||
await toastModel?.updateToastDescription("Action addition error: actions must require an input. Please contact the action dev!")
|
||||
print("Action name \(actionJson.name) does not have a requires parameter")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
guard let deeplink = actionJson.deeplink else {
|
||||
await toastModel?.updateToastDescription("Action addition error: only deeplink actions can be added to Ferrite iOS. Please contact the action dev!")
|
||||
print("Action name \(actionJson.name) did not have a deeplink")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let existingActionRequest = Action.fetchRequest()
|
||||
existingActionRequest.predicate = NSPredicate(format: "name == %@", actionJson.name)
|
||||
existingActionRequest.fetchLimit = 1
|
||||
|
||||
if let existingAction = try? backgroundContext.fetch(existingActionRequest).first {
|
||||
if doUpsert {
|
||||
PersistenceController.shared.delete(existingAction, context: backgroundContext)
|
||||
} else {
|
||||
await toastModel?.updateToastDescription("Could not install action with name \(actionJson.name) because it is already installed")
|
||||
print("Action name \(actionJson.name) already exists in user's DB")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let newAction = Action(context: backgroundContext)
|
||||
newAction.id = UUID()
|
||||
newAction.name = actionJson.name
|
||||
newAction.version = actionJson.version
|
||||
newAction.author = actionJson.author ?? "Unknown"
|
||||
newAction.listId = actionJson.listId
|
||||
newAction.requires = actionJson.requires.map { $0.rawValue }
|
||||
newAction.enabled = true
|
||||
|
||||
if let jsonTags = actionJson.tags {
|
||||
for tag in jsonTags {
|
||||
let newTag = PluginTag(context: backgroundContext)
|
||||
newTag.name = tag.name
|
||||
newTag.colorHex = tag.colorHex
|
||||
|
||||
newTag.parentAction = newAction
|
||||
}
|
||||
}
|
||||
|
||||
newAction.deeplink = deeplink
|
||||
|
||||
do {
|
||||
try backgroundContext.save()
|
||||
} catch {
|
||||
await toastModel?.updateToastDescription("Action addition error: \(error)")
|
||||
print("Action addition error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public func installSource(sourceJson: SourceJson?, doUpsert: Bool = false) async {
|
||||
guard let sourceJson else {
|
||||
await toastModel?.updateToastDescription("Source addition error: No source present. Contact the app dev!")
|
||||
return
|
||||
}
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
// If there's no base URL and it isn't dynamic, return before any transactions occur
|
||||
let dynamicBaseUrl = sourceJson.dynamicBaseUrl ?? false
|
||||
if !dynamicBaseUrl, sourceJson.baseUrl == nil {
|
||||
await toastModel?.updateToastDescription("Not adding this source because base URL parameters are malformed. Please contact the source dev.")
|
||||
print("Not adding source \(sourceJson.name) because base URL parameters are malformed")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// If a source exists, don't add the new one unless upserting
|
||||
let existingSourceRequest = Source.fetchRequest()
|
||||
existingSourceRequest.predicate = NSPredicate(format: "name == %@", sourceJson.name)
|
||||
existingSourceRequest.fetchLimit = 1
|
||||
|
||||
if let existingSource = try? backgroundContext.fetch(existingSourceRequest).first {
|
||||
if doUpsert {
|
||||
PersistenceController.shared.delete(existingSource, context: backgroundContext)
|
||||
} else {
|
||||
await toastModel?.updateToastDescription("Could not install source with name \(sourceJson.name) because it is already installed.")
|
||||
print("Source name \(sourceJson.name) already exists")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let newSource = Source(context: backgroundContext)
|
||||
newSource.id = UUID()
|
||||
newSource.name = sourceJson.name
|
||||
newSource.version = sourceJson.version
|
||||
newSource.dynamicBaseUrl = dynamicBaseUrl
|
||||
newSource.baseUrl = sourceJson.baseUrl
|
||||
newSource.fallbackUrls = dynamicBaseUrl ? nil : sourceJson.fallbackUrls
|
||||
newSource.author = sourceJson.author ?? "Unknown"
|
||||
newSource.listId = sourceJson.listId
|
||||
newSource.trackers = sourceJson.trackers
|
||||
|
||||
if let jsonTags = sourceJson.tags {
|
||||
for tag in jsonTags {
|
||||
let newTag = PluginTag(context: backgroundContext)
|
||||
newTag.name = tag.name
|
||||
newTag.colorHex = tag.colorHex
|
||||
|
||||
newTag.parentSource = newSource
|
||||
}
|
||||
}
|
||||
|
||||
if let sourceApiJson = sourceJson.api {
|
||||
addSourceApi(newSource: newSource, apiJson: sourceApiJson)
|
||||
}
|
||||
|
||||
if let jsonParserJson = sourceJson.jsonParser {
|
||||
addJsonParser(newSource: newSource, jsonParserJson: jsonParserJson)
|
||||
}
|
||||
|
||||
// Adds an RSS parser if present
|
||||
if let rssParserJson = sourceJson.rssParser {
|
||||
addRssParser(newSource: newSource, rssParserJson: rssParserJson)
|
||||
}
|
||||
|
||||
// Adds an HTML parser if present
|
||||
if let htmlParserJson = sourceJson.htmlParser {
|
||||
addHtmlParser(newSource: newSource, htmlParserJson: htmlParserJson)
|
||||
}
|
||||
|
||||
// Add an API condition as well
|
||||
if newSource.jsonParser != nil {
|
||||
newSource.preferredParser = Int16(SourcePreferredParser.siteApi.rawValue)
|
||||
} else if newSource.rssParser != nil {
|
||||
newSource.preferredParser = Int16(SourcePreferredParser.rss.rawValue)
|
||||
} else {
|
||||
newSource.preferredParser = Int16(SourcePreferredParser.scraping.rawValue)
|
||||
}
|
||||
|
||||
newSource.enabled = true
|
||||
|
||||
do {
|
||||
try backgroundContext.save()
|
||||
} catch {
|
||||
await toastModel?.updateToastDescription("Source addition error: \(error)")
|
||||
print("Source addition error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func addSourceApi(newSource: Source, apiJson: SourceApiJson) {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
let newSourceApi = SourceApi(context: backgroundContext)
|
||||
newSourceApi.apiUrl = apiJson.apiUrl
|
||||
|
||||
if let clientIdJson = apiJson.clientId {
|
||||
let newClientId = SourceApiClientId(context: backgroundContext)
|
||||
newClientId.query = clientIdJson.query
|
||||
newClientId.urlString = clientIdJson.url
|
||||
newClientId.dynamic = clientIdJson.dynamic ?? false
|
||||
newClientId.value = clientIdJson.value
|
||||
newClientId.responseType = clientIdJson.responseType?.rawValue ?? ApiCredentialResponseType.json.rawValue
|
||||
newClientId.expiryLength = clientIdJson.expiryLength ?? 0
|
||||
newClientId.timeStamp = Date()
|
||||
|
||||
newSourceApi.clientId = newClientId
|
||||
}
|
||||
|
||||
if let clientSecretJson = apiJson.clientSecret {
|
||||
let newClientSecret = SourceApiClientSecret(context: backgroundContext)
|
||||
newClientSecret.query = clientSecretJson.query
|
||||
newClientSecret.urlString = clientSecretJson.url
|
||||
newClientSecret.dynamic = clientSecretJson.dynamic ?? false
|
||||
newClientSecret.value = clientSecretJson.value
|
||||
newClientSecret.responseType = clientSecretJson.responseType?.rawValue ?? ApiCredentialResponseType.json.rawValue
|
||||
newClientSecret.expiryLength = clientSecretJson.expiryLength ?? 0
|
||||
newClientSecret.timeStamp = Date()
|
||||
|
||||
newSourceApi.clientSecret = newClientSecret
|
||||
}
|
||||
|
||||
newSource.api = newSourceApi
|
||||
}
|
||||
|
||||
func addJsonParser(newSource: Source, jsonParserJson: SourceJsonParserJson) {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
let newSourceJsonParser = SourceJsonParser(context: backgroundContext)
|
||||
newSourceJsonParser.searchUrl = jsonParserJson.searchUrl
|
||||
newSourceJsonParser.results = jsonParserJson.results
|
||||
newSourceJsonParser.subResults = jsonParserJson.subResults
|
||||
|
||||
// Tune these complex queries to the final JSON parser format
|
||||
if let magnetLinkJson = jsonParserJson.magnetLink {
|
||||
let newSourceMagnetLink = SourceMagnetLink(context: backgroundContext)
|
||||
newSourceMagnetLink.query = magnetLinkJson.query
|
||||
newSourceMagnetLink.attribute = magnetLinkJson.attribute ?? "text"
|
||||
newSourceMagnetLink.discriminator = magnetLinkJson.discriminator
|
||||
|
||||
newSourceJsonParser.magnetLink = newSourceMagnetLink
|
||||
}
|
||||
|
||||
if let magnetHashJson = jsonParserJson.magnetHash {
|
||||
let newSourceMagnetHash = SourceMagnetHash(context: backgroundContext)
|
||||
newSourceMagnetHash.query = magnetHashJson.query
|
||||
newSourceMagnetHash.attribute = magnetHashJson.attribute ?? "text"
|
||||
newSourceMagnetHash.discriminator = magnetHashJson.discriminator
|
||||
|
||||
newSourceJsonParser.magnetHash = newSourceMagnetHash
|
||||
}
|
||||
|
||||
if let subNameJson = jsonParserJson.subName {
|
||||
let newSourceSubName = SourceSubName(context: backgroundContext)
|
||||
newSourceSubName.query = subNameJson.query
|
||||
newSourceSubName.attribute = subNameJson.query
|
||||
newSourceSubName.discriminator = subNameJson.discriminator
|
||||
|
||||
newSourceJsonParser.subName = newSourceSubName
|
||||
}
|
||||
|
||||
if let titleJson = jsonParserJson.title {
|
||||
let newSourceTitle = SourceTitle(context: backgroundContext)
|
||||
newSourceTitle.query = titleJson.query
|
||||
newSourceTitle.attribute = titleJson.attribute ?? "text"
|
||||
newSourceTitle.discriminator = titleJson.discriminator
|
||||
|
||||
newSourceJsonParser.title = newSourceTitle
|
||||
}
|
||||
|
||||
if let sizeJson = jsonParserJson.size {
|
||||
let newSourceSize = SourceSize(context: backgroundContext)
|
||||
newSourceSize.query = sizeJson.query
|
||||
newSourceSize.attribute = sizeJson.attribute ?? "text"
|
||||
newSourceSize.discriminator = sizeJson.discriminator
|
||||
|
||||
newSourceJsonParser.size = newSourceSize
|
||||
}
|
||||
|
||||
if let seedLeechJson = jsonParserJson.sl {
|
||||
let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext)
|
||||
newSourceSeedLeech.seeders = seedLeechJson.seeders
|
||||
newSourceSeedLeech.leechers = seedLeechJson.leechers
|
||||
newSourceSeedLeech.combined = seedLeechJson.combined
|
||||
newSourceSeedLeech.attribute = seedLeechJson.attribute ?? "text"
|
||||
newSourceSeedLeech.discriminator = seedLeechJson.discriminator
|
||||
newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex
|
||||
newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex
|
||||
|
||||
newSourceJsonParser.seedLeech = newSourceSeedLeech
|
||||
}
|
||||
|
||||
newSource.jsonParser = newSourceJsonParser
|
||||
}
|
||||
|
||||
func addRssParser(newSource: Source, rssParserJson: SourceRssParserJson) {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
let newSourceRssParser = SourceRssParser(context: backgroundContext)
|
||||
newSourceRssParser.rssUrl = rssParserJson.rssUrl
|
||||
newSourceRssParser.searchUrl = rssParserJson.searchUrl
|
||||
newSourceRssParser.items = rssParserJson.items
|
||||
|
||||
if let magnetLinkJson = rssParserJson.magnetLink {
|
||||
let newSourceMagnetLink = SourceMagnetLink(context: backgroundContext)
|
||||
newSourceMagnetLink.query = magnetLinkJson.query
|
||||
newSourceMagnetLink.attribute = magnetLinkJson.attribute ?? "text"
|
||||
newSourceMagnetLink.discriminator = magnetLinkJson.discriminator
|
||||
newSourceMagnetLink.regex = magnetLinkJson.regex
|
||||
|
||||
newSourceRssParser.magnetLink = newSourceMagnetLink
|
||||
}
|
||||
|
||||
if let magnetHashJson = rssParserJson.magnetHash {
|
||||
let newSourceMagnetHash = SourceMagnetHash(context: backgroundContext)
|
||||
newSourceMagnetHash.query = magnetHashJson.query
|
||||
newSourceMagnetHash.attribute = magnetHashJson.attribute ?? "text"
|
||||
newSourceMagnetHash.discriminator = magnetHashJson.discriminator
|
||||
newSourceMagnetHash.regex = magnetHashJson.regex
|
||||
|
||||
newSourceRssParser.magnetHash = newSourceMagnetHash
|
||||
}
|
||||
|
||||
if let subNameJson = rssParserJson.subName {
|
||||
let newSourceSubName = SourceSubName(context: backgroundContext)
|
||||
newSourceSubName.query = subNameJson.query
|
||||
newSourceSubName.attribute = subNameJson.attribute ?? "text"
|
||||
newSourceSubName.discriminator = subNameJson.discriminator
|
||||
newSourceSubName.regex = subNameJson.regex
|
||||
|
||||
newSourceRssParser.subName = newSourceSubName
|
||||
}
|
||||
|
||||
if let titleJson = rssParserJson.title {
|
||||
let newSourceTitle = SourceTitle(context: backgroundContext)
|
||||
newSourceTitle.query = titleJson.query
|
||||
newSourceTitle.attribute = titleJson.attribute ?? "text"
|
||||
newSourceTitle.discriminator = titleJson.discriminator
|
||||
newSourceTitle.regex = titleJson.regex
|
||||
|
||||
newSourceRssParser.title = newSourceTitle
|
||||
}
|
||||
|
||||
if let sizeJson = rssParserJson.size {
|
||||
let newSourceSize = SourceSize(context: backgroundContext)
|
||||
newSourceSize.query = sizeJson.query
|
||||
newSourceSize.attribute = sizeJson.attribute ?? "text"
|
||||
newSourceSize.discriminator = sizeJson.discriminator
|
||||
newSourceSize.regex = sizeJson.regex
|
||||
|
||||
newSourceRssParser.size = newSourceSize
|
||||
}
|
||||
|
||||
if let seedLeechJson = rssParserJson.sl {
|
||||
let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext)
|
||||
newSourceSeedLeech.seeders = seedLeechJson.seeders
|
||||
newSourceSeedLeech.leechers = seedLeechJson.leechers
|
||||
newSourceSeedLeech.combined = seedLeechJson.combined
|
||||
newSourceSeedLeech.attribute = seedLeechJson.attribute ?? "text"
|
||||
newSourceSeedLeech.discriminator = seedLeechJson.discriminator
|
||||
newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex
|
||||
newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex
|
||||
|
||||
newSourceRssParser.seedLeech = newSourceSeedLeech
|
||||
}
|
||||
|
||||
newSource.rssParser = newSourceRssParser
|
||||
}
|
||||
|
||||
func addHtmlParser(newSource: Source, htmlParserJson: SourceHtmlParserJson) {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
let newSourceHtmlParser = SourceHtmlParser(context: backgroundContext)
|
||||
newSourceHtmlParser.searchUrl = htmlParserJson.searchUrl
|
||||
newSourceHtmlParser.rows = htmlParserJson.rows
|
||||
|
||||
if let subNameJson = htmlParserJson.subName {
|
||||
let newSourceSubName = SourceSubName(context: backgroundContext)
|
||||
newSourceSubName.query = subNameJson.query
|
||||
newSourceSubName.attribute = subNameJson.attribute ?? "text"
|
||||
newSourceSubName.regex = subNameJson.regex
|
||||
|
||||
newSourceHtmlParser.subName = newSourceSubName
|
||||
}
|
||||
|
||||
// Adds a title complex query if present
|
||||
if let titleJson = htmlParserJson.title {
|
||||
let newSourceTitle = SourceTitle(context: backgroundContext)
|
||||
newSourceTitle.query = titleJson.query
|
||||
newSourceTitle.attribute = titleJson.attribute ?? "text"
|
||||
newSourceTitle.regex = titleJson.regex
|
||||
|
||||
newSourceHtmlParser.title = newSourceTitle
|
||||
}
|
||||
|
||||
// Adds a size complex query if present
|
||||
if let sizeJson = htmlParserJson.size {
|
||||
let newSourceSize = SourceSize(context: backgroundContext)
|
||||
newSourceSize.query = sizeJson.query
|
||||
newSourceSize.attribute = sizeJson.attribute ?? "text"
|
||||
newSourceSize.regex = sizeJson.regex
|
||||
|
||||
newSourceHtmlParser.size = newSourceSize
|
||||
}
|
||||
|
||||
if let seedLeechJson = htmlParserJson.sl {
|
||||
let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext)
|
||||
newSourceSeedLeech.seeders = seedLeechJson.seeders
|
||||
newSourceSeedLeech.leechers = seedLeechJson.leechers
|
||||
newSourceSeedLeech.combined = seedLeechJson.combined
|
||||
newSourceSeedLeech.attribute = seedLeechJson.attribute ?? "text"
|
||||
newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex
|
||||
newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex
|
||||
|
||||
newSourceHtmlParser.seedLeech = newSourceSeedLeech
|
||||
}
|
||||
|
||||
// Adds a magnet complex query and its unique properties
|
||||
let newSourceMagnet = SourceMagnetLink(context: backgroundContext)
|
||||
newSourceMagnet.externalLinkQuery = htmlParserJson.magnet.externalLinkQuery
|
||||
newSourceMagnet.query = htmlParserJson.magnet.query
|
||||
newSourceMagnet.attribute = htmlParserJson.magnet.attribute
|
||||
newSourceMagnet.regex = htmlParserJson.magnet.regex
|
||||
|
||||
newSourceHtmlParser.magnetLink = newSourceMagnet
|
||||
|
||||
newSource.htmlParser = newSourceHtmlParser
|
||||
}
|
||||
|
||||
// Adds a plugin list
|
||||
// Can move this to PersistenceController if needed
|
||||
public func addPluginList(_ url: String, isSheet: Bool = false, existingPluginList: PluginList? = nil) async throws {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
if url.isEmpty || URL(string: url) == nil {
|
||||
throw PluginManagerError.ListAddition(description: "The provided source list is invalid. Please check if the URL is formatted properly.")
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: url)!))
|
||||
let rawResponse = try JSONDecoder().decode(PluginListJson.self, from: data)
|
||||
|
||||
if let existingPluginList {
|
||||
existingPluginList.urlString = url
|
||||
existingPluginList.name = rawResponse.name
|
||||
existingPluginList.author = rawResponse.author
|
||||
|
||||
try PersistenceController.shared.container.viewContext.save()
|
||||
} else {
|
||||
let pluginListRequest = PluginList.fetchRequest()
|
||||
let urlPredicate = NSPredicate(format: "urlString == %@", url)
|
||||
let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", rawResponse.author, rawResponse.name)
|
||||
pluginListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate])
|
||||
pluginListRequest.fetchLimit = 1
|
||||
|
||||
if let existingPluginList = try? backgroundContext.fetch(pluginListRequest).first, !isSheet {
|
||||
PersistenceController.shared.delete(existingPluginList, context: backgroundContext)
|
||||
} else if isSheet {
|
||||
throw PluginManagerError.ListAddition(description: "An existing plugin list with this information was found. Please try editing an existing plugin list instead.")
|
||||
}
|
||||
|
||||
let newPluginList = PluginList(context: backgroundContext)
|
||||
newPluginList.id = UUID()
|
||||
newPluginList.urlString = url
|
||||
newPluginList.name = rawResponse.name
|
||||
newPluginList.author = rawResponse.author
|
||||
|
||||
try backgroundContext.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,13 +18,23 @@ class ScrapingViewModel: ObservableObject {
|
|||
|
||||
var runningSearchTask: Task<Void, Error>?
|
||||
@Published var searchResults: [SearchResult] = []
|
||||
@Published var searchText: String = ""
|
||||
@Published var filteredSource: Source?
|
||||
@Published var currentSourceName: String?
|
||||
|
||||
// Only add results with valid magnet hashes to the search results array
|
||||
@MainActor
|
||||
func updateSearchResults(newResults: [SearchResult]) {
|
||||
searchResults = newResults
|
||||
searchResults += newResults.filter { $0.magnet.hash != nil }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func clearSearchResults() {
|
||||
searchResults = []
|
||||
}
|
||||
|
||||
func cancelCurrentTask() {
|
||||
runningSearchTask?.cancel()
|
||||
runningSearchTask = nil
|
||||
}
|
||||
|
||||
// Utility function to print source specific errors
|
||||
|
|
@ -38,7 +48,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
print(newDescription)
|
||||
}
|
||||
|
||||
public func scanSources(sources: [Source]) async {
|
||||
public func scanSources(sources: [Source], searchText: String) async {
|
||||
if sources.isEmpty {
|
||||
await toastModel?.updateToastDescription("There are no sources to search!", newToastType: .info)
|
||||
|
||||
|
|
@ -46,13 +56,20 @@ class ScrapingViewModel: ObservableObject {
|
|||
return
|
||||
}
|
||||
|
||||
var tempResults: [SearchResult] = []
|
||||
await clearSearchResults()
|
||||
|
||||
await toastModel?.updateIndeterminateToast("Loading", cancelAction: {
|
||||
self.cancelCurrentTask()
|
||||
})
|
||||
|
||||
for source in sources {
|
||||
// If the search is cancelled, return
|
||||
if let runningSearchTask, runningSearchTask.isCancelled {
|
||||
return
|
||||
}
|
||||
|
||||
if source.enabled {
|
||||
Task { @MainActor in
|
||||
currentSourceName = source.name
|
||||
}
|
||||
await toastModel?.updateIndeterminateToast("Loading \(source.name)", cancelAction: nil)
|
||||
|
||||
guard let baseUrl = source.baseUrl else {
|
||||
await toastModel?.updateToastDescription("The base URL could not be found for source \(source.name)")
|
||||
|
|
@ -86,7 +103,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
let html = String(data: data, encoding: .utf8)
|
||||
{
|
||||
let sourceResults = await scrapeHtml(source: source, baseUrl: baseUrl, html: html)
|
||||
tempResults += sourceResults
|
||||
await updateSearchResults(newResults: sourceResults)
|
||||
}
|
||||
}
|
||||
case .rss:
|
||||
|
|
@ -111,7 +128,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
let rss = String(data: data, encoding: .utf8)
|
||||
{
|
||||
let sourceResults = await scrapeRss(source: source, rss: rss)
|
||||
tempResults += sourceResults
|
||||
await updateSearchResults(newResults: sourceResults)
|
||||
}
|
||||
}
|
||||
case .siteApi:
|
||||
|
|
@ -155,7 +172,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
|
||||
if let data {
|
||||
let sourceResults = await scrapeJson(source: source, jsonData: data)
|
||||
tempResults += sourceResults
|
||||
await updateSearchResults(newResults: sourceResults)
|
||||
}
|
||||
}
|
||||
case .none:
|
||||
|
|
@ -164,12 +181,10 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
// If the task is cancelled, return
|
||||
// If the search is cancelled, return
|
||||
if let searchTask = runningSearchTask, searchTask.isCancelled {
|
||||
return
|
||||
}
|
||||
|
||||
await updateSearchResults(newResults: tempResults)
|
||||
}
|
||||
|
||||
// Checks the base URL for any website data then iterates through the fallback URLs
|
||||
|
|
@ -399,6 +414,12 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
var subName: String?
|
||||
if let subNameParser = jsonParser.subName {
|
||||
let rawSubName = result[subNameParser.query.components(separatedBy: ".")].rawValue
|
||||
subName = rawSubName is NSNull ? nil : String(describing: rawSubName)
|
||||
}
|
||||
|
||||
var link: String? = existingSearchResult?.magnet.link
|
||||
if let magnetLinkParser = jsonParser.magnetLink, link == nil {
|
||||
let rawLink = result[magnetLinkParser.query.components(separatedBy: ".")].rawValue
|
||||
|
|
@ -432,7 +453,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
|
||||
let result = SearchResult(
|
||||
title: title,
|
||||
source: source.name,
|
||||
source: subName.map { "\(source.name) - \($0)" } ?? source.name,
|
||||
size: size,
|
||||
magnet: Magnet(hash: magnetHash, link: link, title: title, trackers: source.trackers),
|
||||
seeders: seeders,
|
||||
|
|
@ -462,6 +483,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
for item in items {
|
||||
//print(item)
|
||||
// Parse magnet link or translate hash
|
||||
var magnetHash: String?
|
||||
if let magnetHashParser = rssParser.magnetHash {
|
||||
|
|
@ -474,6 +496,18 @@ class ScrapingViewModel: ObservableObject {
|
|||
)
|
||||
}
|
||||
|
||||
// Fetches the subName for the source if there is one
|
||||
var subName: String?
|
||||
if let subNameParser = rssParser.subName {
|
||||
subName = try? runRssComplexQuery(
|
||||
item: item,
|
||||
query: subNameParser.query,
|
||||
attribute: subNameParser.attribute,
|
||||
discriminator: subNameParser.discriminator,
|
||||
regexString: subNameParser.regex
|
||||
)
|
||||
}
|
||||
|
||||
var title: String?
|
||||
if let titleParser = rssParser.title {
|
||||
title = try? runRssComplexQuery(
|
||||
|
|
@ -485,21 +519,15 @@ class ScrapingViewModel: ObservableObject {
|
|||
)
|
||||
}
|
||||
|
||||
var link: String?
|
||||
var href: String?
|
||||
if let magnetLinkParser = rssParser.magnetLink {
|
||||
link = try? runRssComplexQuery(
|
||||
href = try? runRssComplexQuery(
|
||||
item: item,
|
||||
query: magnetLinkParser.query,
|
||||
attribute: magnetLinkParser.attribute,
|
||||
discriminator: magnetLinkParser.discriminator,
|
||||
regexString: magnetLinkParser.regex
|
||||
)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let href = link, href.starts(with: "magnet:") else {
|
||||
continue
|
||||
}
|
||||
|
||||
var size: String?
|
||||
|
|
@ -543,7 +571,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
|
||||
let result = SearchResult(
|
||||
title: title ?? "No title",
|
||||
source: source.name,
|
||||
source: subName.map { "\(source.name) - \($0)" } ?? source.name,
|
||||
size: size ?? "",
|
||||
magnet: Magnet(hash: magnetHash, link: href, title: title, trackers: source.trackers),
|
||||
seeders: seeders,
|
||||
|
|
@ -649,10 +677,6 @@ class ScrapingViewModel: ObservableObject {
|
|||
href = link
|
||||
}
|
||||
|
||||
if !href.starts(with: "magnet:") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Fetches the episode/movie title
|
||||
var title: String?
|
||||
if let titleParser = htmlParser.title {
|
||||
|
|
@ -664,8 +688,17 @@ class ScrapingViewModel: ObservableObject {
|
|||
)
|
||||
}
|
||||
|
||||
// Fetches the torrent's size
|
||||
// TODO: Add int translation
|
||||
var subName: String?
|
||||
if let subNameParser = htmlParser.subName {
|
||||
subName = try? runHtmlComplexQuery(
|
||||
row: row,
|
||||
query: subNameParser.query,
|
||||
attribute: subNameParser.attribute,
|
||||
regexString: subNameParser.regex
|
||||
)
|
||||
}
|
||||
|
||||
// Fetches the size
|
||||
var size: String?
|
||||
if let sizeParser = htmlParser.size {
|
||||
size = try? runHtmlComplexQuery(
|
||||
|
|
@ -718,7 +751,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
|
||||
let result = SearchResult(
|
||||
title: title ?? "No title",
|
||||
source: source.name,
|
||||
source: subName.map { "\(source.name) - \($0)" } ?? source.name,
|
||||
size: size ?? "",
|
||||
magnet: Magnet(hash: nil, link: href),
|
||||
seeders: seeders,
|
||||
|
|
|
|||
|
|
@ -1,426 +0,0 @@
|
|||
//
|
||||
// SourceManager.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/25/22.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public class SourceManager: ObservableObject {
|
||||
var toastModel: ToastViewModel?
|
||||
|
||||
@Published var availableSources: [SourceJson] = []
|
||||
|
||||
var urlErrorAlertText = ""
|
||||
@Published var showUrlErrorAlert = false
|
||||
|
||||
@MainActor
|
||||
public func fetchSourcesFromUrl() async {
|
||||
let sourceListRequest = SourceList.fetchRequest()
|
||||
do {
|
||||
let sourceLists = try PersistenceController.shared.backgroundContext.fetch(sourceListRequest)
|
||||
var tempAvailableSources: [SourceJson] = []
|
||||
|
||||
for sourceList in sourceLists {
|
||||
guard let url = URL(string: sourceList.urlString) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Always get the up-to-date source list
|
||||
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
let sourceResponse = try JSONDecoder().decode(SourceListJson.self, from: data)
|
||||
|
||||
for var source in sourceResponse.sources {
|
||||
// If there is a minVersion, check and see if the source is valid
|
||||
if checkAppVersion(minVersion: source.minVersion) {
|
||||
source.author = sourceList.author
|
||||
source.listId = sourceList.id
|
||||
|
||||
tempAvailableSources.append(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
availableSources = tempAvailableSources
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchUpdatedSources(installedSources: FetchedResults<Source>) -> [SourceJson] {
|
||||
var updatedSources: [SourceJson] = []
|
||||
|
||||
for source in installedSources {
|
||||
if let availableSource = availableSources.first(where: {
|
||||
source.listId == $0.listId && source.name == $0.name && source.author == $0.author
|
||||
}),
|
||||
availableSource.version > source.version
|
||||
{
|
||||
updatedSources.append(availableSource)
|
||||
}
|
||||
}
|
||||
|
||||
return updatedSources
|
||||
}
|
||||
|
||||
// Checks if the current app version is supported by the source
|
||||
func checkAppVersion(minVersion: String?) -> Bool {
|
||||
// If there's no min version, assume that every version is supported
|
||||
guard let minVersion else {
|
||||
return true
|
||||
}
|
||||
|
||||
return Application.shared.appVersion >= minVersion
|
||||
}
|
||||
|
||||
// Fetches sources using the background context
|
||||
public func fetchInstalledSources() -> [Source] {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
if let sources = try? backgroundContext.fetch(Source.fetchRequest()) {
|
||||
return sources.compactMap { $0 }
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public func installSource(sourceJson: SourceJson, doUpsert: Bool = false) async {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
// If there's no base URL and it isn't dynamic, return before any transactions occur
|
||||
let dynamicBaseUrl = sourceJson.dynamicBaseUrl ?? false
|
||||
|
||||
if !dynamicBaseUrl, sourceJson.baseUrl == nil {
|
||||
await toastModel?.updateToastDescription("Not adding this source because base URL parameters are malformed. Please contact the source dev.")
|
||||
|
||||
print("Not adding this source because base URL parameters are malformed")
|
||||
return
|
||||
}
|
||||
|
||||
// If a source exists, don't add the new one unless upserting
|
||||
let existingSourceRequest = Source.fetchRequest()
|
||||
existingSourceRequest.predicate = NSPredicate(format: "name == %@", sourceJson.name)
|
||||
existingSourceRequest.fetchLimit = 1
|
||||
|
||||
if let existingSource = try? backgroundContext.fetch(existingSourceRequest).first {
|
||||
if doUpsert {
|
||||
PersistenceController.shared.delete(existingSource, context: backgroundContext)
|
||||
} else {
|
||||
await toastModel?.updateToastDescription("Could not install source with name \(sourceJson.name) because it is already installed.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let newSource = Source(context: backgroundContext)
|
||||
newSource.id = UUID()
|
||||
newSource.name = sourceJson.name
|
||||
newSource.version = sourceJson.version
|
||||
newSource.dynamicBaseUrl = dynamicBaseUrl
|
||||
newSource.baseUrl = sourceJson.baseUrl
|
||||
newSource.fallbackUrls = dynamicBaseUrl ? nil : sourceJson.fallbackUrls
|
||||
newSource.author = sourceJson.author ?? "Unknown"
|
||||
newSource.listId = sourceJson.listId
|
||||
newSource.trackers = sourceJson.trackers
|
||||
|
||||
if let sourceApiJson = sourceJson.api {
|
||||
addSourceApi(newSource: newSource, apiJson: sourceApiJson)
|
||||
}
|
||||
|
||||
if let jsonParserJson = sourceJson.jsonParser {
|
||||
addJsonParser(newSource: newSource, jsonParserJson: jsonParserJson)
|
||||
}
|
||||
|
||||
// Adds an RSS parser if present
|
||||
if let rssParserJson = sourceJson.rssParser {
|
||||
addRssParser(newSource: newSource, rssParserJson: rssParserJson)
|
||||
}
|
||||
|
||||
// Adds an HTML parser if present
|
||||
if let htmlParserJson = sourceJson.htmlParser {
|
||||
addHtmlParser(newSource: newSource, htmlParserJson: htmlParserJson)
|
||||
}
|
||||
|
||||
// Add an API condition as well
|
||||
if newSource.jsonParser != nil {
|
||||
newSource.preferredParser = Int16(SourcePreferredParser.siteApi.rawValue)
|
||||
} else if newSource.rssParser != nil {
|
||||
newSource.preferredParser = Int16(SourcePreferredParser.rss.rawValue)
|
||||
} else {
|
||||
newSource.preferredParser = Int16(SourcePreferredParser.scraping.rawValue)
|
||||
}
|
||||
|
||||
newSource.enabled = true
|
||||
|
||||
do {
|
||||
try backgroundContext.save()
|
||||
} catch {
|
||||
await toastModel?.updateToastDescription(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func addSourceApi(newSource: Source, apiJson: SourceApiJson) {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
let newSourceApi = SourceApi(context: backgroundContext)
|
||||
newSourceApi.apiUrl = apiJson.apiUrl
|
||||
|
||||
if let clientIdJson = apiJson.clientId {
|
||||
let newClientId = SourceApiClientId(context: backgroundContext)
|
||||
newClientId.query = clientIdJson.query
|
||||
newClientId.urlString = clientIdJson.url
|
||||
newClientId.dynamic = clientIdJson.dynamic ?? false
|
||||
newClientId.value = clientIdJson.value
|
||||
newClientId.responseType = clientIdJson.responseType?.rawValue ?? ApiCredentialResponseType.json.rawValue
|
||||
newClientId.expiryLength = clientIdJson.expiryLength ?? 0
|
||||
newClientId.timeStamp = Date()
|
||||
|
||||
newSourceApi.clientId = newClientId
|
||||
}
|
||||
|
||||
if let clientSecretJson = apiJson.clientSecret {
|
||||
let newClientSecret = SourceApiClientSecret(context: backgroundContext)
|
||||
newClientSecret.query = clientSecretJson.query
|
||||
newClientSecret.urlString = clientSecretJson.url
|
||||
newClientSecret.dynamic = clientSecretJson.dynamic ?? false
|
||||
newClientSecret.value = clientSecretJson.value
|
||||
newClientSecret.responseType = clientSecretJson.responseType?.rawValue ?? ApiCredentialResponseType.json.rawValue
|
||||
newClientSecret.expiryLength = clientSecretJson.expiryLength ?? 0
|
||||
newClientSecret.timeStamp = Date()
|
||||
|
||||
newSourceApi.clientSecret = newClientSecret
|
||||
}
|
||||
|
||||
newSource.api = newSourceApi
|
||||
}
|
||||
|
||||
func addJsonParser(newSource: Source, jsonParserJson: SourceJsonParserJson) {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
let newSourceJsonParser = SourceJsonParser(context: backgroundContext)
|
||||
newSourceJsonParser.searchUrl = jsonParserJson.searchUrl
|
||||
newSourceJsonParser.results = jsonParserJson.results
|
||||
newSourceJsonParser.subResults = jsonParserJson.subResults
|
||||
|
||||
// Tune these complex queries to the final JSON parser format
|
||||
if let magnetLinkJson = jsonParserJson.magnetLink {
|
||||
let newSourceMagnetLink = SourceMagnetLink(context: backgroundContext)
|
||||
newSourceMagnetLink.query = magnetLinkJson.query
|
||||
newSourceMagnetLink.attribute = magnetLinkJson.attribute ?? "text"
|
||||
newSourceMagnetLink.discriminator = magnetLinkJson.discriminator
|
||||
|
||||
newSourceJsonParser.magnetLink = newSourceMagnetLink
|
||||
}
|
||||
|
||||
if let magnetHashJson = jsonParserJson.magnetHash {
|
||||
let newSourceMagnetHash = SourceMagnetHash(context: backgroundContext)
|
||||
newSourceMagnetHash.query = magnetHashJson.query
|
||||
newSourceMagnetHash.attribute = magnetHashJson.attribute ?? "text"
|
||||
newSourceMagnetHash.discriminator = magnetHashJson.discriminator
|
||||
|
||||
newSourceJsonParser.magnetHash = newSourceMagnetHash
|
||||
}
|
||||
|
||||
if let titleJson = jsonParserJson.title {
|
||||
let newSourceTitle = SourceTitle(context: backgroundContext)
|
||||
newSourceTitle.query = titleJson.query
|
||||
newSourceTitle.attribute = titleJson.attribute ?? "text"
|
||||
newSourceTitle.discriminator = titleJson.discriminator
|
||||
|
||||
newSourceJsonParser.title = newSourceTitle
|
||||
}
|
||||
|
||||
if let sizeJson = jsonParserJson.size {
|
||||
let newSourceSize = SourceSize(context: backgroundContext)
|
||||
newSourceSize.query = sizeJson.query
|
||||
newSourceSize.attribute = sizeJson.attribute ?? "text"
|
||||
newSourceSize.discriminator = sizeJson.discriminator
|
||||
|
||||
newSourceJsonParser.size = newSourceSize
|
||||
}
|
||||
|
||||
if let seedLeechJson = jsonParserJson.sl {
|
||||
let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext)
|
||||
newSourceSeedLeech.seeders = seedLeechJson.seeders
|
||||
newSourceSeedLeech.leechers = seedLeechJson.leechers
|
||||
newSourceSeedLeech.combined = seedLeechJson.combined
|
||||
newSourceSeedLeech.attribute = seedLeechJson.attribute ?? "text"
|
||||
newSourceSeedLeech.discriminator = seedLeechJson.discriminator
|
||||
newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex
|
||||
newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex
|
||||
|
||||
newSourceJsonParser.seedLeech = newSourceSeedLeech
|
||||
}
|
||||
|
||||
newSource.jsonParser = newSourceJsonParser
|
||||
}
|
||||
|
||||
func addRssParser(newSource: Source, rssParserJson: SourceRssParserJson) {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
let newSourceRssParser = SourceRssParser(context: backgroundContext)
|
||||
newSourceRssParser.rssUrl = rssParserJson.rssUrl
|
||||
newSourceRssParser.searchUrl = rssParserJson.searchUrl
|
||||
newSourceRssParser.items = rssParserJson.items
|
||||
|
||||
if let magnetLinkJson = rssParserJson.magnetLink {
|
||||
let newSourceMagnetLink = SourceMagnetLink(context: backgroundContext)
|
||||
newSourceMagnetLink.query = magnetLinkJson.query
|
||||
newSourceMagnetLink.attribute = magnetLinkJson.attribute ?? "text"
|
||||
newSourceMagnetLink.discriminator = magnetLinkJson.discriminator
|
||||
|
||||
newSourceRssParser.magnetLink = newSourceMagnetLink
|
||||
}
|
||||
|
||||
if let magnetHashJson = rssParserJson.magnetHash {
|
||||
let newSourceMagnetHash = SourceMagnetHash(context: backgroundContext)
|
||||
newSourceMagnetHash.query = magnetHashJson.query
|
||||
newSourceMagnetHash.attribute = magnetHashJson.attribute ?? "text"
|
||||
newSourceMagnetHash.discriminator = magnetHashJson.discriminator
|
||||
|
||||
newSourceRssParser.magnetHash = newSourceMagnetHash
|
||||
}
|
||||
|
||||
if let titleJson = rssParserJson.title {
|
||||
let newSourceTitle = SourceTitle(context: backgroundContext)
|
||||
newSourceTitle.query = titleJson.query
|
||||
newSourceTitle.attribute = titleJson.attribute ?? "text"
|
||||
newSourceTitle.discriminator = titleJson.discriminator
|
||||
|
||||
newSourceRssParser.title = newSourceTitle
|
||||
}
|
||||
|
||||
if let sizeJson = rssParserJson.size {
|
||||
let newSourceSize = SourceSize(context: backgroundContext)
|
||||
newSourceSize.query = sizeJson.query
|
||||
newSourceSize.attribute = sizeJson.attribute ?? "text"
|
||||
newSourceSize.discriminator = sizeJson.discriminator
|
||||
|
||||
newSourceRssParser.size = newSourceSize
|
||||
}
|
||||
|
||||
if let seedLeechJson = rssParserJson.sl {
|
||||
let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext)
|
||||
newSourceSeedLeech.seeders = seedLeechJson.seeders
|
||||
newSourceSeedLeech.leechers = seedLeechJson.leechers
|
||||
newSourceSeedLeech.combined = seedLeechJson.combined
|
||||
newSourceSeedLeech.attribute = seedLeechJson.attribute ?? "text"
|
||||
newSourceSeedLeech.discriminator = seedLeechJson.discriminator
|
||||
newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex
|
||||
newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex
|
||||
|
||||
newSourceRssParser.seedLeech = newSourceSeedLeech
|
||||
}
|
||||
|
||||
newSource.rssParser = newSourceRssParser
|
||||
}
|
||||
|
||||
func addHtmlParser(newSource: Source, htmlParserJson: SourceHtmlParserJson) {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
let newSourceHtmlParser = SourceHtmlParser(context: backgroundContext)
|
||||
newSourceHtmlParser.searchUrl = htmlParserJson.searchUrl
|
||||
newSourceHtmlParser.rows = htmlParserJson.rows
|
||||
|
||||
// Adds a title complex query if present
|
||||
if let titleJson = htmlParserJson.title {
|
||||
let newSourceTitle = SourceTitle(context: backgroundContext)
|
||||
newSourceTitle.query = titleJson.query
|
||||
newSourceTitle.attribute = titleJson.attribute ?? "text"
|
||||
newSourceTitle.regex = titleJson.regex
|
||||
|
||||
newSourceHtmlParser.title = newSourceTitle
|
||||
}
|
||||
|
||||
// Adds a size complex query if present
|
||||
if let sizeJson = htmlParserJson.size {
|
||||
let newSourceSize = SourceSize(context: backgroundContext)
|
||||
newSourceSize.query = sizeJson.query
|
||||
newSourceSize.attribute = sizeJson.attribute ?? "text"
|
||||
newSourceSize.regex = sizeJson.regex
|
||||
|
||||
newSourceHtmlParser.size = newSourceSize
|
||||
}
|
||||
|
||||
if let seedLeechJson = htmlParserJson.sl {
|
||||
let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext)
|
||||
newSourceSeedLeech.seeders = seedLeechJson.seeders
|
||||
newSourceSeedLeech.leechers = seedLeechJson.leechers
|
||||
newSourceSeedLeech.combined = seedLeechJson.combined
|
||||
newSourceSeedLeech.attribute = seedLeechJson.attribute ?? "text"
|
||||
newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex
|
||||
newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex
|
||||
|
||||
newSourceHtmlParser.seedLeech = newSourceSeedLeech
|
||||
}
|
||||
|
||||
// Adds a magnet complex query and its unique properties
|
||||
let newSourceMagnet = SourceMagnetLink(context: backgroundContext)
|
||||
newSourceMagnet.externalLinkQuery = htmlParserJson.magnet.externalLinkQuery
|
||||
newSourceMagnet.query = htmlParserJson.magnet.query
|
||||
newSourceMagnet.attribute = htmlParserJson.magnet.attribute
|
||||
newSourceMagnet.regex = htmlParserJson.magnet.regex
|
||||
|
||||
newSourceHtmlParser.magnetLink = newSourceMagnet
|
||||
|
||||
newSource.htmlParser = newSourceHtmlParser
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func addSourceList(sourceUrl: String, existingSourceList: SourceList?) async -> Bool {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
if sourceUrl.isEmpty || URL(string: sourceUrl) == nil {
|
||||
urlErrorAlertText = "The provided source list is invalid. Please check if the URL is formatted properly."
|
||||
showUrlErrorAlert.toggle()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!))
|
||||
let rawResponse = try JSONDecoder().decode(SourceListJson.self, from: data)
|
||||
|
||||
if let existingSourceList {
|
||||
existingSourceList.urlString = sourceUrl
|
||||
existingSourceList.name = rawResponse.name
|
||||
existingSourceList.author = rawResponse.author
|
||||
|
||||
try PersistenceController.shared.container.viewContext.save()
|
||||
} else {
|
||||
let sourceListRequest = SourceList.fetchRequest()
|
||||
let urlPredicate = NSPredicate(format: "urlString == %@", sourceUrl)
|
||||
let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", rawResponse.author, rawResponse.name)
|
||||
sourceListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate])
|
||||
sourceListRequest.fetchLimit = 1
|
||||
|
||||
if (try? backgroundContext.fetch(sourceListRequest).first) != nil {
|
||||
urlErrorAlertText = "An existing source with this information was found. Please try editing the source list instead."
|
||||
showUrlErrorAlert.toggle()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
let newSourceUrl = SourceList(context: backgroundContext)
|
||||
newSourceUrl.id = UUID()
|
||||
newSourceUrl.urlString = sourceUrl
|
||||
newSourceUrl.name = rawResponse.name
|
||||
newSourceUrl.author = rawResponse.author
|
||||
|
||||
try backgroundContext.save()
|
||||
}
|
||||
|
||||
return true
|
||||
} catch {
|
||||
print(error)
|
||||
urlErrorAlertText = error.localizedDescription
|
||||
showUrlErrorAlert.toggle()
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,10 @@ class ToastViewModel: ObservableObject {
|
|||
|
||||
@Published var showToast: Bool = false
|
||||
|
||||
@Published var indeterminateToastDescription: String? = nil
|
||||
@Published var indeterminateCancelAction: (() -> ())? = nil
|
||||
@Published var showIndeterminateToast: Bool = false
|
||||
|
||||
public func updateToastDescription(_ description: String, newToastType: ToastType? = nil) {
|
||||
if let newToastType {
|
||||
toastType = newToastType
|
||||
|
|
@ -43,6 +47,24 @@ class ToastViewModel: ObservableObject {
|
|||
toastDescription = description
|
||||
}
|
||||
|
||||
public func updateIndeterminateToast(_ description: String, cancelAction: (() -> ())?) {
|
||||
indeterminateToastDescription = description
|
||||
|
||||
if let cancelAction {
|
||||
indeterminateCancelAction = cancelAction
|
||||
}
|
||||
|
||||
if !showIndeterminateToast {
|
||||
showIndeterminateToast = true
|
||||
}
|
||||
}
|
||||
|
||||
public func hideIndeterminateToast() {
|
||||
showIndeterminateToast = false
|
||||
indeterminateToastDescription = ""
|
||||
indeterminateCancelAction = nil
|
||||
}
|
||||
|
||||
// Default the toast type to error since the majority of toasts are errors
|
||||
@Published var toastType: ToastType = .error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ struct AlertButton: Identifiable {
|
|||
}
|
||||
|
||||
// Used for buttons with no action
|
||||
init(_ label: String = "Cancel", role: Role? = nil) {
|
||||
init(_ label: String? = nil, role: Role? = nil) {
|
||||
id = UUID()
|
||||
self.label = label
|
||||
self.label = label ?? (role == .cancel ? "Cancel" : "OK")
|
||||
action = {}
|
||||
self.role = role
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import Introspect
|
||||
|
||||
public struct Backport<Content> {
|
||||
public let content: Content
|
||||
|
|
@ -20,7 +21,12 @@ extension View {
|
|||
}
|
||||
|
||||
extension Backport where Content: View {
|
||||
@ViewBuilder func alert(isPresented: Binding<Bool>, title: String, message: String?, buttons: [AlertButton]) -> some View {
|
||||
@ViewBuilder func alert(
|
||||
isPresented: Binding<Bool>,
|
||||
title: String,
|
||||
message: String?,
|
||||
buttons: [AlertButton] = []
|
||||
) -> some View {
|
||||
if #available(iOS 15, *) {
|
||||
content
|
||||
.alert(
|
||||
|
|
@ -55,7 +61,7 @@ extension Backport where Content: View {
|
|||
return Alert(
|
||||
title: Text(title),
|
||||
message: message.map { Text($0) } ?? nil,
|
||||
dismissButton: buttons[0].toActionButton()
|
||||
dismissButton: buttons[safe: 0].map { $0.toActionButton() } ?? .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -63,7 +69,11 @@ extension Backport where Content: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func confirmationDialog(isPresented: Binding<Bool>, title: String, message: String?, buttons: [AlertButton]) -> some View {
|
||||
@ViewBuilder func confirmationDialog(
|
||||
isPresented: Binding<Bool>,
|
||||
title: String, message: String?,
|
||||
buttons: [AlertButton]
|
||||
) -> some View {
|
||||
if #available(iOS 15, *) {
|
||||
content
|
||||
.confirmationDialog(
|
||||
|
|
@ -100,4 +110,31 @@ extension Backport where Content: View {
|
|||
.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) -> ()) -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,10 @@ struct DynamicFetchRequest<T: NSManagedObject, Content: View>: View {
|
|||
}
|
||||
|
||||
init(predicate: NSPredicate?,
|
||||
sortDescriptors: [NSSortDescriptor] = [],
|
||||
@ViewBuilder content: @escaping (FetchedResults<T>) -> Content)
|
||||
{
|
||||
_fetchRequest = FetchRequest<T>(sortDescriptors: [], predicate: predicate)
|
||||
_fetchRequest = FetchRequest<T>(entity: T.entity(), sortDescriptors: sortDescriptors, predicate: predicate)
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
Ferrite/Views/CommonViews/FilterLabelView.swift
Normal file
27
Ferrite/Views/CommonViews/FilterLabelView.swift
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// FilterLabelView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/12/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct FilterLabelView: View {
|
||||
var name: String
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 4) {
|
||||
Text(name)
|
||||
.opacity(0.6)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Image(systemName: "chevron.down")
|
||||
.foregroundColor(.tertiaryLabel)
|
||||
}
|
||||
.padding(.horizontal, 9)
|
||||
.padding(.vertical, 7)
|
||||
.font(.caption, weight: .medium)
|
||||
.background(Capsule().foregroundColor(.secondarySystemFill))
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ struct IndeterminateProgressView: View {
|
|||
.offset(x: -reader.size.width * 0.6, y: 0)
|
||||
.offset(x: reader.size.width * 1.2 * self.offset, y: 0)
|
||||
.animation(.default.repeatForever().speed(0.5), value: self.offset)
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
withAnimation {
|
||||
self.offset = 1
|
||||
}
|
||||
|
|
|
|||
17
Ferrite/Views/CommonViews/LibraryHeaderView.swift
Normal file
17
Ferrite/Views/CommonViews/LibraryHeaderView.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// LibraryHeaderView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/12/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryHeaderView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@Binding var selectedSegment: LibraryPickerSegment
|
||||
var body: some View {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct ConditionalContextMenu<InternalContent: View, ID: Hashable>: ViewModifier {
|
||||
struct ConditionalContextMenuModifier<InternalContent: View, ID: Hashable>: ViewModifier {
|
||||
let internalContent: () -> InternalContent
|
||||
let id: ID
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct ConditionalId<ID: Hashable>: ViewModifier {
|
||||
struct ConditionalIdModifier<ID: Hashable>: ViewModifier {
|
||||
let id: ID
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
|
|
|
|||
68
Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift
Normal file
68
Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// SearchAppearance.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/14/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Introspect
|
||||
|
||||
struct CustomScopeBarModifier<V: View>: ViewModifier {
|
||||
let hostingContent: V
|
||||
@State private var hostingController: UIHostingController<V>?
|
||||
|
||||
// Don't use AppStorage since it causes a view update
|
||||
var autocorrectSearch: Bool {
|
||||
UserDefaults.standard.bool(forKey: "Behavior.AutocorrectSearch")
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 15, *) {
|
||||
content
|
||||
.backport.introspectSearchController { searchController in
|
||||
searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no
|
||||
searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none
|
||||
|
||||
// MARK: One-time setup
|
||||
guard hostingController == nil else { return }
|
||||
|
||||
searchController.hidesNavigationBarDuringPresentation = true
|
||||
searchController.searchBar.showsScopeBar = true
|
||||
searchController.searchBar.scopeButtonTitles = [""]
|
||||
(searchController.searchBar.value(forKey: "_scopeBar") as? UIView)?.isHidden = true
|
||||
|
||||
let hostingController = UIHostingController(rootView: hostingContent)
|
||||
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
hostingController.view.backgroundColor = .clear
|
||||
|
||||
guard let containerView = searchController.searchBar.value(forKey: "_scopeBarContainerView") as? UIView else {
|
||||
return
|
||||
}
|
||||
containerView.addSubview(hostingController.view)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
hostingController.view.widthAnchor.constraint(equalTo: containerView.widthAnchor),
|
||||
hostingController.view.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
hostingController.view.heightAnchor.constraint(equalTo: containerView.heightAnchor)
|
||||
])
|
||||
|
||||
self.hostingController = hostingController
|
||||
}
|
||||
.introspectNavigationController { navigationController in
|
||||
navigationController.navigationBar.prefersLargeTitles = true
|
||||
navigationController.navigationBar.sizeToFit()
|
||||
}
|
||||
} else {
|
||||
VStack {
|
||||
hostingContent
|
||||
content
|
||||
Spacer()
|
||||
}
|
||||
.backport.introspectSearchController { searchController in
|
||||
searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no
|
||||
searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct DisableInteraction: ViewModifier {
|
||||
struct DisableInteractionModifier: ViewModifier {
|
||||
let disabled: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct DisabledAppearance: ViewModifier {
|
||||
struct DisabledAppearanceModifier: ViewModifier {
|
||||
let disabled: Bool
|
||||
let dimmedOpacity: Double?
|
||||
let animation: Animation?
|
||||
|
|
|
|||
|
|
@ -11,17 +11,19 @@
|
|||
import Introspect
|
||||
import SwiftUI
|
||||
|
||||
struct InlinedList: ViewModifier {
|
||||
struct InlinedListModifier: ViewModifier {
|
||||
let inset: CGFloat
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 16, *) {
|
||||
content
|
||||
.introspectCollectionView { collectionView in
|
||||
collectionView.contentInset.top = -20
|
||||
collectionView.contentInset.top = inset
|
||||
}
|
||||
} else {
|
||||
content
|
||||
.introspectTableView { tableView in
|
||||
tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 20))
|
||||
tableView.contentInset.top = inset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
Ferrite/Views/CommonViews/Modifiers/ViewDidAppear.swift
Normal file
17
Ferrite/Views/CommonViews/Modifiers/ViewDidAppear.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -11,19 +11,16 @@
|
|||
import SwiftUI
|
||||
|
||||
struct NavView<Content: View>: View {
|
||||
let content: () -> Content
|
||||
init(@ViewBuilder _ content: @escaping () -> Content) {
|
||||
self.content = content
|
||||
}
|
||||
@ViewBuilder var content: Content
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16, *) {
|
||||
NavigationStack {
|
||||
content()
|
||||
content
|
||||
}
|
||||
} else {
|
||||
NavigationView {
|
||||
content()
|
||||
content
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
}
|
||||
|
|
|
|||
37
Ferrite/Views/CommonViews/SearchableContent.swift
Normal file
37
Ferrite/Views/CommonViews/SearchableContent.swift
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// SearchableContent.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/11/23.
|
||||
//
|
||||
// View to link animations together with searchbar
|
||||
// Passes through geometry proxy and last height vars for any comparison
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchableContent<Content: View>: View {
|
||||
@Binding var searching: Bool
|
||||
|
||||
@State private var lastHeight: CGFloat = 0.0
|
||||
|
||||
@ViewBuilder var content: (Bool) -> Content
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geom in
|
||||
// Return if the height has changed as a closure variable for child transactions
|
||||
content(geom.size.height != lastHeight)
|
||||
.backport.onAppear {
|
||||
lastHeight = geom.size.height
|
||||
}
|
||||
.onChange(of: geom.size.height) { newHeight in
|
||||
lastHeight = newHeight
|
||||
}
|
||||
.transaction {
|
||||
if geom.size.height != lastHeight && searching {
|
||||
$0.animation = .default.speed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Ferrite/Views/CommonViews/SectionHeaderView.swift
Normal file
20
Ferrite/Views/CommonViews/SectionHeaderView.swift
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// SectionHeaderView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/15/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SectionHeaderView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
struct SectionHeaderView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SectionHeaderView()
|
||||
}
|
||||
}
|
||||
28
Ferrite/Views/CommonViews/Tag.swift
Normal file
28
Ferrite/Views/CommonViews/Tag.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// Tag.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/7/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Tag: View {
|
||||
let name: String
|
||||
let color: Color?
|
||||
var horizontalPadding: CGFloat = 7
|
||||
var verticalPadding: CGFloat = 4
|
||||
|
||||
var body: some View {
|
||||
Text(name.capitalizingFirstLetter())
|
||||
.font(.caption)
|
||||
.opacity(0.8)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.vertical, verticalPadding)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
.foregroundColor(color.map { $0 } ?? .tertiaryLabel)
|
||||
.opacity(0.3)
|
||||
)
|
||||
}
|
||||
}
|
||||
75
Ferrite/Views/CommonViews/TestHostingView.swift
Normal file
75
Ferrite/Views/CommonViews/TestHostingView.swift
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// TestHostingView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/13/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TestHostingView: View {
|
||||
@State private var textName = "First"
|
||||
@State private var secondTextName = "First"
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
Menu {
|
||||
Picker("", selection: $textName) {
|
||||
Text("First").tag("First")
|
||||
Text("Second").tag("Second")
|
||||
Text("Third").tag("Third")
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 2) {
|
||||
Text(textName)
|
||||
.opacity(0.6)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Image(systemName: "chevron.down")
|
||||
.foregroundColor(.tertiaryLabel)
|
||||
}
|
||||
.padding(.horizontal, 9)
|
||||
.padding(.vertical, 7)
|
||||
.font(.caption, weight: .bold)
|
||||
.background(Capsule().foregroundColor(.secondarySystemFill))
|
||||
}
|
||||
.id(textName)
|
||||
.transaction {
|
||||
$0.animation = .none
|
||||
}
|
||||
|
||||
Menu {
|
||||
Picker("", selection: $secondTextName) {
|
||||
Text("First").tag("First")
|
||||
Text("Second").tag("Second")
|
||||
Text("Third").tag("Third")
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 2) {
|
||||
Text(secondTextName)
|
||||
.opacity(0.6)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Image(systemName: "chevron.down")
|
||||
.foregroundColor(.tertiaryLabel)
|
||||
}
|
||||
.padding(.horizontal, 9)
|
||||
.padding(.vertical, 7)
|
||||
.font(.caption, weight: .bold)
|
||||
.background(Capsule().foregroundColor(.secondarySystemFill))
|
||||
}
|
||||
.id(secondTextName)
|
||||
.transaction {
|
||||
$0.animation = .none
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TestHostingView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TestHostingView()
|
||||
}
|
||||
}
|
||||
|
|
@ -15,31 +15,31 @@ struct DebridLabelView: View {
|
|||
|
||||
var body: some View {
|
||||
if let selectedDebridType = debridManager.selectedDebridType {
|
||||
Text(selectedDebridType.toString(abbreviated: true))
|
||||
.fontWeight(.bold)
|
||||
.padding(2)
|
||||
.background {
|
||||
Group {
|
||||
if let magnet, cloudLinks.isEmpty {
|
||||
switch debridManager.matchMagnetHash(magnet) {
|
||||
case .full:
|
||||
Color.green
|
||||
case .partial:
|
||||
Color.orange
|
||||
case .none:
|
||||
Color.red
|
||||
}
|
||||
} else if cloudLinks.count == 1 {
|
||||
Color.green
|
||||
} else if cloudLinks.count > 1 {
|
||||
Color.orange
|
||||
} else {
|
||||
Color.red
|
||||
}
|
||||
}
|
||||
.cornerRadius(4)
|
||||
.opacity(0.5)
|
||||
}
|
||||
Tag(
|
||||
name: selectedDebridType.toString(abbreviated: true),
|
||||
color: getTagColor(),
|
||||
horizontalPadding: 5,
|
||||
verticalPadding: 3
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func getTagColor() -> Color {
|
||||
if let magnet, cloudLinks.isEmpty {
|
||||
switch debridManager.matchMagnetHash(magnet) {
|
||||
case .full:
|
||||
return Color.green
|
||||
case .partial:
|
||||
return Color.orange
|
||||
case .none:
|
||||
return Color.red
|
||||
}
|
||||
} else if cloudLinks.count == 1 {
|
||||
return Color.green
|
||||
} else if cloudLinks.count > 1 {
|
||||
return Color.orange
|
||||
} else {
|
||||
return Color.red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,17 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct DebridChoiceView: View {
|
||||
struct DebridPickerView<Content: View>: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@ViewBuilder var label: Content
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Picker("", selection: $debridManager.selectedDebridType) {
|
||||
Text("None")
|
||||
.tag(nil as DebridType?)
|
||||
|
||||
|
||||
ForEach(DebridType.allCases, id: \.self) { (debridType: DebridType) in
|
||||
if debridManager.enabledDebrids.contains(debridType) {
|
||||
Text(debridType.toString())
|
||||
|
|
@ -24,14 +26,7 @@ struct DebridChoiceView: View {
|
|||
}
|
||||
}
|
||||
} label: {
|
||||
Text(debridManager.selectedDebridType?.toString(abbreviated: true) ?? "Debrid")
|
||||
label
|
||||
}
|
||||
.animation(.none)
|
||||
}
|
||||
}
|
||||
|
||||
struct DebridChoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DebridChoiceView()
|
||||
}
|
||||
}
|
||||
|
|
@ -21,37 +21,39 @@ struct BookmarksView: View {
|
|||
@State private var bookmarkPredicate: NSPredicate?
|
||||
|
||||
var body: some View {
|
||||
DynamicFetchRequest(predicate: bookmarkPredicate) { (bookmarks: FetchedResults<Bookmark>) in
|
||||
DynamicFetchRequest(
|
||||
predicate: bookmarkPredicate,
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \Bookmark.orderNum, ascending: true)]
|
||||
) { (bookmarks: FetchedResults<Bookmark>) in
|
||||
List {
|
||||
if !bookmarks.isEmpty {
|
||||
ForEach(bookmarks, id: \.self) { bookmark in
|
||||
SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let bookmark = bookmarks[safe: index] {
|
||||
PersistenceController.shared.delete(bookmark, context: backgroundContext)
|
||||
|
||||
NotificationCenter.default.post(name: .didDeleteBookmark, object: bookmark)
|
||||
if !bookmarks.isEmpty {
|
||||
ForEach(bookmarks, id: \.self) { bookmark in
|
||||
SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let bookmark = bookmarks[safe: index] {
|
||||
PersistenceController.shared.delete(bookmark, context: backgroundContext)
|
||||
NotificationCenter.default.post(name: .didDeleteBookmark, object: bookmark)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onMove { source, destination in
|
||||
var changedBookmarks = bookmarks.map { $0 }
|
||||
.onMove { source, destination in
|
||||
var changedBookmarks = bookmarks.map { $0 }
|
||||
|
||||
changedBookmarks.move(fromOffsets: source, toOffset: destination)
|
||||
changedBookmarks.move(fromOffsets: source, toOffset: destination)
|
||||
|
||||
for reverseIndex in stride(from: changedBookmarks.count - 1, through: 0, by: -1) {
|
||||
changedBookmarks[reverseIndex].orderNum = Int16(reverseIndex)
|
||||
for reverseIndex in stride(from: changedBookmarks.count - 1, through: 0, by: -1) {
|
||||
changedBookmarks[reverseIndex].orderNum = Int16(reverseIndex)
|
||||
}
|
||||
|
||||
PersistenceController.shared.save()
|
||||
}
|
||||
|
||||
PersistenceController.shared.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
.inlinedList()
|
||||
.listStyle(.insetGrouped)
|
||||
.onAppear {
|
||||
.inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 15 : -25)
|
||||
.backport.onAppear {
|
||||
if debridManager.enabledDebrids.count > 0 {
|
||||
viewTask = Task {
|
||||
let magnets = bookmarks.compactMap {
|
||||
|
|
@ -69,7 +71,7 @@ struct BookmarksView: View {
|
|||
viewTask?.cancel()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
applyPredicate()
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import SwiftUI
|
|||
struct AllDebridCloudView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
|
|
@ -37,8 +38,11 @@ struct AllDebridCloudView: View {
|
|||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
historyInfo.url = debridManager.downloadUrl
|
||||
PersistenceController.shared.createHistory(historyInfo)
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
PersistenceController.shared.createHistory(historyInfo, performSave: true)
|
||||
pluginManager.runDebridAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -80,7 +84,7 @@ struct AllDebridCloudView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
viewTask = Task {
|
||||
await debridManager.fetchAdCloud()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import SwiftUIX
|
|||
struct PremiumizeCloudView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
|
|
@ -34,10 +35,14 @@ struct PremiumizeCloudView: View {
|
|||
name: item.name,
|
||||
url: debridManager.downloadUrl,
|
||||
source: DebridType.premiumize.toString()
|
||||
)
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
pluginManager.runDebridAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,7 +59,7 @@ struct PremiumizeCloudView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
viewTask = Task {
|
||||
await debridManager.fetchPmCloud()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import SwiftUI
|
|||
struct RealDebridCloudView: View {
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
|
|
@ -31,10 +32,14 @@ struct RealDebridCloudView: View {
|
|||
name: downloadResponse.filename,
|
||||
url: downloadResponse.download,
|
||||
source: DebridType.realDebrid.toString()
|
||||
)
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
pluginManager.runDebridAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
||||
)
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
|
|
@ -69,9 +74,12 @@ struct RealDebridCloudView: View {
|
|||
await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink)
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
historyInfo.url = debridManager.downloadUrl
|
||||
PersistenceController.shared.createHistory(historyInfo)
|
||||
PersistenceController.shared.createHistory(historyInfo, performSave: true)
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
pluginManager.runDebridAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -116,7 +124,7 @@ struct RealDebridCloudView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
viewTask = Task {
|
||||
await debridManager.fetchRdCloud()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ struct HistoryButtonView: View {
|
|||
@EnvironmentObject var toastModel: ToastViewModel
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
let entry: HistoryEntry
|
||||
|
||||
|
|
@ -23,14 +24,20 @@ struct HistoryButtonView: View {
|
|||
if url.starts(with: "https://") {
|
||||
Task {
|
||||
debridManager.downloadUrl = url
|
||||
navModel.runDebridAction(urlString: url)
|
||||
pluginManager.runDebridAction(
|
||||
urlString: url,
|
||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
||||
)
|
||||
|
||||
if navModel.currentChoiceSheet != .magnet {
|
||||
if navModel.currentChoiceSheet != .action {
|
||||
debridManager.downloadUrl = ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
navModel.runMagnetAction(magnet: Magnet(hash: nil, link: url))
|
||||
pluginManager.runMagnetAction(
|
||||
urlString: url,
|
||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
||||
)
|
||||
}
|
||||
} else {
|
||||
toastModel.updateToastDescription("URL invalid. Cannot load this history entry. Please delete it.")
|
||||
|
|
@ -77,4 +84,12 @@ struct HistoryButtonView: View {
|
|||
.backport.tint(.primary)
|
||||
.disableInteraction(navModel.currentChoiceSheet != nil)
|
||||
}
|
||||
|
||||
func getTagColor() -> Color {
|
||||
if let url = entry.url, url.starts(with: "https://") {
|
||||
return Color.green
|
||||
} else {
|
||||
return Color.red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ struct HistoryView: View {
|
|||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
applyPredicate()
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
|
|
@ -71,7 +71,7 @@ struct HistorySectionView: View {
|
|||
|
||||
var body: some View {
|
||||
if compareGroup(historyGroup) > 0 {
|
||||
Section(header: Text(formatter.string(from: historyGroup[0].date ?? Date()))) {
|
||||
Section(header: InlineHeader(formatter.string(from: historyGroup[0].date ?? Date()))) {
|
||||
ForEach(historyGroup, id: \.self) { history in
|
||||
ForEach(history.entryArray.filter { allEntries.contains($0) }, id: \.self) { entry in
|
||||
HistoryButtonView(entry: entry)
|
||||
|
|
|
|||
31
Ferrite/Views/ComponentViews/Library/LibraryPickerView.swift
Normal file
31
Ferrite/Views/ComponentViews/Library/LibraryPickerView.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// LibraryPickerView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/13/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryPickerView: View {
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Picker("Segments", selection: $navModel.libraryPickerSelection) {
|
||||
Text("Bookmarks").tag(NavigationViewModel.LibraryPickerSegment.bookmarks)
|
||||
Text("History").tag(NavigationViewModel.LibraryPickerSegment.history)
|
||||
|
||||
if !debridManager.enabledDebrids.isEmpty {
|
||||
Text("Cloud").tag(NavigationViewModel.LibraryPickerSegment.debridCloud)
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.horizontal, verticalSizeClass == .compact && UIDevice.current.hasNotch ? 65 : 18)
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,52 +7,60 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct InstalledSourceButtonView: View {
|
||||
struct InstalledPluginButtonView<P: Plugin>: View {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@ObservedObject var installedSource: Source
|
||||
@ObservedObject var installedPlugin: P
|
||||
|
||||
var body: some View {
|
||||
Toggle(isOn: Binding<Bool>(
|
||||
get: { installedSource.enabled },
|
||||
get: { installedPlugin.enabled },
|
||||
set: {
|
||||
installedSource.enabled = $0
|
||||
installedPlugin.enabled = $0
|
||||
PersistenceController.shared.save()
|
||||
}
|
||||
)) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
Text(installedSource.name)
|
||||
Text("v\(installedSource.version)")
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
Text(installedPlugin.name)
|
||||
Text("v\(installedPlugin.version)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("by \(installedPlugin.author)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("by \(installedSource.author)")
|
||||
.foregroundColor(.secondary)
|
||||
if let tags = installedPlugin.getTags(), !tags.isEmpty {
|
||||
PluginTagsView(tags: tags)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
.contextMenu {
|
||||
Button {
|
||||
navModel.selectedSource = installedSource
|
||||
navModel.showSourceSettings.toggle()
|
||||
} label: {
|
||||
Text("Settings")
|
||||
Image(systemName: "gear")
|
||||
if let installedSource = installedPlugin as? Source {
|
||||
Button {
|
||||
navModel.selectedSource = installedSource
|
||||
navModel.showSourceSettings.toggle()
|
||||
} label: {
|
||||
Text("Settings")
|
||||
Image(systemName: "gear")
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
Button(role: .destructive) {
|
||||
PersistenceController.shared.delete(installedSource, context: backgroundContext)
|
||||
PersistenceController.shared.delete(installedPlugin, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
PersistenceController.shared.delete(installedSource, context: backgroundContext)
|
||||
PersistenceController.shared.delete(installedPlugin, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// SourceCatalogButtonView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/5/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PluginCatalogButtonView<PJ: PluginJson>: View {
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
let availablePlugin: PJ
|
||||
let doUpsert: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
Text(availablePlugin.name)
|
||||
Text("v\(availablePlugin.version)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("by \(availablePlugin.author ?? "No author")")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if let tags = availablePlugin.getTags(), !tags.isEmpty {
|
||||
PluginTagsView(tags: tags)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Install") {
|
||||
Task {
|
||||
if let availableSource = availablePlugin as? SourceJson {
|
||||
await pluginManager.installSource(sourceJson: availableSource, doUpsert: doUpsert)
|
||||
} else if let availableAction = availablePlugin as? ActionJson {
|
||||
await pluginManager.installAction(actionJson: availableAction, doUpsert: doUpsert)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
import SwiftUI
|
||||
|
||||
struct SourceCatalogButtonView: View {
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
let availableSource: SourceJson
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ struct SourceCatalogButtonView: View {
|
|||
|
||||
Button("Install") {
|
||||
Task {
|
||||
await sourceManager.installSource(sourceJson: availableSource)
|
||||
await pluginManager.installSource(sourceJson: availableSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
78
Ferrite/Views/ComponentViews/Plugin/PluginListView.swift
Normal file
78
Ferrite/Views/ComponentViews/Plugin/PluginListView.swift
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// SourceListView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
struct PluginListView<P: Plugin, PJ: PluginJson>: View {
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
@State private var isEditingSearch = false
|
||||
@State private var isSearching = false
|
||||
|
||||
@State private var sourcePredicate: NSPredicate?
|
||||
|
||||
var body: some View {
|
||||
DynamicFetchRequest(predicate: sourcePredicate) { (installedPlugins: FetchedResults<P>) in
|
||||
List {
|
||||
if
|
||||
let filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(
|
||||
forType: PJ.self,
|
||||
installedPlugins: installedPlugins,
|
||||
searchText: searchText
|
||||
),
|
||||
!filteredUpdatedPlugins.isEmpty
|
||||
{
|
||||
Section(header: InlineHeader("Updates")) {
|
||||
ForEach(filteredUpdatedPlugins, id: \.self) { (updatedPlugin: PJ) in
|
||||
PluginCatalogButtonView(availablePlugin: updatedPlugin, doUpsert: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !installedPlugins.isEmpty {
|
||||
Section(header: InlineHeader("Installed")) {
|
||||
ForEach(installedPlugins, id: \.self) { source in
|
||||
InstalledPluginButtonView(installedPlugin: source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, doUpsert: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.inlinedList(inset: 0)
|
||||
.listStyle(.insetGrouped)
|
||||
.sheet(isPresented: $navModel.showSourceSettings) {
|
||||
if String(describing: P.self) == "Source" {
|
||||
SourceSettingsView()
|
||||
.environmentObject(navModel)
|
||||
}
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
sourcePredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Ferrite/Views/ComponentViews/Plugin/PluginPickerView.swift
Normal file
27
Ferrite/Views/ComponentViews/Plugin/PluginPickerView.swift
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// PluginPickerView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/14/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PluginPickerView: View {
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@State private var textName = ""
|
||||
@State private var secondTextName = ""
|
||||
|
||||
var body: some View {
|
||||
Picker("Segments", selection: $navModel.pluginPickerSelection) {
|
||||
Text("Sources").tag(NavigationViewModel.PluginPickerSegment.sources)
|
||||
Text("Actions").tag(NavigationViewModel.PluginPickerSegment.actions)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.horizontal, verticalSizeClass == .compact && UIDevice.current.hasNotch ? 65 : 18)
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
}
|
||||
22
Ferrite/Views/ComponentViews/Plugin/PluginTagsView.swift
Normal file
22
Ferrite/Views/ComponentViews/Plugin/PluginTagsView.swift
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// PluginTagView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/7/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PluginTagsView: View {
|
||||
let tags: [PluginTagJson]
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
ForEach(tags, id: \.self) { tag in
|
||||
Tag(name: tag.name, color: tag.colorHex.map { Color(hexadecimal: $0) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,33 +12,46 @@ struct SourceSettingsView: View {
|
|||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@FetchRequest(
|
||||
entity: PluginList.entity(),
|
||||
sortDescriptors: []
|
||||
) var pluginLists: FetchedResults<PluginList>
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
List {
|
||||
if let selectedSource = navModel.selectedSource {
|
||||
Section(header: InlineHeader("Info")) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
Text(selectedSource.name)
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
Text(selectedSource.name)
|
||||
|
||||
Text("v\(selectedSource.version)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("by \(selectedSource.author)")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Group {
|
||||
Text("ID: \(selectedSource.id)")
|
||||
|
||||
if let listId = selectedSource.listId {
|
||||
Text("List ID: \(listId)")
|
||||
} else {
|
||||
Text("No list ID found. This source should be removed.")
|
||||
Text("v\(selectedSource.version)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("by \(selectedSource.author)")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Group {
|
||||
Text("ID: \(selectedSource.id)")
|
||||
|
||||
if let pluginList = pluginLists.first(where: { $0.id == selectedSource.listId })
|
||||
{
|
||||
Text("List: \(pluginList.name)")
|
||||
Text("List ID: \(pluginList.id.uuidString)")
|
||||
} else {
|
||||
Text("No plugin list found. This source should be removed.")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
if let tags = selectedSource.getTags(), !tags.isEmpty {
|
||||
PluginTagsView(tags: tags)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
|
@ -91,7 +104,7 @@ struct SourceSettingsBaseUrlView: View {
|
|||
}
|
||||
})
|
||||
.keyboardType(.URL)
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
tempBaseUrl = selectedSource.baseUrl ?? ""
|
||||
}
|
||||
}
|
||||
|
|
@ -121,7 +134,7 @@ struct SourceSettingsApiView: View {
|
|||
}
|
||||
})
|
||||
.autocapitalization(.none)
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
tempClientId = clientId.value ?? ""
|
||||
}
|
||||
}
|
||||
|
|
@ -134,7 +147,7 @@ struct SourceSettingsApiView: View {
|
|||
}
|
||||
})
|
||||
.autocapitalization(.none)
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
tempClientSecret = clientSecret.value ?? ""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// SearchFilterHeaderView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/13/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchFilterHeaderView: View {
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@FetchRequest(
|
||||
entity: Source.entity(),
|
||||
sortDescriptors: []
|
||||
) var sources: FetchedResults<Source>
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 6) {
|
||||
Menu {
|
||||
Picker("", selection: $scrapingModel.filteredSource) {
|
||||
Text("None").tag(nil as Source?)
|
||||
|
||||
ForEach(sources, id: \.self) { source in
|
||||
if let name = source.name, source.enabled {
|
||||
Text(name)
|
||||
.tag(Source?.some(source))
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
FilterLabelView(name: scrapingModel.filteredSource?.name ?? "Source")
|
||||
}
|
||||
.id(scrapingModel.filteredSource)
|
||||
|
||||
DebridPickerView() {
|
||||
FilterLabelView(name: debridManager.selectedDebridType?.toString() ?? "Debrid")
|
||||
}
|
||||
.id(debridManager.selectedDebridType)
|
||||
}
|
||||
.padding(.horizontal, verticalSizeClass == .compact ? (Application.shared.osVersion.majorVersion > 14 ? 65 : 18) : 18)
|
||||
.padding(.top, Application.shared.osVersion.majorVersion > 14 ? 0 : 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ struct SearchResultButtonView: View {
|
|||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
var result: SearchResult
|
||||
|
||||
|
|
@ -38,12 +39,16 @@ struct SearchResultButtonView: View {
|
|||
name: result.title,
|
||||
url: debridManager.downloadUrl,
|
||||
source: result.source
|
||||
)
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
pluginManager.runDebridAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
||||
)
|
||||
|
||||
if navModel.currentChoiceSheet != .magnet {
|
||||
if navModel.currentChoiceSheet != .action {
|
||||
debridManager.downloadUrl = ""
|
||||
}
|
||||
}
|
||||
|
|
@ -63,10 +68,14 @@ struct SearchResultButtonView: View {
|
|||
name: result.title,
|
||||
url: result.magnet.link,
|
||||
source: result.source
|
||||
)
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
navModel.runMagnetAction(magnet: result.magnet)
|
||||
pluginManager.runMagnetAction(
|
||||
urlString: result.magnet.link,
|
||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
||||
)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
|
|
@ -136,7 +145,7 @@ struct SearchResultButtonView: View {
|
|||
existingBookmark = nil
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
// Only run a exists request if a bookmark isn't passed to the view
|
||||
if existingBookmark == nil, !runOnce {
|
||||
let bookmarkRequest = Bookmark.fetchRequest()
|
||||
|
|
|
|||
|
|
@ -44,11 +44,11 @@ struct BackupsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.inlinedList()
|
||||
.inlinedList(inset: -20)
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
backupManager.backupUrls = FileManager.default.appDirectory
|
||||
.appendingPathComponent("Backups", isDirectory: true).contentsByDateAdded
|
||||
}
|
||||
|
|
@ -57,7 +57,9 @@ struct BackupsView: View {
|
|||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
backupManager.createBackup()
|
||||
Task {
|
||||
await backupManager.createBackup()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
//
|
||||
// DefaultActionsPickerViews.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/11/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DefaultActionPickerView: View {
|
||||
@EnvironmentObject var toastModel: ToastViewModel
|
||||
|
||||
let actionRequirement: ActionRequirement
|
||||
@Binding var defaultActionName: String?
|
||||
@Binding var defaultActionList: String?
|
||||
|
||||
@FetchRequest(
|
||||
entity: Action.entity(),
|
||||
sortDescriptors: []
|
||||
) var actions: FetchedResults<Action>
|
||||
|
||||
@FetchRequest(
|
||||
entity: PluginList.entity(),
|
||||
sortDescriptors: []
|
||||
) var pluginLists: FetchedResults<PluginList>
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
UserChoiceButton(
|
||||
defaultActionName: $defaultActionName,
|
||||
defaultActionList: $defaultActionList,
|
||||
pluginLists: pluginLists
|
||||
)
|
||||
|
||||
ForEach(actions.filter { $0.requires.contains(actionRequirement.rawValue) }, id: \.id) { action in
|
||||
Button {
|
||||
if let actionListId = action.listId?.uuidString {
|
||||
defaultActionName = action.name
|
||||
defaultActionList = actionListId
|
||||
} else {
|
||||
toastModel.updateToastDescription(
|
||||
"Default action error: 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)
|
||||
}
|
||||
Spacer()
|
||||
|
||||
if
|
||||
let defaultActionList,
|
||||
action.listId?.uuidString == defaultActionList,
|
||||
action.name == defaultActionName
|
||||
{
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList(inset: -20)
|
||||
.navigationTitle("Default \(actionRequirement == .debrid ? "debrid" : "magnet") action")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
private struct UserChoiceButton: View {
|
||||
@Binding var defaultActionName: String?
|
||||
@Binding var defaultActionList: String?
|
||||
var pluginLists: FetchedResults<PluginList>
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
defaultActionName = nil
|
||||
defaultActionList = nil
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Let me choose")
|
||||
Spacer()
|
||||
|
||||
// Force "Let me choose" if the name OR list ID is nil
|
||||
// Prevents any mismatches
|
||||
if defaultActionName == nil || pluginLists.contains(where: { $0.id.uuidString == defaultActionList }) {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
//
|
||||
// DefaultActionsPickerViews.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/11/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MagnetActionPickerView: View {
|
||||
@AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(DefaultMagnetActionType.allCases, id: \.self) { action in
|
||||
Button {
|
||||
defaultMagnetAction = action
|
||||
} label: {
|
||||
HStack {
|
||||
Text(fetchPickerChoiceName(choice: action))
|
||||
Spacer()
|
||||
if action == defaultMagnetAction {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList()
|
||||
.navigationTitle("Default magnet action")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
func fetchPickerChoiceName(choice: DefaultMagnetActionType) -> String {
|
||||
switch choice {
|
||||
case .none:
|
||||
return "Let me choose"
|
||||
case .webtor:
|
||||
return "Open in Webtor"
|
||||
case .shareMagnet:
|
||||
return "Share magnet link"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DebridActionPickerView: View {
|
||||
@AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(DefaultDebridActionType.allCases, id: \.self) { action in
|
||||
Button {
|
||||
defaultDebridAction = action
|
||||
} label: {
|
||||
HStack {
|
||||
Text(fetchPickerChoiceName(choice: action))
|
||||
Spacer()
|
||||
if action == defaultDebridAction {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList()
|
||||
.navigationTitle("Default debrid action")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
func fetchPickerChoiceName(choice: DefaultDebridActionType) -> String {
|
||||
switch choice {
|
||||
case .none:
|
||||
return "Let me choose"
|
||||
case .outplayer:
|
||||
return "Open in Outplayer"
|
||||
case .vlc:
|
||||
return "Open in VLC"
|
||||
case .infuse:
|
||||
return "Open in Infuse"
|
||||
case .shareDownload:
|
||||
return "Share download link"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,38 +7,41 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct SourceListEditorView: View {
|
||||
struct PluginListEditorView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@State private var sourceUrlSet = false
|
||||
@State var selectedPluginList: PluginList?
|
||||
|
||||
@State private var sourceUrl: String = ""
|
||||
@State private var sourceUrlSet = false
|
||||
@State private var showUrlErrorAlert = false
|
||||
|
||||
@State private var pluginListUrl: String = ""
|
||||
@State private var urlErrorAlertText: String = ""
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
Form {
|
||||
TextField("Enter URL", text: $sourceUrl)
|
||||
TextField("Enter URL", text: $pluginListUrl)
|
||||
.disableAutocorrection(true)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
.conditionalId(sourceUrlSet)
|
||||
}
|
||||
.onAppear {
|
||||
sourceUrl = navModel.selectedSourceList?.urlString ?? ""
|
||||
.backport.onAppear {
|
||||
pluginListUrl = selectedPluginList?.urlString ?? ""
|
||||
sourceUrlSet = true
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $sourceManager.showUrlErrorAlert,
|
||||
isPresented: $showUrlErrorAlert,
|
||||
title: "Error",
|
||||
message: sourceManager.urlErrorAlertText,
|
||||
buttons: [AlertButton("OK")]
|
||||
message: urlErrorAlertText
|
||||
)
|
||||
.navigationTitle("Editing source list")
|
||||
.navigationTitle("Editing Plugin List")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
|
|
@ -50,25 +53,23 @@ struct SourceListEditorView: View {
|
|||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
Task {
|
||||
if await sourceManager.addSourceList(
|
||||
sourceUrl: sourceUrl,
|
||||
existingSourceList: navModel.selectedSourceList
|
||||
) {
|
||||
do {
|
||||
try await pluginManager.addPluginList(pluginListUrl, existingPluginList: selectedPluginList)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} catch {
|
||||
urlErrorAlertText = error.localizedDescription
|
||||
showUrlErrorAlert.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
navModel.selectedSourceList = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SourceListEditorView_Previews: PreviewProvider {
|
||||
struct PluginListEditorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SourceListEditorView()
|
||||
PluginListEditorView()
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ struct SettingsAppVersionView: View {
|
|||
.listStyle(.insetGrouped)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
viewTask = Task {
|
||||
do {
|
||||
if let fetchedReleases = try await Github().fetchReleases() {
|
||||
|
|
|
|||
|
|
@ -7,40 +7,41 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsSourceListView: View {
|
||||
struct SettingsPluginListView: View {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@FetchRequest(
|
||||
entity: SourceList.entity(),
|
||||
entity: PluginList.entity(),
|
||||
sortDescriptors: []
|
||||
) var sourceLists: FetchedResults<SourceList>
|
||||
) var pluginLists: FetchedResults<PluginList>
|
||||
|
||||
@State private var presentSourceSheet = false
|
||||
@State private var selectedSourceList: SourceList?
|
||||
@State private var selectedPluginList: PluginList?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if sourceLists.isEmpty {
|
||||
if pluginLists.isEmpty {
|
||||
EmptyInstructionView(title: "No Lists", message: "Add a source list using the + button in the top-right")
|
||||
} else {
|
||||
List {
|
||||
ForEach(sourceLists, id: \.self) { sourceList in
|
||||
ForEach(pluginLists, id: \.self) { pluginList in
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(sourceList.name)
|
||||
Text(pluginList.name)
|
||||
|
||||
Text(sourceList.author)
|
||||
.foregroundColor(.gray)
|
||||
Group {
|
||||
Text(pluginList.author)
|
||||
|
||||
Text("ID: \(sourceList.id)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
Text("ID: \(pluginList.id)")
|
||||
.font(.caption)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
.contextMenu {
|
||||
Button {
|
||||
navModel.selectedSourceList = sourceList
|
||||
selectedPluginList = pluginList
|
||||
presentSourceSheet.toggle()
|
||||
} label: {
|
||||
Text("Edit")
|
||||
|
|
@ -49,14 +50,14 @@ struct SettingsSourceListView: View {
|
|||
|
||||
if #available(iOS 15.0, *) {
|
||||
Button(role: .destructive) {
|
||||
PersistenceController.shared.delete(sourceList, context: backgroundContext)
|
||||
PersistenceController.shared.delete(pluginList, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
PersistenceController.shared.delete(sourceList, context: backgroundContext)
|
||||
PersistenceController.shared.delete(pluginList, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
|
|
@ -66,25 +67,25 @@ struct SettingsSourceListView: View {
|
|||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let list = sourceLists[safe: index] {
|
||||
if let list = pluginLists[safe: index] {
|
||||
PersistenceController.shared.delete(list, context: backgroundContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList()
|
||||
.inlinedList(inset: -20)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $presentSourceSheet) {
|
||||
if #available(iOS 16, *) {
|
||||
SourceListEditorView()
|
||||
PluginListEditorView(selectedPluginList: selectedPluginList)
|
||||
.presentationDetents([.medium])
|
||||
} else {
|
||||
SourceListEditorView()
|
||||
PluginListEditorView(selectedPluginList: selectedPluginList)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Source Lists")
|
||||
.navigationTitle("Plugin Lists")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
|
|
@ -98,8 +99,8 @@ struct SettingsSourceListView: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct SettingsSourceListView_Previews: PreviewProvider {
|
||||
struct SettingsPluginListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsSourceListView()
|
||||
SettingsPluginListView()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
//
|
||||
// SourceUpdateButtonView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/5/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SourceUpdateButtonView: View {
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
|
||||
let updatedSource: SourceJson
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
Text(updatedSource.name)
|
||||
Text("v\(updatedSource.version)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("by \(updatedSource.author ?? "Unknown")")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Update") {
|
||||
Task {
|
||||
await sourceManager.installSource(sourceJson: updatedSource, doUpsert: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,120 +12,129 @@ struct ContentView: View {
|
|||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
@EnvironmentObject var toastModel: ToastViewModel
|
||||
|
||||
@FetchRequest(
|
||||
entity: Source.entity(),
|
||||
sortDescriptors: []
|
||||
) var sources: FetchedResults<Source>
|
||||
@AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText: Bool = false
|
||||
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
@State private var isEditingSearch = false
|
||||
@State private var isSearching = false
|
||||
@State private var searchText: String = ""
|
||||
|
||||
@State private var selectedSource: Source? {
|
||||
didSet {
|
||||
scrapingModel.filteredSource = selectedSource
|
||||
}
|
||||
}
|
||||
@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 {
|
||||
NavView {
|
||||
VStack(spacing: 10) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Filter")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Menu {
|
||||
Button {
|
||||
selectedSource = nil
|
||||
} label: {
|
||||
Text("None")
|
||||
|
||||
if selectedSource == nil {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(sources, id: \.self) { source in
|
||||
if let name = source.name, source.enabled {
|
||||
Button {
|
||||
selectedSource = source
|
||||
} label: {
|
||||
if selectedSource == source {
|
||||
Label(name, systemImage: "checkmark")
|
||||
} else {
|
||||
Text(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(selectedSource?.name ?? "Source")
|
||||
.padding(.trailing, -3)
|
||||
Image(systemName: "chevron.down")
|
||||
List {
|
||||
ForEach(scrapingModel.searchResults, id: \.self) { result in
|
||||
if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil {
|
||||
SearchResultButtonView(result: result)
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
.animation(.none)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 5)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
SearchResultsView()
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 20 : -20)
|
||||
.overlay {
|
||||
if scrapingModel.searchResults.isEmpty && isSearching && scrapingModel.runningSearchTask == nil {
|
||||
Text("No results found")
|
||||
}
|
||||
}
|
||||
.onChange(of: searchText) { newText in
|
||||
if newText.isEmpty && isSearching {
|
||||
searchBarText = getSearchBarText()
|
||||
}
|
||||
}
|
||||
.onChange(of: scrapingModel.searchResults) { _ in
|
||||
// Cleans up any leftover search results in the event of an abrupt cancellation
|
||||
if !isSearching {
|
||||
scrapingModel.searchResults = []
|
||||
}
|
||||
}
|
||||
.onChange(of: navModel.selectedTab) { tab in
|
||||
// Cancel the search if tab is switched while search is in progress
|
||||
if tab != .search, scrapingModel.runningSearchTask != nil {
|
||||
scrapingModel.searchResults = []
|
||||
scrapingModel.runningSearchTask?.cancel()
|
||||
scrapingModel.runningSearchTask = nil
|
||||
isSearching = false
|
||||
searchText = ""
|
||||
}
|
||||
}
|
||||
.navigationTitle("Search")
|
||||
.navigationBarTitleDisplayMode(
|
||||
navModel.isSearching && Application.shared.osVersion.majorVersion > 14 ? .inline : .large
|
||||
)
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search",
|
||||
text: $scrapingModel.searchText,
|
||||
isEditing: $navModel.isEditingSearch,
|
||||
onCommit: {
|
||||
scrapingModel.searchResults = []
|
||||
scrapingModel.runningSearchTask = Task {
|
||||
navModel.isSearching = true
|
||||
navModel.showSearchProgress = true
|
||||
SearchBar(
|
||||
searchBarText,
|
||||
text: $searchText,
|
||||
isEditing: $isEditingSearch,
|
||||
onCommit: {
|
||||
if let runningSearchTask = scrapingModel.runningSearchTask, runningSearchTask.isCancelled {
|
||||
scrapingModel.runningSearchTask = nil
|
||||
return
|
||||
}
|
||||
|
||||
let sources = sourceManager.fetchInstalledSources()
|
||||
await scrapingModel.scanSources(sources: sources)
|
||||
scrapingModel.runningSearchTask = Task {
|
||||
isSearching = true
|
||||
|
||||
if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty {
|
||||
debridManager.clearIAValues()
|
||||
let sources = pluginManager.fetchInstalledSources()
|
||||
await scrapingModel.scanSources(sources: sources, searchText: searchText)
|
||||
|
||||
// Remove magnets that don't have a hash
|
||||
let magnets = scrapingModel.searchResults.compactMap {
|
||||
if let magnetHash = $0.magnet.hash {
|
||||
return Magnet(hash: magnetHash, link: $0.magnet.link)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
await debridManager.populateDebridIA(magnets)
|
||||
}
|
||||
if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty {
|
||||
debridManager.clearIAValues()
|
||||
|
||||
navModel.showSearchProgress = false
|
||||
}
|
||||
})
|
||||
.showsCancelButton(navModel.isEditingSearch || navModel.isSearching)
|
||||
.onCancel {
|
||||
scrapingModel.searchResults = []
|
||||
scrapingModel.runningSearchTask?.cancel()
|
||||
scrapingModel.runningSearchTask = nil
|
||||
navModel.isSearching = false
|
||||
scrapingModel.searchText = ""
|
||||
}
|
||||
}
|
||||
.introspectSearchController { searchController in
|
||||
searchController.hidesNavigationBarDuringPresentation = false
|
||||
searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no
|
||||
searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
DebridChoiceView()
|
||||
let magnets = scrapingModel.searchResults.map(\.magnet)
|
||||
await debridManager.populateDebridIA(magnets)
|
||||
}
|
||||
|
||||
toastModel.hideIndeterminateToast()
|
||||
scrapingModel.runningSearchTask = nil
|
||||
}
|
||||
}
|
||||
)
|
||||
.showsCancelButton(isEditingSearch || isSearching)
|
||||
.onCancel {
|
||||
scrapingModel.searchResults = []
|
||||
scrapingModel.runningSearchTask?.cancel()
|
||||
scrapingModel.runningSearchTask = nil
|
||||
isSearching = false
|
||||
searchText = ""
|
||||
searchBarText = getSearchBarText()
|
||||
}
|
||||
}
|
||||
.navigationSearchBarHiddenWhenScrolling(false)
|
||||
.customScopeBar {
|
||||
SearchFilterHeaderView()
|
||||
.environmentObject(scrapingModel)
|
||||
.environmentObject(debridManager)
|
||||
}
|
||||
}
|
||||
.backport.onAppear {
|
||||
searchBarText = getSearchBarText()
|
||||
}
|
||||
}
|
||||
|
||||
// Fetches random searchbar text if enabled, otherwise deinit the last case value
|
||||
func getSearchBarText() -> String {
|
||||
if usesRandomSearchText {
|
||||
let num = Int.random(in: 0..<searchBarTextArray.count - 1)
|
||||
if num == lastSearchTextIndex {
|
||||
lastSearchTextIndex = num + 1
|
||||
return searchBarTextArray[safe: num + 1] ?? "Search"
|
||||
} else {
|
||||
lastSearchTextIndex = num
|
||||
return searchBarTextArray[safe: num] ?? "Search"
|
||||
}
|
||||
} else {
|
||||
lastSearchTextIndex = -1
|
||||
return "Search"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,8 @@ import SwiftUI
|
|||
import SwiftUIX
|
||||
|
||||
struct LibraryView: View {
|
||||
enum LibraryPickerSegment {
|
||||
case bookmarks
|
||||
case history
|
||||
case debridCloud
|
||||
}
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@FetchRequest(
|
||||
entity: Bookmark.entity(),
|
||||
|
|
@ -32,7 +26,6 @@ struct LibraryView: View {
|
|||
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
|
||||
@State private var selectedSegment: LibraryPickerSegment = .bookmarks
|
||||
@State private var editMode: EditMode = .inactive
|
||||
|
||||
@State private var searchText: String = ""
|
||||
|
|
@ -41,20 +34,8 @@ struct LibraryView: View {
|
|||
|
||||
var body: some View {
|
||||
NavView {
|
||||
VStack {
|
||||
Picker("Segments", selection: $selectedSegment) {
|
||||
Text("Bookmarks").tag(LibraryPickerSegment.bookmarks)
|
||||
Text("History").tag(LibraryPickerSegment.history)
|
||||
|
||||
if !debridManager.enabledDebrids.isEmpty {
|
||||
Text("Cloud").tag(LibraryPickerSegment.debridCloud)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 5)
|
||||
|
||||
switch selectedSegment {
|
||||
ZStack {
|
||||
switch navModel.libraryPickerSelection {
|
||||
case .bookmarks:
|
||||
BookmarksView(searchText: $searchText)
|
||||
case .history:
|
||||
|
|
@ -62,25 +43,9 @@ struct LibraryView: View {
|
|||
case .debridCloud:
|
||||
DebridCloudView(searchText: $searchText)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: {
|
||||
isSearching = true
|
||||
})
|
||||
.showsCancelButton(isEditingSearch || isSearching)
|
||||
.onCancel {
|
||||
searchText = ""
|
||||
isSearching = false
|
||||
}
|
||||
}
|
||||
.introspectSearchController { searchController in
|
||||
searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no
|
||||
searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none
|
||||
}
|
||||
.overlay {
|
||||
switch selectedSegment {
|
||||
switch navModel.libraryPickerSelection {
|
||||
case .bookmarks:
|
||||
if bookmarks.isEmpty {
|
||||
EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results")
|
||||
|
|
@ -102,19 +67,39 @@ struct LibraryView: View {
|
|||
Spacer()
|
||||
EditButton()
|
||||
|
||||
switch selectedSegment {
|
||||
switch navModel.libraryPickerSelection {
|
||||
case .bookmarks, .debridCloud:
|
||||
DebridChoiceView()
|
||||
DebridPickerView() {
|
||||
Text(debridManager.selectedDebridType?.toString(abbreviated: true) ?? "Debrid")
|
||||
}
|
||||
.transaction {
|
||||
$0.animation = .none
|
||||
}
|
||||
case .history:
|
||||
HistoryActionsView()
|
||||
}
|
||||
}
|
||||
.animation(.none)
|
||||
}
|
||||
}
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: {
|
||||
isSearching = true
|
||||
})
|
||||
.showsCancelButton(isEditingSearch || isSearching)
|
||||
.onCancel {
|
||||
searchText = ""
|
||||
isSearching = false
|
||||
}
|
||||
}
|
||||
.navigationSearchBarHiddenWhenScrolling(false)
|
||||
.customScopeBar {
|
||||
LibraryPickerView()
|
||||
.environmentObject(debridManager)
|
||||
.environmentObject(navModel)
|
||||
}
|
||||
.environment(\.editMode, $editMode)
|
||||
}
|
||||
.onChange(of: selectedSegment) { _ in
|
||||
.onChange(of: navModel.libraryPickerSelection) { _ in
|
||||
editMode = .inactive
|
||||
}
|
||||
.onDisappear {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ struct MainView: View {
|
|||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var backupManager: BackupManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true
|
||||
|
||||
|
|
@ -28,33 +29,35 @@ struct MainView: View {
|
|||
.tabItem {
|
||||
Label("Search", systemImage: "magnifyingglass")
|
||||
}
|
||||
.tag(ViewTab.search)
|
||||
.tag(NavigationViewModel.ViewTab.search)
|
||||
|
||||
LibraryView()
|
||||
.tabItem {
|
||||
Label("Library", systemImage: "book.closed")
|
||||
}
|
||||
.tag(ViewTab.library)
|
||||
.tag(NavigationViewModel.ViewTab.library)
|
||||
|
||||
SourcesView()
|
||||
PluginsView()
|
||||
.tabItem {
|
||||
Label("Sources", systemImage: "doc.text")
|
||||
Label("Plugins", systemImage: "doc.text")
|
||||
}
|
||||
.tag(ViewTab.sources)
|
||||
.tag(NavigationViewModel.ViewTab.plugins)
|
||||
|
||||
SettingsView()
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
.tag(ViewTab.settings)
|
||||
.tag(NavigationViewModel.ViewTab.settings)
|
||||
}
|
||||
.sheet(item: $navModel.currentChoiceSheet) { item in
|
||||
switch item {
|
||||
case .magnet:
|
||||
MagnetChoiceView()
|
||||
case .action:
|
||||
ActionChoiceView()
|
||||
.environmentObject(debridManager)
|
||||
.environmentObject(scrapingModel)
|
||||
.environmentObject(navModel)
|
||||
.environmentObject(pluginManager)
|
||||
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
|
||||
case .batch:
|
||||
BatchChoiceView()
|
||||
.environmentObject(debridManager)
|
||||
|
|
@ -69,7 +72,7 @@ struct MainView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
.backport.onAppear {
|
||||
if autoUpdateNotifs {
|
||||
viewTask = Task {
|
||||
do {
|
||||
|
|
@ -101,42 +104,53 @@ struct MainView: View {
|
|||
backupManager.showRestoreAlert.toggle()
|
||||
}
|
||||
}
|
||||
// Global alerts for backups
|
||||
.backport.alert(
|
||||
// Global alerts and dialogs for backups
|
||||
.backport.confirmationDialog(
|
||||
isPresented: $backupManager.showRestoreAlert,
|
||||
title: "Restore backup?",
|
||||
message: "Restoring this backup will merge all your data!",
|
||||
message:
|
||||
"Merge (preferred): Will merge your current data with the backup \n\n" +
|
||||
"Overwrite: Will delete and replace all your data \n\n" +
|
||||
"If Merge causes app instability, uninstall Ferrite and use the Overwrite option.",
|
||||
buttons: [
|
||||
.init("Restore", role: .destructive) {
|
||||
backupManager.restoreBackup()
|
||||
.init("Merge", role: .destructive) {
|
||||
Task {
|
||||
await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: false)
|
||||
}
|
||||
},
|
||||
.init(role: .cancel)
|
||||
.init("Overwrite", role: .destructive) {
|
||||
Task {
|
||||
await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: true)
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
.backport.alert(
|
||||
isPresented: $backupManager.showRestoreCompletedAlert,
|
||||
title: "Backup restored",
|
||||
message: backupManager.backupSourceNames.isEmpty ?
|
||||
"No sources need to be reinstalled" :
|
||||
"Reinstall sources: \(backupManager.backupSourceNames.joined(separator: ", "))",
|
||||
message: backupManager.restoreCompletedMessage.joined(separator: " \n\n"),
|
||||
buttons: [
|
||||
.init("OK") {}
|
||||
.init("OK") {
|
||||
backupManager.restoreCompletedMessage = []
|
||||
}
|
||||
]
|
||||
)
|
||||
// Updater alert
|
||||
.backport.alert(
|
||||
isPresented: $showUpdateAlert,
|
||||
title: "Update available",
|
||||
message: "Ferrite \(releaseVersionString) can be downloaded. \n\n This alert can be disabled in Settings.",
|
||||
message:
|
||||
"Ferrite \(releaseVersionString) can be downloaded. \n\n" +
|
||||
"This alert can be disabled in Settings.",
|
||||
buttons: [
|
||||
AlertButton("Download") {
|
||||
.init("Download") {
|
||||
guard let releaseUrl = URL(string: releaseUrlString) else {
|
||||
return
|
||||
}
|
||||
|
||||
UIApplication.shared.open(releaseUrl)
|
||||
},
|
||||
AlertButton(role: .cancel)
|
||||
.init(role: .cancel)
|
||||
]
|
||||
)
|
||||
.overlay {
|
||||
|
|
@ -159,17 +173,18 @@ struct MainView: View {
|
|||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
if debridManager.showLoadingProgress {
|
||||
if toastModel.showIndeterminateToast {
|
||||
VStack {
|
||||
Text("Loading content")
|
||||
Text(toastModel.indeterminateToastDescription ?? "Loading...")
|
||||
|
||||
HStack {
|
||||
IndeterminateProgressView()
|
||||
|
||||
Button("Cancel") {
|
||||
debridManager.currentDebridTask?.cancel()
|
||||
debridManager.currentDebridTask = nil
|
||||
debridManager.showLoadingProgress = false
|
||||
if let cancelAction = toastModel.indeterminateCancelAction {
|
||||
Button("Cancel") {
|
||||
cancelAction()
|
||||
toastModel.hideIndeterminateToast()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -186,7 +201,7 @@ struct MainView: View {
|
|||
.foregroundColor(.clear)
|
||||
.frame(height: 60)
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.3), value: toastModel.showToast || debridManager.showLoadingProgress)
|
||||
.animation(.easeInOut(duration: 0.3), value: toastModel.showToast || toastModel.showIndeterminateToast)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
97
Ferrite/Views/PluginsView.swift
Normal file
97
Ferrite/Views/PluginsView.swift
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
//
|
||||
// PluginsView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 1/11/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftUIX
|
||||
|
||||
struct PluginsView: View {
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@FetchRequest(
|
||||
entity: Source.entity(),
|
||||
sortDescriptors: []
|
||||
) var sources: FetchedResults<Source>
|
||||
|
||||
@FetchRequest(
|
||||
entity: Action.entity(),
|
||||
sortDescriptors: []
|
||||
) var actions: FetchedResults<Action>
|
||||
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
|
||||
@State private var checkedForPlugins = false
|
||||
|
||||
@State private var isEditingSearch = false
|
||||
@State private var isSearching = false
|
||||
@State private var searchText: String = ""
|
||||
|
||||
@State private var viewTask: Task<Void, Never>?
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
ZStack {
|
||||
if checkedForPlugins {
|
||||
switch navModel.pluginPickerSelection {
|
||||
case .sources:
|
||||
PluginListView<Source, SourceJson>(searchText: $searchText)
|
||||
case .actions:
|
||||
PluginListView<Action, ActionJson>(searchText: $searchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
if checkedForPlugins {
|
||||
switch navModel.pluginPickerSelection {
|
||||
case .sources:
|
||||
if sources.isEmpty && pluginManager.availableSources.isEmpty {
|
||||
EmptyInstructionView(title: "No Sources", message: "Add a plugin list in Settings")
|
||||
}
|
||||
case .actions:
|
||||
if actions.isEmpty && pluginManager.availableActions.isEmpty {
|
||||
EmptyInstructionView(title: "No Actions", message: "Add a plugin list in Settings")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.backport.onAppear {
|
||||
viewTask = Task {
|
||||
await pluginManager.fetchPluginsFromUrl()
|
||||
checkedForPlugins = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
checkedForPlugins = false
|
||||
}
|
||||
.navigationTitle("Plugins")
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: {
|
||||
isSearching = true
|
||||
})
|
||||
.showsCancelButton(isEditingSearch || isSearching)
|
||||
.onCancel {
|
||||
searchText = ""
|
||||
isSearching = false
|
||||
}
|
||||
}
|
||||
.navigationSearchBarHiddenWhenScrolling(false)
|
||||
.customScopeBar {
|
||||
PluginPickerView()
|
||||
.environmentObject(navModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PluginsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PluginsView()
|
||||
}
|
||||
}
|
||||
43
Ferrite/Views/RepresentableViews/ViewDidAppearHandler.swift
Normal file
43
Ferrite/Views/RepresentableViews/ViewDidAppearHandler.swift
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// 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) {}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
//
|
||||
// SearchResultsView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/11/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchResultsView: View {
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(scrapingModel.searchResults, id: \.self) { result in
|
||||
if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil {
|
||||
SearchResultButtonView(result: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList()
|
||||
.overlay {
|
||||
if scrapingModel.searchResults.isEmpty {
|
||||
if navModel.showSearchProgress {
|
||||
VStack(spacing: 5) {
|
||||
ProgressView()
|
||||
Text("Loading \(scrapingModel.currentSourceName ?? "")")
|
||||
}
|
||||
} else if navModel.isSearching, scrapingModel.runningSearchTask != nil {
|
||||
Text("No results found")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: navModel.selectedTab) { tab in
|
||||
// Cancel the search if tab is switched while search is in progress
|
||||
if tab != .search, navModel.showSearchProgress {
|
||||
scrapingModel.searchResults = []
|
||||
scrapingModel.runningSearchTask?.cancel()
|
||||
scrapingModel.runningSearchTask = nil
|
||||
navModel.isSearching = false
|
||||
scrapingModel.searchText = ""
|
||||
}
|
||||
}
|
||||
.onChange(of: scrapingModel.searchResults) { _ in
|
||||
// Cleans up any leftover search results in the event of an abrupt cancellation
|
||||
if !navModel.isSearching {
|
||||
scrapingModel.searchResults = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchResultsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SearchResultsView()
|
||||
}
|
||||
}
|
||||
|
|
@ -11,16 +11,20 @@ import SwiftUI
|
|||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
@AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText = false
|
||||
|
||||
@AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true
|
||||
|
||||
@AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none
|
||||
@AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none
|
||||
@AppStorage("Actions.DefaultDebridName") var defaultDebridActionName: String?
|
||||
@AppStorage("Actions.DefaultDebridList") var defaultDebridActionList: String?
|
||||
|
||||
@AppStorage("Actions.DefaultMagnetName") var defaultMagnetActionName: String?
|
||||
@AppStorage("Actions.DefaultMagnetList") var defaultMagnetActionList: String?
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
|
|
@ -78,80 +82,74 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Behavior")) {
|
||||
Section(header: InlineHeader("Behavior")) {
|
||||
Toggle(isOn: $autocorrectSearch) {
|
||||
Text("Autocorrect search")
|
||||
}
|
||||
|
||||
Toggle(isOn: $usesRandomSearchText) {
|
||||
Text("Random searchbar text")
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Source management")) {
|
||||
NavigationLink("Source lists", destination: SettingsSourceListView())
|
||||
Section(header: InlineHeader("Plugin management")) {
|
||||
NavigationLink("Plugin lists", destination: SettingsPluginListView())
|
||||
}
|
||||
|
||||
Section(header: Text("Default actions")) {
|
||||
if debridManager.enabledDebrids.count > 0 {
|
||||
Section(header: InlineHeader("Default actions")) {
|
||||
//if debridManager.enabledDebrids.count > 0 {
|
||||
NavigationLink(
|
||||
destination: DebridActionPickerView(),
|
||||
destination: DefaultActionPickerView(
|
||||
actionRequirement: .debrid,
|
||||
defaultActionName: $defaultDebridActionName,
|
||||
defaultActionList: $defaultDebridActionList
|
||||
),
|
||||
label: {
|
||||
HStack {
|
||||
Text("Default debrid action")
|
||||
Text("Debrid action")
|
||||
Spacer()
|
||||
Group {
|
||||
switch defaultDebridAction {
|
||||
case .none:
|
||||
Text("User choice")
|
||||
case .outplayer:
|
||||
Text("Outplayer")
|
||||
case .vlc:
|
||||
Text("VLC")
|
||||
case .infuse:
|
||||
Text("Infuse")
|
||||
case .shareDownload:
|
||||
Text("Share")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
|
||||
// TODO: Maybe make this check for nil list as well?
|
||||
Text(defaultDebridActionName.map { $0 } ?? "User choice")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
//}
|
||||
|
||||
NavigationLink(
|
||||
destination: MagnetActionPickerView(),
|
||||
destination: DefaultActionPickerView(
|
||||
actionRequirement: .magnet,
|
||||
defaultActionName: $defaultMagnetActionName,
|
||||
defaultActionList: $defaultMagnetActionList
|
||||
),
|
||||
label: {
|
||||
HStack {
|
||||
Text("Default magnet action")
|
||||
Text("Magnet action")
|
||||
Spacer()
|
||||
Group {
|
||||
switch defaultMagnetAction {
|
||||
case .none:
|
||||
Text("User choice")
|
||||
case .webtor:
|
||||
Text("Webtor")
|
||||
case .shareMagnet:
|
||||
Text("Share")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
|
||||
// TODO: Maybe make this check for nil list as well?
|
||||
Text(defaultMagnetActionName.map { $0 } ?? "User choice")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Section(header: Text("Backups")) {
|
||||
Section(header: InlineHeader("Backups")) {
|
||||
NavigationLink(destination: BackupsView()) {
|
||||
Text("Backups")
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Updates")) {
|
||||
Section(header: InlineHeader("Updates")) {
|
||||
Toggle(isOn: $autoUpdateNotifs) {
|
||||
Text("Show update alerts")
|
||||
}
|
||||
NavigationLink("Version history", destination: SettingsAppVersionView())
|
||||
}
|
||||
|
||||
Section(header: Text("Information")) {
|
||||
Section(header: InlineHeader("Information")) {
|
||||
ListRowLinkView(text: "Donate", link: "https://ko-fi.com/kingbri")
|
||||
ListRowLinkView(text: "Report issues", link: "https://github.com/bdashore3/Ferrite/issues")
|
||||
NavigationLink("About", destination: AboutView())
|
||||
|
|
|
|||
|
|
@ -8,14 +8,18 @@
|
|||
import SwiftUI
|
||||
import SwiftUIX
|
||||
|
||||
struct MagnetChoiceView: View {
|
||||
struct ActionChoiceView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
@FetchRequest(
|
||||
entity: Action.entity(),
|
||||
sortDescriptors: []
|
||||
) var actions: FetchedResults<Action>
|
||||
|
||||
@State private var showLinkCopyAlert = false
|
||||
@State private var showMagnetCopyAlert = false
|
||||
|
|
@ -39,16 +43,12 @@ struct MagnetChoiceView: View {
|
|||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
Section(header: "Debrid options") {
|
||||
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .outplayer)
|
||||
}
|
||||
|
||||
ListRowButtonView("Play on VLC", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .vlc)
|
||||
}
|
||||
|
||||
ListRowButtonView("Play on Infuse", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .infuse)
|
||||
ForEach(actions, id: \.id) { action in
|
||||
if action.requires.contains(ActionRequirement.debrid.rawValue) {
|
||||
ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") {
|
||||
pluginManager.runDeeplinkAction(action, urlString: debridManager.downloadUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") {
|
||||
|
|
@ -73,6 +73,14 @@ struct MagnetChoiceView: View {
|
|||
|
||||
if !navModel.resultFromCloud {
|
||||
Section(header: "Magnet options") {
|
||||
ForEach(actions, id: \.id) { action in
|
||||
if action.requires.contains(ActionRequirement.magnet.rawValue) {
|
||||
ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") {
|
||||
pluginManager.runDeeplinkAction(action, urlString: navModel.selectedMagnet?.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") {
|
||||
UIPasteboard.general.string = navModel.selectedMagnet?.link
|
||||
showMagnetCopyAlert.toggle()
|
||||
|
|
@ -92,10 +100,6 @@ struct MagnetChoiceView: View {
|
|||
navModel.showLocalActivitySheet.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runMagnetAction(magnet: navModel.selectedMagnet, .webtor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -108,6 +112,13 @@ struct MagnetChoiceView: View {
|
|||
AppActivityView(activityItems: navModel.activityItems)
|
||||
}
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $pluginManager.showBrokenDefaultActionAlert,
|
||||
title: "Action not found",
|
||||
message:
|
||||
"The default action could not be run. The action choice sheet has been opened. \n\n" +
|
||||
"Please check your default actions in Settings"
|
||||
)
|
||||
.onDisappear {
|
||||
debridManager.downloadUrl = ""
|
||||
navModel.selectedTitle = ""
|
||||
|
|
@ -131,8 +142,8 @@ struct MagnetChoiceView: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct MagnetChoiceView_Previews: PreviewProvider {
|
||||
struct ActionChoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MagnetChoiceView()
|
||||
ActionChoiceView()
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,11 @@ struct BatchChoiceView: View {
|
|||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
// TODO: Make this generic for IA(?) and add searchbar
|
||||
var body: some View {
|
||||
NavView {
|
||||
List {
|
||||
|
|
@ -48,7 +50,7 @@ struct BatchChoiceView: View {
|
|||
}
|
||||
.backport.tint(.primary)
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList()
|
||||
.inlinedList(inset: -20)
|
||||
.navigationTitle("Select a file")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
|
@ -79,10 +81,13 @@ struct BatchChoiceView: View {
|
|||
if var selectedHistoryInfo = navModel.selectedHistoryInfo {
|
||||
selectedHistoryInfo.url = debridManager.downloadUrl
|
||||
selectedHistoryInfo.subName = fileName
|
||||
PersistenceController.shared.createHistory(selectedHistoryInfo)
|
||||
PersistenceController.shared.createHistory(selectedHistoryInfo, performSave: true)
|
||||
}
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
pluginManager.runDebridAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
||||
)
|
||||
}
|
||||
|
||||
debridManager.clearSelectedDebridItems()
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
//
|
||||
// SourceListView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
|
||||
import Introspect
|
||||
import SwiftUI
|
||||
import SwiftUIX
|
||||
|
||||
struct SourcesView: View {
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
|
||||
@State private var checkedForSources = false
|
||||
@State private var isEditingSearch = false
|
||||
@State private var isSearching = false
|
||||
|
||||
@State private var viewTask: Task<Void, Never>? = nil
|
||||
@State private var searchText: String = ""
|
||||
@State private var filteredUpdatedSources: [SourceJson] = []
|
||||
@State private var filteredAvailableSources: [SourceJson] = []
|
||||
@State private var sourcePredicate: NSPredicate?
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
DynamicFetchRequest(predicate: sourcePredicate) { (installedSources: FetchedResults<Source>) in
|
||||
ZStack {
|
||||
if !checkedForSources {
|
||||
ProgressView()
|
||||
} else if installedSources.isEmpty, sourceManager.availableSources.isEmpty {
|
||||
EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings")
|
||||
} else {
|
||||
List {
|
||||
if !filteredUpdatedSources.isEmpty {
|
||||
Section(header: InlineHeader("Updates")) {
|
||||
ForEach(filteredUpdatedSources, id: \.self) { source in
|
||||
SourceUpdateButtonView(updatedSource: source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !installedSources.isEmpty {
|
||||
Section(header: InlineHeader("Installed")) {
|
||||
ForEach(installedSources, id: \.self) { source in
|
||||
InstalledSourceButtonView(installedSource: source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !filteredAvailableSources.isEmpty {
|
||||
Section(header: InlineHeader("Catalog")) {
|
||||
ForEach(filteredAvailableSources, id: \.self) { availableSource in
|
||||
if !installedSources.contains(where: {
|
||||
availableSource.name == $0.name &&
|
||||
availableSource.listId == $0.listId &&
|
||||
availableSource.author == $0.author
|
||||
}) {
|
||||
SourceCatalogButtonView(availableSource: availableSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.conditionalId(UUID())
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $navModel.showSourceSettings) {
|
||||
SourceSettingsView()
|
||||
.environmentObject(navModel)
|
||||
}
|
||||
.onAppear {
|
||||
viewTask = Task {
|
||||
await sourceManager.fetchSourcesFromUrl()
|
||||
filteredAvailableSources = sourceManager.availableSources.filter { availableSource in
|
||||
!installedSources.contains(where: {
|
||||
availableSource.name == $0.name &&
|
||||
availableSource.listId == $0.listId &&
|
||||
availableSource.author == $0.author
|
||||
})
|
||||
}
|
||||
|
||||
filteredUpdatedSources = sourceManager.fetchUpdatedSources(installedSources: installedSources)
|
||||
checkedForSources = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
sourcePredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText)
|
||||
}
|
||||
.onReceive(installedSources.publisher.count()) { _ in
|
||||
filteredAvailableSources = sourceManager.availableSources.filter { availableSource in
|
||||
let sourceExists = installedSources.contains(where: {
|
||||
availableSource.name == $0.name &&
|
||||
availableSource.listId == $0.listId &&
|
||||
availableSource.author == $0.author
|
||||
})
|
||||
|
||||
if searchText.isEmpty {
|
||||
return !sourceExists
|
||||
} else {
|
||||
return !sourceExists && availableSource.name.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
|
||||
filteredUpdatedSources = sourceManager.fetchUpdatedSources(installedSources: installedSources).filter {
|
||||
searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
.navigationTitle("Sources")
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: {
|
||||
isSearching = true
|
||||
})
|
||||
.showsCancelButton(isEditingSearch || isSearching)
|
||||
.onCancel {
|
||||
searchText = ""
|
||||
isSearching = false
|
||||
}
|
||||
}
|
||||
.introspectSearchController { searchController in
|
||||
searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no
|
||||
searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SourcesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SourcesView()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue