Ios 14 #5

Merged
kingbri1 merged 6 commits from ios-14 into default 2022-08-16 18:19:40 +00:00
20 changed files with 232 additions and 268 deletions

View file

@ -20,6 +20,7 @@
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; };
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; };
0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7376EF28A97D1400D60918 /* SwiftUIX */; };
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 */; };
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */; };
@ -28,6 +29,8 @@
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 */; };
0C7D11FC28AA01E900ED92DB /* DynamicAccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */; };
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7D11FD28AA03FE00ED92DB /* View.swift */; };
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */; };
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47A2895BFED0074B7C9 /* Source+CoreDataClass.swift */; };
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */; };
@ -35,7 +38,6 @@
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 */; };
0C90E32C2888E5D000C0BC89 /* ActivityView in Frameworks */ = {isa = PBXBuildFile; productRef = 0C90E32B2888E5D000C0BC89 /* ActivityView */; };
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 */; };
@ -88,6 +90,8 @@
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>"; };
0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicAccentColor.swift; sourceTree = "<group>"; };
0C7D11FD28AA03FE00ED92DB /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB.xcdatamodel; sourceTree = "<group>"; };
0C84F47A2895BFED0074B7C9 /* Source+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataClass.swift"; sourceTree = "<group>"; };
0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataProperties.swift"; sourceTree = "<group>"; };
@ -134,9 +138,9 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0C90E32C2888E5D000C0BC89 /* ActivityView in Frameworks */,
0C64A4B4288903680079976D /* Base32 in Frameworks */,
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */,
0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */,
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */,
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */,
);
@ -221,6 +225,7 @@
0CA148C1288903F000DE2211 /* NavView.swift */,
0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */,
0C32FB562890D1F2002BD219 /* ListRowViews.swift */,
0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */,
);
path = CommonViews;
sourceTree = "<group>";
@ -240,6 +245,7 @@
0CA148CA288903F000DE2211 /* Data.swift */,
0CA148CB288903F000DE2211 /* Task.swift */,
0C32FB542890D1BF002BD219 /* UIApplication.swift */,
0C7D11FD28AA03FE00ED92DB /* View.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -338,10 +344,10 @@
name = Ferrite;
packageProductDependencies = (
0CAF1C7A286F5C8600296F86 /* SwiftSoup */,
0C90E32B2888E5D000C0BC89 /* ActivityView */,
0C64A4B3288903680079976D /* Base32 */,
0C64A4B6288903880079976D /* KeychainSwift */,
0C4CFC452897030D00AD9FAD /* Regex */,
0C7376EF28A97D1400D60918 /* SwiftUIX */,
);
productName = Torrenter;
productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */;
@ -373,10 +379,10 @@
mainGroup = 0CAF1C5F286F5C0D00296F86;
packageReferences = (
0CAF1C79286F5C8600296F86 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
0C90E32A2888E5D000C0BC89 /* XCRemoteSwiftPackageReference "ActivityView" */,
0C64A4B2288903680079976D /* XCRemoteSwiftPackageReference "Base32" */,
0C64A4B5288903880079976D /* XCRemoteSwiftPackageReference "keychain-swift" */,
0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */,
0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */,
);
productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */;
projectDirPath = "";
@ -419,6 +425,7 @@
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
0CF501F2289AE06A0099C785 /* SourceTracker+CoreDataClass.swift in Sources */,
0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */,
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */,
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */,
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */,
@ -439,6 +446,7 @@
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */,
0CA148E2288903F000DE2211 /* Data.swift in Sources */,
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */,
0C7D11FC28AA01E900ED92DB /* DynamicAccentColor.swift in Sources */,
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */,
@ -513,7 +521,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -567,7 +575,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@ -583,7 +591,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_ASSET_PATHS = "\"Ferrite/Preview Content\"";
DEVELOPMENT_TEAM = 8A74DBQ6S3;
ENABLE_PREVIEWS = YES;
@ -596,11 +604,12 @@
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.3.2;
MARKETING_VERSION = 0.3.3;
PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@ -615,7 +624,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_ASSET_PATHS = "\"Ferrite/Preview Content\"";
DEVELOPMENT_TEAM = 8A74DBQ6S3;
ENABLE_PREVIEWS = YES;
@ -628,11 +637,12 @@
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.3.2;
MARKETING_VERSION = 0.3.3;
PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@ -689,12 +699,12 @@
kind = branch;
};
};
0C90E32A2888E5D000C0BC89 /* XCRemoteSwiftPackageReference "ActivityView" */ = {
0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SwiftUI-Plus/ActivityView.git";
repositoryURL = "https://github.com/SwiftUIX/SwiftUIX";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
branch = master;
kind = branch;
};
};
0CAF1C79286F5C8600296F86 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
@ -723,10 +733,10 @@
package = 0C64A4B5288903880079976D /* XCRemoteSwiftPackageReference "keychain-swift" */;
productName = KeychainSwift;
};
0C90E32B2888E5D000C0BC89 /* ActivityView */ = {
0C7376EF28A97D1400D60918 /* SwiftUIX */ = {
isa = XCSwiftPackageProductDependency;
package = 0C90E32A2888E5D000C0BC89 /* XCRemoteSwiftPackageReference "ActivityView" */;
productName = ActivityView;
package = 0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */;
productName = SwiftUIX;
};
0CAF1C7A286F5C8600296F86 /* SwiftSoup */ = {
isa = XCSwiftPackageProductDependency;

View file

@ -46,6 +46,10 @@ struct PersistenceController {
backgroundContext.automaticallyMergesChangesFromParent = true
backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
try? backgroundContext.setQueryGenerationFrom(.current)
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
try? container.viewContext.setQueryGenerationFrom(.current)
}
func save(_ context: NSManagedObjectContext? = nil) {

View file

@ -0,0 +1,16 @@
//
// View.swift
// Ferrite
//
// Created by Brian Dashore on 8/15/22.
//
import SwiftUI
extension View {
// MARK: Modifiers
func dynamicAccentColor(_ color: Color) -> some View {
modifier(DynamicAccentColor(color: color))
}
}

View file

@ -5,7 +5,6 @@
// Created by Brian Dashore on 7/24/22.
//
import ActivityView
import SwiftUI
enum ViewTab {
@ -28,8 +27,14 @@ class NavigationViewModel: ObservableObject {
case batch
}
@Published var isEditingSearch: Bool = false
@Published var isSearching: Bool = false
@Published var hideNavigationBar = false
@Published var currentChoiceSheet: ChoiceSheetType?
@Published var currentActivityItem: ActivityItem?
@Published var activityItems: [Any] = []
@Published var showActivityView: Bool = false
@Published var selectedTab: ViewTab = .search
@Published var showSearchProgress: Bool = false
@ -70,7 +75,8 @@ class NavigationViewModel: ObservableObject {
}
case .shareDownload:
if let downloadUrl = URL(string: urlString), currentChoiceSheet == nil {
currentActivityItem = ActivityItem(items: downloadUrl)
activityItems = [downloadUrl]
showActivityView.toggle()
} else {
toastModel?.toastDescription = "Could not create object for sharing"
}
@ -91,7 +97,8 @@ class NavigationViewModel: ObservableObject {
}
case .shareMagnet:
if let magnetUrl = URL(string: searchResult.magnetLink), currentChoiceSheet == nil {
currentActivityItem = ActivityItem(items: magnetUrl)
activityItems = [magnetUrl]
showActivityView.toggle()
} else {
toastModel?.toastDescription = "Could not create object for sharing"
}

View file

@ -274,6 +274,8 @@ public class SourceManager: ObservableObject {
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)
@ -293,9 +295,9 @@ public class SourceManager: ObservableObject {
newSourceUrl.urlString = sourceUrl
newSourceUrl.name = rawResponse.name
newSourceUrl.author = rawResponse.author
}
try backgroundContext.save()
try backgroundContext.save()
}
return true
} catch {

View file

@ -8,8 +8,6 @@
import SwiftUI
struct AboutView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
VStack {
Image("AppImage")

View file

@ -8,7 +8,7 @@
import SwiftUI
struct BatchChoiceView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var scrapingModel: ScrapingViewModel
@ -25,19 +25,22 @@ struct BatchChoiceView: View {
Task {
await debridManager.fetchRdDownload(searchResult: searchResult, iaFile: file)
// The download may complete before this sheet dismisses
try? await Task.sleep(seconds: 1)
navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl)
if !debridManager.realDebridDownloadUrl.isEmpty {
// The download may complete before this sheet dismisses
try? await Task.sleep(seconds: 1)
navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl)
}
debridManager.selectedRealDebridFile = nil
debridManager.selectedRealDebridItem = nil
}
}
dismiss()
presentationMode.wrappedValue.dismiss()
}
}
}
.listStyle(.insetGrouped)
.navigationTitle("Select a file")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
@ -45,7 +48,7 @@ struct BatchChoiceView: View {
Button("Done") {
debridManager.selectedRealDebridItem = nil
dismiss()
presentationMode.wrappedValue.dismiss()
}
}
}

