Settings: Fix Default actions and Kodi

Fix how default actions are picked and add in default app actions
as options for both debrid and magnet defaults. Kodi shows the
action choice sheet with the DisclosureGroup dropped down.

The new Kodi server framework also wasn't implemented in the Kodi
wrapper. Fix that.

Finally, add some iOS 14 fixes and repair the autocorrect search
setting to actually work.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2023-03-21 13:49:19 -04:00
parent edfba1c62e
commit 6e95c6072c
20 changed files with 197 additions and 118 deletions

View file

@ -16,6 +16,8 @@
0C0D50E7288DFF850035ECC8 /* PluginAggregateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* PluginAggregateView.swift */; }; 0C0D50E7288DFF850035ECC8 /* PluginAggregateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* PluginAggregateView.swift */; };
0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; }; 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; };
0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; }; 0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; };
0C1A3E5229C8A7F500DA9730 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */; };
0C1A3E5629C9488C00DA9730 /* CodableWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */; };
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; }; 0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; };
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; }; 0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; };
0C2D9653299316CC00A504B6 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2D9652299316CC00A504B6 /* Tag.swift */; }; 0C2D9653299316CC00A504B6 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2D9652299316CC00A504B6 /* Tag.swift */; };
@ -153,6 +155,8 @@
0C0D50E6288DFF850035ECC8 /* PluginAggregateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginAggregateView.swift; sourceTree = "<group>"; }; 0C0D50E6288DFF850035ECC8 /* PluginAggregateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginAggregateView.swift; sourceTree = "<group>"; };
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.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>"; }; 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = "<group>"; };
0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = "<group>"; };
0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableWrapper.swift; sourceTree = "<group>"; };
0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.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>"; }; 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>"; }; 0C2D9652299316CC00A504B6 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
@ -369,6 +373,7 @@
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */, 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */, 0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */,
0C6771F929B3D1AE005D38D2 /* KodiModels.swift */, 0C6771F929B3D1AE005D38D2 /* KodiModels.swift */,
0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */,
); );
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
@ -414,12 +419,13 @@
path = Plugin; path = Plugin;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
0C44E2A628D4DDC6007711AE /* Classes */ = { 0C44E2A628D4DDC6007711AE /* Utils */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0C44E2A728D4DDDC007711AE /* Application.swift */, 0C44E2A728D4DDDC007711AE /* Application.swift */,
0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */,
); );
path = Classes; path = Utils;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
0C44E2A928D4DFC4007711AE /* Modifiers */ = { 0C44E2A928D4DFC4007711AE /* Modifiers */ = {
@ -494,7 +500,7 @@
0C0D50E3288DFE6E0035ECC8 /* Models */, 0C0D50E3288DFE6E0035ECC8 /* Models */,
0CA148EF2889061600DE2211 /* ViewModels */, 0CA148EF2889061600DE2211 /* ViewModels */,
0CA148EE2889061200DE2211 /* Views */, 0CA148EE2889061200DE2211 /* Views */,
0C44E2A628D4DDC6007711AE /* Classes */, 0C44E2A628D4DDC6007711AE /* Utils */,
0C5005552992B9C20064606A /* Protocols */, 0C5005552992B9C20064606A /* Protocols */,
0CA148C8288903F000DE2211 /* Extensions */, 0CA148C8288903F000DE2211 /* Extensions */,
0CA148C5288903F000DE2211 /* Preview Content */, 0CA148C5288903F000DE2211 /* Preview Content */,
@ -775,6 +781,7 @@
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */, 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */, 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */,
0C50055A2992BA6A0064606A /* PluginTag+CoreDataClass.swift in Sources */, 0C50055A2992BA6A0064606A /* PluginTag+CoreDataClass.swift in Sources */,
0C1A3E5229C8A7F500DA9730 /* SettingsModels.swift in Sources */,
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */, 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */, 0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */,
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */, 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */,
@ -828,6 +835,7 @@
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */, 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */,
0CA148E3288903F000DE2211 /* Task.swift in Sources */, 0CA148E3288903F000DE2211 /* Task.swift in Sources */,
0CA148E7288903F000DE2211 /* LoggingManager.swift in Sources */, 0CA148E7288903F000DE2211 /* LoggingManager.swift in Sources */,
0C1A3E5629C9488C00DA9730 /* CodableWrapper.swift in Sources */,
0C6771FC29B3E0DB005D38D2 /* HybridSecureField.swift in Sources */, 0C6771FC29B3E0DB005D38D2 /* HybridSecureField.swift in Sources */,
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */, 0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */,
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */, 0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */,

View file

@ -94,27 +94,21 @@ public class Kodi {
} }
} }
public func sendVideoUrl(urlString: String) async throws { public func sendVideoUrl(urlString: String, server: KodiServer) async throws {
guard let baseUrl = UserDefaults.standard.string(forKey: "ExternalServices.KodiUrl") else {
throw KodiError.InvalidBaseUrl
}
if URL(string: urlString) == nil { if URL(string: urlString) == nil {
throw KodiError.InvalidPlaybackUrl throw KodiError.InvalidPlaybackUrl
} }
let username = UserDefaults.standard.string(forKey: "ExternalServices.KodiUsername")
let password = UserDefaults.standard.string(forKey: "ExternalServices.KodiPassword")
let requestBody = RPCPayload( let requestBody = RPCPayload(
method: "Player.Open", method: "Player.Open",
params: Params(item: Item(file: urlString)) params: Params(item: Item(file: urlString))
) )
var request = URLRequest(url: URL(string: "\(baseUrl)/jsonrpc")!) var request = URLRequest(url: URL(string: "\(server.urlString)/jsonrpc")!)
request.httpMethod = "POST" request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let username, let password { if let username = server.username, let password = server.password {
request.setValue("Basic \(Data("\(username):\(password)".utf8).base64EncodedString())", forHTTPHeaderField: "Authorization") request.setValue("Basic \(Data("\(username):\(password)".utf8).base64EncodedString())", forHTTPHeaderField: "Authorization")
} }

View file

@ -0,0 +1,19 @@
//
// SettingsModels.swift
// Ferrite
//
// Created by Brian Dashore on 3/20/23.
//
import Foundation
enum DefaultAction: Codable, CaseIterable, Hashable {
static var allCases: [DefaultAction] {
[.none, .share, .kodi, .custom(name: "", listId: "")]
}
case none
case share
case kodi
case custom(name: String, listId: String)
}

View file

@ -0,0 +1,36 @@
//
// CodableWrapper.swift
// Ferrite
//
// Created by Brian Dashore on 3/20/23.
//
// From https://forums.swift.org/t/rawrepresentable-conformance-leads-to-crash/51912/4
// Prevents recursion when using Codable with RawRepresentable without needing manual conformance
import Foundation
struct CodableWrapper<Value: Codable> {
var value: Value
}
extension CodableWrapper: RawRepresentable {
var rawValue: String {
guard
let data = try? JSONEncoder().encode(value),
let string = String(data: data, encoding: .utf8)
else {
return ""
}
return string
}
init?(rawValue: String) {
guard
let data = rawValue.data(using: .utf8),
let decoded = try? JSONDecoder().decode(Value.self, from: data)
else {
return nil
}
value = decoded
}
}

View file

@ -48,7 +48,7 @@ public class NavigationViewModel: ObservableObject {
@Published var selectedTitle: String = "" @Published var selectedTitle: String = ""
@Published var selectedBatchTitle: String = "" @Published var selectedBatchTitle: String = ""
@Published var hideNavigationBar = false @Published var kodiExpanded: Bool = false
@Published var currentChoiceSheet: ChoiceSheetType? @Published var currentChoiceSheet: ChoiceSheetType?
var activityItems: [Any] = [] var activityItems: [Any] = []

View file

@ -258,57 +258,51 @@ public class PluginManager: ObservableObject {
} }
@MainActor @MainActor
public func runDebridAction(urlString: String?, navModel: NavigationViewModel) { public func runDefaultAction(urlString: String?, navModel: NavigationViewModel) {
let context = PersistenceController.shared.backgroundContext let context = PersistenceController.shared.backgroundContext
if guard let urlString else {
let defaultDebridActionName = UserDefaults.standard.string(forKey: "Actions.DefaultDebridName"), logManager?.error("Default action: Could not run because the URL is invalid")
let defaultDebridActionList = UserDefaults.standard.string(forKey: "Actions.DefaultDebridList") return
{
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 {
navModel.currentChoiceSheet = .action
UserDefaults.standard.set(nil, forKey: "Actions.DefaultDebridName")
UserDefaults.standard.set(nil, forKey: "Action.DefaultDebridList")
actionErrorAlertMessage =
"The default action could not be run. The action choice sheet has been opened. \n\n" +
"Please check your default actions in Settings"
showActionErrorAlert.toggle()
}
} else {
navModel.currentChoiceSheet = .action
} }
}
@MainActor let defaultsKey: String
public func runMagnetAction(urlString: String?, navModel: NavigationViewModel) { // Assume this is a magnet link
let context = PersistenceController.shared.backgroundContext if urlString.starts(with: "magnet") {
defaultsKey = "Actions.DefaultMagnet"
} else {
defaultsKey = "Actions.DefaultDebrid"
}
if if
let defaultMagnetActionName = UserDefaults.standard.string(forKey: "Actions.DefaultMagnetName"), let rawValue = UserDefaults.standard.string(forKey: defaultsKey),
let defaultMagnetActionList = UserDefaults.standard.string(forKey: "Actions.DefaultMagnetList") let defaultAction = CodableWrapper<DefaultAction>(rawValue: rawValue)?.value
{ {
let actionFetchRequest = Action.fetchRequest() switch defaultAction {
actionFetchRequest.fetchLimit = 1 case .none:
actionFetchRequest.predicate = NSPredicate(format: "name == %@ AND listId == %@", defaultMagnetActionName, defaultMagnetActionList)
if let fetchedAction = try? context.fetch(actionFetchRequest).first {
runDeeplinkAction(fetchedAction, urlString: urlString)
} else {
navModel.currentChoiceSheet = .action navModel.currentChoiceSheet = .action
UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetName") case .share:
UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetList") navModel.activityItems = [urlString]
navModel.currentChoiceSheet = .activity
case .kodi:
navModel.kodiExpanded = true
navModel.currentChoiceSheet = .action
case .custom(let name, let listId):
let actionFetchRequest = Action.fetchRequest()
actionFetchRequest.fetchLimit = 1
actionFetchRequest.predicate = NSPredicate(format: "name == %@ AND listId == %@", name, listId)
actionErrorAlertMessage = if let fetchedAction = try? context.fetch(actionFetchRequest).first {
"The default action could not be run. The action choice sheet has been opened. \n\n" + runDeeplinkAction(fetchedAction, urlString: urlString)
"Please check your default actions in Settings" } else {
showActionErrorAlert.toggle() navModel.currentChoiceSheet = .action
UserDefaults.standard.set(CodableWrapper<DefaultAction>(value: .none).rawValue, forKey: "Actions.DefaultDebrid")
actionErrorAlertMessage =
"The default action could not be run. The action choice sheet has been opened. \n\n" +
"Please check your default actions in Settings"
showActionErrorAlert.toggle()
}
} }
} else { } else {
navModel.currentChoiceSheet = .action navModel.currentChoiceSheet = .action
@ -340,7 +334,7 @@ public class PluginManager: ObservableObject {
} }
@MainActor @MainActor
public func sendToKodi(urlString: String?) async { public func sendToKodi(urlString: String?, server: KodiServer) async {
guard let urlString else { guard let urlString else {
actionErrorAlertMessage = "Could not send URL to Kodi since there is no playback URL to send" actionErrorAlertMessage = "Could not send URL to Kodi since there is no playback URL to send"
showActionErrorAlert.toggle() showActionErrorAlert.toggle()
@ -351,7 +345,7 @@ public class PluginManager: ObservableObject {
} }
do { do {
try await kodi.sendVideoUrl(urlString: urlString) try await kodi.sendVideoUrl(urlString: urlString, server: server)
actionSuccessAlertMessage = "Your URL should be playing on Kodi" actionSuccessAlertMessage = "Your URL should be playing on Kodi"
showActionSuccessAlert.toggle() showActionSuccessAlert.toggle()

View file

@ -12,18 +12,10 @@ struct CustomScopeBarModifier<V: View>: ViewModifier {
let hostingContent: V let hostingContent: V
@State private var hostingController: UIHostingController<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 { func body(content: Content) -> some View {
if #available(iOS 15, *) { if #available(iOS 15, *) {
content content
.backport.introspectSearchController { searchController in .backport.introspectSearchController { searchController in
searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no
searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none
// MARK: One-time setup // MARK: One-time setup
guard hostingController == nil else { return } guard hostingController == nil else { return }
@ -64,10 +56,6 @@ struct CustomScopeBarModifier<V: View>: ViewModifier {
content content
Spacer() Spacer()
} }
.backport.introspectSearchController { searchController in
searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no
searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none
}
} }
} }
} }

View file

@ -39,7 +39,7 @@ struct AllDebridCloudView: View {
if !debridManager.downloadUrl.isEmpty { if !debridManager.downloadUrl.isEmpty {
historyInfo.url = debridManager.downloadUrl historyInfo.url = debridManager.downloadUrl
PersistenceController.shared.createHistory(historyInfo, performSave: true) PersistenceController.shared.createHistory(historyInfo, performSave: true)
pluginManager.runDebridAction( pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl, urlString: debridManager.downloadUrl,
navModel: navModel navModel: navModel
) )

View file

@ -39,7 +39,7 @@ struct PremiumizeCloudView: View {
performSave: true performSave: true
) )
pluginManager.runDebridAction( pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl, urlString: debridManager.downloadUrl,
navModel: navModel navModel: navModel
) )

View file

@ -36,7 +36,7 @@ struct RealDebridCloudView: View {
performSave: true performSave: true
) )
pluginManager.runDebridAction( pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl, urlString: debridManager.downloadUrl,
navModel: navModel navModel: navModel
) )
@ -76,7 +76,7 @@ struct RealDebridCloudView: View {
historyInfo.url = debridManager.downloadUrl historyInfo.url = debridManager.downloadUrl
PersistenceController.shared.createHistory(historyInfo, performSave: true) PersistenceController.shared.createHistory(historyInfo, performSave: true)
pluginManager.runDebridAction( pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl, urlString: debridManager.downloadUrl,
navModel: navModel navModel: navModel
) )

View file

@ -24,7 +24,7 @@ struct HistoryButtonView: View {
if url.starts(with: "https://") { if url.starts(with: "https://") {
Task { Task {
debridManager.downloadUrl = url debridManager.downloadUrl = url
pluginManager.runDebridAction( pluginManager.runDefaultAction(
urlString: url, urlString: url,
navModel: navModel navModel: navModel
) )
@ -34,7 +34,7 @@ struct HistoryButtonView: View {
} }
} }
} else { } else {
pluginManager.runMagnetAction( pluginManager.runDefaultAction(
urlString: url, urlString: url,
navModel: navModel navModel: navModel
) )

View file

@ -80,6 +80,7 @@ struct PluginAggregateView<P: Plugin, PJ: PluginJson>: View {
.onChange(of: installedPlugins.count) { newCount in .onChange(of: installedPlugins.count) { newCount in
pluginsEmpty = newCount == 0 pluginsEmpty = newCount == 0
} }
.id(UUID())
} }
} }
} }

View file

@ -43,7 +43,7 @@ struct SearchResultButtonView: View {
performSave: true performSave: true
) )
pluginManager.runDebridAction( pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl, urlString: debridManager.downloadUrl,
navModel: navModel navModel: navModel
) )
@ -72,7 +72,7 @@ struct SearchResultButtonView: View {
performSave: true performSave: true
) )
pluginManager.runMagnetAction( pluginManager.runDefaultAction(
urlString: result.magnet.link, urlString: result.magnet.link,
navModel: navModel navModel: navModel
) )