View file

@ -0,0 +1,22 @@
//
// dynamicAccentColor.swift
// Ferrite
//
// Created by Brian Dashore on 8/15/22.
//
import SwiftUI
struct DynamicAccentColor: ViewModifier {
let color: Color
func body(content: Content) -> some View {
if #available(iOS 15, *) {
content
.tint(color)
} else {
content
.accentColor(color)
}
}
}

View file

@ -14,7 +14,7 @@ struct ErrorGroupBoxStyle: GroupBoxStyle {
configuration.content
}
.padding(10)
.background(Color(uiColor: .secondarySystemGroupedBackground))
.background(Color(UIColor.secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
}

View file

@ -5,8 +5,8 @@
// Created by Brian Dashore on 7/1/22.
//
import ActivityView
import SwiftUI
import SwiftUIX
struct ContentView: View {
@EnvironmentObject var scrapingModel: ScrapingViewModel
@ -66,38 +66,63 @@ struct ContentView: View {
Spacer()
}
.padding(.vertical, 5)
.padding(.horizontal, 20)
SearchResultsView()
}
.searchable(text: $scrapingModel.searchText)
.onSubmit(of: .search) {
scrapingModel.runningSearchTask = Task {
navModel.showSearchProgress = true
await scrapingModel.scanSources(sources: sources.compactMap { $0 })
if realDebridEnabled, !scrapingModel.searchResults.isEmpty {
await debridManager.populateDebridHashes(scrapingModel.searchResults)
.sheet(item: $navModel.currentChoiceSheet) { item in
Group {
switch item {
case .magnet:
MagnetChoiceView()
.environmentObject(debridManager)
.environmentObject(scrapingModel)
.environmentObject(navModel)
case .batch:
BatchChoiceView()
.environmentObject(debridManager)
.environmentObject(scrapingModel)
.environmentObject(navModel)
}
navModel.showSearchProgress = false
}
.dynamicAccentColor(.primary)
}
.sheet(isPresented: $navModel.showActivityView) {
if #available(iOS 16, *) {
AppActivityView(activityItems: navModel.activityItems)
.presentationDetents([.medium])
} else {
AppActivityView(activityItems: navModel.activityItems)
}
}
.navigationTitle("Search")
}
.sheet(item: $navModel.currentChoiceSheet) { item in
Group {
switch item {
case .magnet:
MagnetChoiceView()
case .batch:
BatchChoiceView()
}
.navigationSearchBar {
SearchBar("Search", text: $scrapingModel.searchText, isEditing: $navModel.isEditingSearch,
onCommit: {
scrapingModel.runningSearchTask = Task {
navModel.isSearching = true
navModel.showSearchProgress = true
await scrapingModel.scanSources(sources: sources.compactMap { $0 })
if realDebridEnabled, !scrapingModel.searchResults.isEmpty {
await debridManager.populateDebridHashes(scrapingModel.searchResults)
}
navModel.showSearchProgress = false
}
})
.showsCancelButton(navModel.isEditingSearch || navModel.isSearching)
.onCancel {
scrapingModel.searchResults = []
scrapingModel.runningSearchTask?.cancel()
scrapingModel.runningSearchTask = nil
navModel.isSearching = false
scrapingModel.searchText = ""
}
}
.tint(.primary)
}
.activitySheet($navModel.currentActivityItem)
}
}

View file

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

View file

@ -5,11 +5,11 @@
// Created by Brian Dashore on 7/20/22.
//
import ActivityView
import SwiftUI
import SwiftUIX
struct MagnetChoiceView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject var scrapingModel: ScrapingViewModel
@EnvironmentObject var debridManager: DebridManager
@ -20,13 +20,13 @@ struct MagnetChoiceView: View {
@State private var showActivityView = false
@State private var showLinkCopyAlert = false
@State private var showMagnetCopyAlert = false
@State private var activityItem: ActivityItem?
@State private var activityItems: [Any] = []
var body: some View {
NavView {
Form {
if realDebridEnabled, debridManager.matchSearchResult(result: scrapingModel.selectedSearchResult) != .none {
Section("Real Debrid options") {
Section(header: "Real Debrid options") {
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") {
navModel.runDebridAction(action: .outplayer, urlString: debridManager.realDebridDownloadUrl)
}
@ -52,16 +52,15 @@ struct MagnetChoiceView: View {
}
ListRowButtonView("Share download URL", systemImage: "square.and.arrow.up.fill") {
guard let url = URL(string: debridManager.realDebridDownloadUrl) else {
return
if let url = URL(string: debridManager.realDebridDownloadUrl) {
activityItems = [url]
navModel.showActivityView.toggle()
}
activityItem = ActivityItem(items: url)
}
}
}
Section("Magnet options") {
Section(header: "Magnet options") {
ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") {
UIPasteboard.general.string = scrapingModel.selectedSearchResult?.magnetLink
showMagnetCopyAlert.toggle()
@ -76,7 +75,8 @@ struct MagnetChoiceView: View {
ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") {
if let result = scrapingModel.selectedSearchResult, let url = URL(string: result.magnetLink) {
activityItem = ActivityItem(items: url)
activityItems = [url]
navModel.showActivityView.toggle()
}
}
@ -87,7 +87,14 @@ struct MagnetChoiceView: View {
}
}
}
.activitySheet($activityItem)
.sheet(isPresented: $navModel.showActivityView) {
if #available(iOS 16, *) {
AppActivityView(activityItems: activityItems)
.presentationDetents([.medium])
} else {
AppActivityView(activityItems: activityItems)
}
}
.navigationTitle("Link actions")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
@ -95,7 +102,7 @@ struct MagnetChoiceView: View {
Button("Done") {
debridManager.realDebridDownloadUrl = ""
dismiss()
presentationMode.wrappedValue.dismiss()
}
}
}

View file

@ -51,6 +51,7 @@ struct MainView: View {
}
}
.font(.caption)
.shadow(radius: 10)
.animation(.easeInOut(duration: 0.3), value: toastModel.showToast)
}
}

View file

@ -8,9 +8,6 @@
import SwiftUI
struct SearchResultsView: View {
@Environment(\.isSearching) var isSearching
@Environment(\.dismissSearch) var dismissSearch
@EnvironmentObject var scrapingModel: ScrapingViewModel
@EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var navModel: NavigationViewModel
@ -29,7 +26,10 @@ struct SearchResultsView: View {
case .full:
Task {
await debridManager.fetchRdDownload(searchResult: result)
navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl)
if !debridManager.realDebridDownloadUrl.isEmpty {
navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl)
}
}
case .partial:
if debridManager.setSelectedRdResult(result: result) {
@ -43,7 +43,7 @@ struct SearchResultsView: View {
.font(.callout)
.fixedSize(horizontal: false, vertical: true)
}
.tint(.primary)
.dynamicAccentColor(.primary)
.padding(.bottom, 5)
SearchResultRDView(result: result)
@ -51,6 +51,7 @@ struct SearchResultsView: View {
}
}
}
.listStyle(.insetGrouped)
.overlay {
if scrapingModel.searchResults.isEmpty {
if navModel.showSearchProgress {
@ -58,7 +59,7 @@ struct SearchResultsView: View {
ProgressView()
Text("Loading \(scrapingModel.currentSourceName ?? "")")
}
} else if isSearching, scrapingModel.runningSearchTask != nil {
} else if navModel.isSearching, scrapingModel.runningSearchTask != nil {
Text("No results found")
}
}
@ -66,25 +67,19 @@ struct SearchResultsView: View {
.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
dismissSearch()
navModel.isSearching = false
scrapingModel.searchText = ""
}
}
.onChange(of: scrapingModel.searchResults) { _ in
// Cleans up any leftover search results in the event of an abrupt cancellation
if !isSearching {
if !navModel.isSearching {
scrapingModel.searchResults = []
}
}
.onChange(of: isSearching) { changed in
// Clear the results array and cleans up search tasks on cancel
if !changed {
scrapingModel.searchResults = []
scrapingModel.runningSearchTask?.cancel()
scrapingModel.runningSearchTask = nil
}
}
}
}

View file

@ -22,7 +22,7 @@ struct SettingsView: View {
var body: some View {
NavView {
Form {
Section("Debrid services") {
Section(header: "Debrid services") {
HStack {
Text("Real Debrid")
Spacer()
@ -42,11 +42,11 @@ struct SettingsView: View {
}
}
Section("Source management") {
Section(header: "Source management") {
NavigationLink("Source lists", destination: SettingsSourceListView())
}
Section("Default actions") {
Section(header: "Default actions") {
if realDebridEnabled {
NavigationLink(
destination: DebridActionPickerView(),

View file

@ -8,7 +8,7 @@
import SwiftUI
struct SourceListEditorView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var sourceManager: SourceManager
@ -42,7 +42,7 @@ struct SourceListEditorView: View {
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
presentationMode.wrappedValue.dismiss()
}
}
@ -53,7 +53,7 @@ struct SourceListEditorView: View {
sourceUrl: sourceUrl,
existingSourceList: navModel.selectedSourceList
) {
dismiss()
presentationMode.wrappedValue.dismiss()
}
}
}

View file

@ -8,15 +8,15 @@
import SwiftUI
struct SourceSettingsView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject var navModel: NavigationViewModel
var body: some View {
NavView {
Form {
List {
if let selectedSource = navModel.selectedSource {
Section("Info") {
Section(header: "Info") {
VStack(alignment: .leading, spacing: 5) {
HStack {
Text(selectedSource.name)
@ -53,6 +53,7 @@ struct SourceSettingsView: View {
SourceSettingsMethodView(selectedSource: selectedSource)
}
}
.listStyle(.insetGrouped)
.onDisappear {
PersistenceController.shared.save()
}
@ -60,7 +61,7 @@ struct SourceSettingsView: View {
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
presentationMode.wrappedValue.dismiss()
}
}
}
@ -71,29 +72,25 @@ struct SourceSettingsView: View {
struct SourceSettingsBaseUrlView: View {
@ObservedObject var selectedSource: Source
@FocusState var baseUrlFocused: Bool
@State private var tempBaseUrl: String = ""
var body: some View {
Section(
header: Text("Base URL"),
footer: Text("Enter the base URL of your server.")
) {
TextField("https://...", text: $tempBaseUrl)
.keyboardType(.URL)
.focused($baseUrlFocused)
.onChange(of: baseUrlFocused) { isFocused in
if !isFocused {
if tempBaseUrl.last == "/" {
selectedSource.baseUrl = String(tempBaseUrl.dropLast())
} else {
selectedSource.baseUrl = tempBaseUrl
}
TextField("https://...", text: $tempBaseUrl, onEditingChanged: { isFocused in
if !isFocused {
if tempBaseUrl.last == "/" {
selectedSource.baseUrl = String(tempBaseUrl.dropLast())
} else {
selectedSource.baseUrl = tempBaseUrl
}
}
.onAppear {
tempBaseUrl = selectedSource.baseUrl ?? ""
}
})
.keyboardType(.URL)
.onAppear {
tempBaseUrl = selectedSource.baseUrl ?? ""
}
}
}
}
@ -101,9 +98,6 @@ struct SourceSettingsBaseUrlView: View {
struct SourceSettingsApiView: View {
@ObservedObject var selectedSourceApi: SourceApi
@FocusState var clientIdFieldFocused: Bool
@FocusState var tokenFieldFocused: Bool
@State private var tempClientId: String = ""
@State private var tempClientSecret: String = ""
@ -117,31 +111,27 @@ struct SourceSettingsApiView: View {
footer: Text("Grab the required API credentials from the website. A client secret can be an API token.")
) {
if selectedSourceApi.dynamicClientId {
TextField("Client ID", text: $tempClientId)
.textInputAutocapitalization(.never)
.focused($clientIdFieldFocused)
.onChange(of: clientIdFieldFocused) { isFocused in
if !isFocused {
selectedSourceApi.clientId = tempClientId
}
}
.onAppear {
tempClientId = selectedSourceApi.clientId ?? ""
TextField("Client ID", text: $tempClientId, onEditingChanged: { isFocused in
if !isFocused {
selectedSourceApi.clientId = tempClientId
}
})
.autocapitalization(.none)
.onAppear {
tempClientId = selectedSourceApi.clientId ?? ""
}
}
if selectedSourceApi.clientSecret != nil {
TextField("Token", text: $tempClientSecret)
.textInputAutocapitalization(.never)
.focused($tokenFieldFocused)
.onChange(of: clientIdFieldFocused) { isFocused in
if !isFocused {
selectedSourceApi.clientSecret = tempClientSecret
}
}
.onAppear {
tempClientSecret = selectedSourceApi.clientSecret ?? ""
TextField("Token", text: $tempClientSecret, onEditingChanged: { isFocused in
if !isFocused {
selectedSourceApi.clientSecret = tempClientSecret
}
})
.autocapitalization(.none)
.onAppear {
tempClientSecret = selectedSourceApi.clientSecret ?? ""
}
}
}
}

View file

@ -36,11 +36,13 @@ struct SourcesView: View {
return tempSources
}
@State private var viewTask: Task<Void, Never>? = nil
var body: some View {
NavView {
List {
if !updatedSources.isEmpty {
Section("Updates") {
Section(header: "Updates") {
ForEach(updatedSources, id: \.self) { source in
SourceUpdateButtonView(updatedSource: source)
}
@ -48,13 +50,10 @@ struct SourcesView: View {
}
if !sources.isEmpty {
Section("Installed") {
Section(header: "Installed") {
ForEach(sources, id: \.self) { source in
InstalledSourceView(installedSource: source)
}
.sheet(isPresented: $navModel.showSourceSettings) {
SourceSettingsView()
}
}
}
@ -67,7 +66,7 @@ struct SourcesView: View {
}
)
}) {
Section("Catalog") {
Section(header: "Catalog") {
ForEach(sourceManager.availableSources, id: \.self) { availableSource in
if !sources.contains(
where: {
@ -82,8 +81,18 @@ struct SourcesView: View {
}
}
}
.task {
await sourceManager.fetchSourcesFromUrl()
.listStyle(.insetGrouped)
.sheet(isPresented: $navModel.showSourceSettings) {
SourceSettingsView()
.environmentObject(navModel)
}
.onAppear {
viewTask = Task {
await sourceManager.fetchSourcesFromUrl()
}
}
.onDisappear {
viewTask?.cancel()
}
.navigationTitle("Sources")
}

View file

@ -18,9 +18,7 @@ I also wanted to support the use of RealDebrid since there aren't any (free) opt
## What iOS versions are supported?
iOS 15 and up. This is because SwiftUI is very flexible with these versions. I have no intention of backporting Ferrite to iOS 14, so please update if you want to use it.
However, if you want an immersive browser for your websites with iOS 14 support, please look at my sister app Asobi.
iOS 14 and up. I was able to successfully backport the app!
## Planned features

View file

@ -1,124 +0,0 @@
# Sources
This is a tentative guide for Ferrite sources. The source format can change at any time without warning throughout the duration of the alpha so do not jump the gun if you don't want to.
## Source lists
Source lists must adhere to the following template. All of these fields are `required` to properly add a source to Ferrite.
(Note: Name and author fields are not enforced, but they will be in future versions of Ferrite)
```json
{
"name": "Repository name",
"author": "Repository author",
"sources": ["source objects go here"]
}
```
## Creating a source object
Here is a quick template to what a source entry looks like with all the possible parameters. You can copy/paste this template into your editor of choice. I will go through all the keys one-by-one.
```json
{
"name": "Website (source) name",
"version": "1",
"baseUrl": "https://sourceurl.com",
"htmlParser": {
"searchUrl": "?q={query}",
"rows": "row selector",
"magnet": {
"query": "magnet selector",
"externalLinkQuery": "https://sourceurl.com/magnetUrl"
"attribute": "href",
"regex": "regex"
},
"title": {
"query": "title selector",
"attribute": "text",
"regex": "regex"
},
"size": {
"query": "size selector",
"attribute": "text",
"regex": "regex"
},
"sl": {
"seeders": "seeder selector",
"leechers": "leecher selector",
"combined": "seeder/leecher ratio selector",
"seederRegex": "regex",
"leecherRegex": "regex"
}
}
}
```
### name
`Required`: This is the name that is shown when the user is looking at a source
### version
`Required`: This is the version number of the source. Each update to the source increments the version by 1. Only increment the version when you are sure that the source is ready to be published.
### baseUrl
`Required`: The base URL of the website. For example, `https://google.com` is the base URL of Google. DO NOT include the slash on the end of the base URL otherwise the source will break.
### htmlParser
`Optional`: The web scraping module for a source. Use this if a source does not have an API and allows scraping! (NOTE: API support will be added in a future build of Ferrite, use the htmlParser for now)
### searchUrl
`Required for htmlParser`: The URL given when searching content on a website. For example, when given a URL such as `https://www.google.com/search?q=hello`, the search URL is whatever comes after the base URL (in this case `/search?q=hello`). It is important to include the slash at the beginning otherwise the source will break.
### rows
`Required for htmlParser`: The CSS selector for selecting a table row. Most of these sites use HTML tables. Please consult this while web scraping.
### magnet
`Required for htmlParser`: A complex query. Please reference complex queries to understand the other keys. Unique keys are provided below:
- externalLinkQuery: If a magnet link is located on a different page, this fetches the URL required to navigate to that page and fetch the magnet link.
### title
`Optional for htmlParser`: This is a complex query. Please reference complex queries to understand the keys
### size
`Optional for htmlParser`: This is a complex query. Please reference complex queries to understand the keys
### sl (seeders and leechers)
`Optional for htmlParser`: Used to get seeder and leecher values on a website. All the below properties are optional.
- seeders: The seeder CSS selector
- leechers: The leecher CSS selector
- combined: A CSS selector used when seeders and leechers are in one string (ex. `Seeders: 100 / Leechers: 200`)
- seederRegex: Regex used to strip the seeder value from a string (follows the same rules as complex query regexes)
- leecherRegex: Regex used to strip the leecher value from a string (follows the same rules as complex query regexes)
## Complex queries
These are generic queries used by Ferrite for keys that require a little more information when parsing the contents. Any key that has a complex query disclaimer will always use these parameters:
- query `Required`: The CSS selector for selecting the element in question
- attribute `Required`: The attribute to look for after selecting the query (ex. href, title, span). If you want the textContent, use `text` in the attribute parameter.
- regex `Optional`: Runs regex on the query result before presentation to the user.
- Do not include the beginning and end slashes in this string (ex. `/regex/`)
- When using a `\` character, escape it using `\\`
- This regex must have only one capturing group. [Don't know what a capture group is?](https://www.regular-expressions.info/brackets.html)