View file

@ -11,8 +11,8 @@ struct DefaultActionPickerView: View {
@EnvironmentObject var logManager: LoggingManager @EnvironmentObject var logManager: LoggingManager
let actionRequirement: ActionRequirement let actionRequirement: ActionRequirement
@Binding var defaultActionName: String?
@Binding var defaultActionList: String? @Binding var defaultAction: DefaultAction
@FetchRequest( @FetchRequest(
entity: Action.entity(), entity: Action.entity(),
@ -24,19 +24,25 @@ struct DefaultActionPickerView: View {
sortDescriptors: [] sortDescriptors: []
) var pluginLists: FetchedResults<PluginList> ) var pluginLists: FetchedResults<PluginList>
@FetchRequest(
entity: KodiServer.entity(),
sortDescriptors: []
) var kodiServers: FetchedResults<KodiServer>
var body: some View { var body: some View {
List { List {
UserChoiceButton( DefaultChoiceButton(defaultAction: $defaultAction, selectedOption: .none)
defaultActionName: $defaultActionName, DefaultChoiceButton(defaultAction: $defaultAction, selectedOption: .share)
defaultActionList: $defaultActionList,
pluginLists: pluginLists
)
if actionRequirement == .debrid && !kodiServers.isEmpty {
DefaultChoiceButton(defaultAction: $defaultAction, selectedOption: .kodi)
}
// Handle custom here
ForEach(actions.filter { $0.requires.contains(actionRequirement.rawValue) }, id: \.id) { action in ForEach(actions.filter { $0.requires.contains(actionRequirement.rawValue) }, id: \.id) { action in
Button { Button {
if let actionListId = action.listId?.uuidString { if let actionListId = action.listId?.uuidString {
defaultActionName = action.name defaultAction = .custom(name: action.name, listId: actionListId)
defaultActionList = actionListId
} else { } else {
logManager.error( logManager.error(
"Default action: This action doesn't have a corresponding plugin list! Please uninstall the action" "Default action: This action doesn't have a corresponding plugin list! Please uninstall the action"
@ -63,9 +69,9 @@ struct DefaultActionPickerView: View {
Spacer() Spacer()
if if
let defaultActionList, case let .custom(name, listId) = defaultAction,
action.listId?.uuidString == defaultActionList, action.listId?.uuidString == listId,
action.name == defaultActionName action.name == name
{ {
Image(systemName: "checkmark") Image(systemName: "checkmark")
.foregroundColor(.blue) .foregroundColor(.blue)
@ -77,28 +83,24 @@ struct DefaultActionPickerView: View {
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.inlinedList(inset: -20) .inlinedList(inset: -20)
.navigationTitle("Default \(actionRequirement == .debrid ? "debrid" : "magnet") action") .navigationTitle("Default \(actionRequirement == .debrid ? "Debrid" : "Magnet") Action")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
} }
private struct UserChoiceButton: View { private struct DefaultChoiceButton: View {
@Binding var defaultActionName: String? @Binding var defaultAction: DefaultAction
@Binding var defaultActionList: String? let selectedOption: DefaultAction
var pluginLists: FetchedResults<PluginList>
var body: some View { var body: some View {
Button { Button {
defaultActionName = nil defaultAction = selectedOption
defaultActionList = nil
} label: { } label: {
HStack { HStack {
Text("Let me choose") Text(fetchButtonName())
Spacer() Spacer()
// Force "Let me choose" if the name OR list ID is nil if defaultAction == selectedOption {
// Prevents any mismatches
if defaultActionName == nil || pluginLists.contains(where: { $0.id.uuidString == defaultActionList }) {
Image(systemName: "checkmark") Image(systemName: "checkmark")
.foregroundColor(.blue) .foregroundColor(.blue)
} }
@ -106,4 +108,18 @@ private struct UserChoiceButton: View {
} }
.backport.tint(.primary) .backport.tint(.primary)
} }
func fetchButtonName() -> String {
switch selectedOption {
case .none:
return "Let me choose"
case .share:
return "Share link"
case .kodi:
return "Open in Kodi"
case .custom(_, _):
// This should not be called
return "Custom button"
}
}
} }

View file

@ -44,6 +44,7 @@ struct SettingsDebridInfoView: View {
} }
} }
} }
.listStyle(.insetGrouped)
.navigationTitle(debridType.toString()) .navigationTitle(debridType.toString())
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }

View file

@ -15,6 +15,7 @@ struct ContentView: View {
@EnvironmentObject var pluginManager: PluginManager @EnvironmentObject var pluginManager: PluginManager
@EnvironmentObject var logManager: LoggingManager @EnvironmentObject var logManager: LoggingManager
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch: Bool = false
@AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText: Bool = false @AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText: Bool = false
@State private var isEditingSearch = false @State private var isEditingSearch = false
@ -110,6 +111,11 @@ struct ContentView: View {
searchBarText = getSearchBarText() searchBarText = getSearchBarText()
} }
} }
.autocorrectionDisabled(!autocorrectSearch)
.backport.introspectSearchController { searchController in
// TODO: Replace with SwiftUI autocapitalization modifier
searchController.searchBar.autocapitalizationType = .none
}
.navigationSearchBarHiddenWhenScrolling(false) .navigationSearchBarHiddenWhenScrolling(false)
.customScopeBar { .customScopeBar {
SearchFilterHeaderView() SearchFilterHeaderView()

View file

@ -27,14 +27,12 @@ struct SettingsView: View {
@AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true @AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true
@AppStorage("Actions.DefaultDebridName") var defaultDebridActionName: String? @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: CodableWrapper<DefaultAction> = .init(value: .none)
@AppStorage("Actions.DefaultDebridList") var defaultDebridActionList: String? @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: CodableWrapper<DefaultAction> = .init(value: .none)
@AppStorage("Actions.DefaultMagnetName") var defaultMagnetActionName: String?
@AppStorage("Actions.DefaultMagnetList") var defaultMagnetActionList: String?
@AppStorage("Debug.ShowErrorToasts") var showErrorToasts = true @AppStorage("Debug.ShowErrorToasts") var showErrorToasts = true
var body: some View { var body: some View {
NavView { NavView {
Form { Form {
@ -85,17 +83,26 @@ struct SettingsView: View {
NavigationLink( NavigationLink(
destination: DefaultActionPickerView( destination: DefaultActionPickerView(
actionRequirement: .debrid, actionRequirement: .debrid,
defaultActionName: $defaultDebridActionName, defaultAction: $defaultDebridAction.value
defaultActionList: $defaultDebridActionList
), ),
label: { label: {
HStack { HStack {
Text("Debrid action") Text("Debrid action")
Spacer() Spacer()
// TODO: Maybe make this check for nil list as well? Group {
Text(defaultDebridActionName.map { $0 } ?? "User choice") switch defaultDebridAction.value {
.foregroundColor(.secondary) case .none:
Text("User choice")
case .share:
Text("Share")
case .kodi:
Text("Kodi")
case .custom(let name, _):
Text(name)
}
}
.foregroundColor(.secondary)
} }
} }
) )
@ -104,17 +111,26 @@ struct SettingsView: View {
NavigationLink( NavigationLink(
destination: DefaultActionPickerView( destination: DefaultActionPickerView(
actionRequirement: .magnet, actionRequirement: .magnet,
defaultActionName: $defaultMagnetActionName, defaultAction: $defaultMagnetAction.value
defaultActionList: $defaultMagnetActionList
), ),
label: { label: {
HStack { HStack {
Text("Magnet action") Text("Magnet action")
Spacer() Spacer()
// TODO: Maybe make this check for nil list as well? Group {
Text(defaultMagnetActionName.map { $0 } ?? "User choice") switch defaultMagnetAction.value {
.foregroundColor(.secondary) case .none:
Text("User choice")
case .share:
Text("Share")
case .kodi:
Text("Kodi")
case .custom(let name, _):
Text(name)
}
}
.foregroundColor(.secondary)
} }
} }
) )

View file

@ -57,11 +57,11 @@ struct ActionChoiceView: View {
} }
if !kodiServers.isEmpty { if !kodiServers.isEmpty {
DisclosureGroup("Open in Kodi") { DisclosureGroup("Open in Kodi", isExpanded: $navModel.kodiExpanded) {
ForEach(kodiServers, id: \.self) { server in ForEach(kodiServers, id: \.self) { server in
Button { Button {
Task { Task {
await pluginManager.sendToKodi(urlString: debridManager.downloadUrl) await pluginManager.sendToKodi(urlString: debridManager.downloadUrl, server: server)
} }
} label: { } label: {
KodiServerView(server: server) KodiServerView(server: server)

View file

@ -84,7 +84,7 @@ struct BatchChoiceView: View {
PersistenceController.shared.createHistory(selectedHistoryInfo, performSave: true) PersistenceController.shared.createHistory(selectedHistoryInfo, performSave: true)
} }
pluginManager.runDebridAction( pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl, urlString: debridManager.downloadUrl,
navModel: navModel navModel: navModel
) )