Library: Add searching and cleanup

Add a searchbar to filter through various library entries so it's
easier to find items.

Also add fixes for < iOS 16 devices and fix up searchbar constraints.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2023-01-07 17:38:58 -05:00
parent 2258036f7b
commit e8f62e3cdc
13 changed files with 214 additions and 122 deletions

View file

@ -32,4 +32,6 @@ public class Application {
}
#endif
}
let osVersion: OperatingSystemVersion = ProcessInfo().operatingSystemVersion
}

View file

@ -544,6 +544,9 @@ public class DebridManager: ObservableObject {
} else {
throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API")
}
// Fetch one more time to add updated data into the RD cloud cache
await fetchRdCloud(bypassTTL: true)
} catch {
switch error {
case RealDebrid.RDError.EmptyTorrents:
@ -640,6 +643,9 @@ public class DebridManager: ObservableObject {
} else {
throw AllDebrid.ADError.FailedRequest(description: "Could not fetch your file from AllDebrid's cache or API")
}
// Fetch one more time to add updated data into the AD cloud cache
await fetchAdCloud(bypassTTL: true)
} catch {
await sendDebridError(error, prefix: "AllDebrid download error", cancelString: "Download cancelled")
}
@ -650,7 +656,6 @@ public class DebridManager: ObservableObject {
if bypassTTL || Date().timeIntervalSince1970 > allDebridCloudTTL {
do {
allDebridCloudMagnets = try await allDebrid.userMagnets()
realDebridCloudDownloads = try await realDebrid.userDownloads()
// 5 minutes
allDebridCloudTTL = Date().timeIntervalSince1970 + 300
@ -685,6 +690,9 @@ public class DebridManager: ObservableObject {
throw Premiumize.PMError.FailedRequest(description: "There were no items or files found!")
}
// Fetch one more time to add updated data into the PM cloud cache
await fetchPmCloud(bypassTTL: true)
// Add a PM transfer if the item exists
if let premiumizeItem = selectedPremiumizeItem {
try await premiumize.createTransfer(magnet: premiumizeItem.magnet)

View file

@ -15,14 +15,15 @@ struct BookmarksView: View {
let backgroundContext = PersistenceController.shared.backgroundContext
var bookmarks: FetchedResults<Bookmark>
@Binding var searchText: String
@State private var viewTask: Task<Void, Never>?
@State private var bookmarkPredicate: NSPredicate?
var body: some View {
ZStack {
if !bookmarks.isEmpty {
List {
DynamicFetchRequest(predicate: bookmarkPredicate) { (bookmarks: FetchedResults<Bookmark>) in
List {
if !bookmarks.isEmpty {
ForEach(bookmarks, id: \.self) { bookmark in
SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark)
}
@ -30,43 +31,53 @@ struct BookmarksView: View {
for index in offsets {
if let bookmark = bookmarks[safe: index] {
PersistenceController.shared.delete(bookmark, context: backgroundContext)
NotificationCenter.default.post(name: .didDeleteBookmark, object: bookmark)
}
}
}
.onMove { source, destination in
var changedBookmarks = bookmarks.map { $0 }
changedBookmarks.move(fromOffsets: source, toOffset: destination)
for reverseIndex in stride(from: changedBookmarks.count - 1, through: 0, by: -1) {
changedBookmarks[reverseIndex].orderNum = Int16(reverseIndex)
}
PersistenceController.shared.save()
}
}
.inlinedList()
.listStyle(.insetGrouped)
}
.inlinedList()
.listStyle(.insetGrouped)
.onAppear {
if debridManager.enabledDebrids.count > 0 {
viewTask = Task {
let magnets = bookmarks.compactMap {
if let magnetHash = $0.magnetHash {
return Magnet(hash: magnetHash, link: $0.magnetLink)
} else {
return nil
}
}
await debridManager.populateDebridIA(magnets)
}
}
}
.onDisappear {
viewTask?.cancel()
}
}
.onAppear {
if debridManager.enabledDebrids.count > 0 {
viewTask = Task {
let magnets = bookmarks.compactMap {
if let magnetHash = $0.magnetHash {
return Magnet(hash: magnetHash, link: $0.magnetLink)
} else {
return nil
}
}
await debridManager.populateDebridIA(magnets)
}
}
applyPredicate()
}
.onDisappear {
viewTask?.cancel()
.onChange(of: searchText) { _ in
applyPredicate()
}
}
func applyPredicate() {
bookmarkPredicate = searchText.isEmpty ? nil : NSPredicate(format: "title CONTAINS[cd] %@", searchText)
}
}

View file

@ -11,11 +11,15 @@ struct AllDebridCloudView: View {
@EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var navModel: NavigationViewModel
@Binding var searchText: String
@State private var viewTask: Task<Void, Never>?
var body: some View {
DisclosureGroup("Magnets") {
ForEach(debridManager.allDebridCloudMagnets, id: \.id) { magnet in
ForEach(debridManager.allDebridCloudMagnets.filter {
searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased())
}, id: \.id) { magnet in
Button {
if magnet.status == "Ready" && !magnet.links.isEmpty {
navModel.resultFromCloud = true
@ -38,8 +42,9 @@ struct AllDebridCloudView: View {
}
}
} else {
debridManager.clearIAValues()
let magnet = Magnet(hash: magnet.hash, link: nil)
// Do not clear old IA values
await debridManager.populateDebridIA([magnet])
if debridManager.selectDebridResult(magnet: magnet) {
@ -85,9 +90,3 @@ struct AllDebridCloudView: View {
}
}
}
struct AllDebridCloudView_Previews: PreviewProvider {
static var previews: some View {
AllDebridCloudView()
}
}

View file

@ -12,11 +12,15 @@ struct PremiumizeCloudView: View {
@EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var navModel: NavigationViewModel
@Binding var searchText: String
@State private var viewTask: Task<Void, Never>?
var body: some View {
DisclosureGroup("Items") {
ForEach(debridManager.premiumizeCloudItems, id: \.id) { item in
ForEach(debridManager.premiumizeCloudItems.filter {
searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased())
}, id: \.id) { item in
Button(item.name) {
Task {
navModel.resultFromCloud = true
@ -60,9 +64,3 @@ struct PremiumizeCloudView: View {
}
}
}
struct PremiumizeCloudView_Previews: PreviewProvider {
static var previews: some View {
PremiumizeCloudView()
}
}

View file

@ -11,12 +11,16 @@ struct RealDebridCloudView: View {
@EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var debridManager: DebridManager
@Binding var searchText: String
@State private var viewTask: Task<Void, Never>?
var body: some View {
Group {
DisclosureGroup("Downloads") {
ForEach(debridManager.realDebridCloudDownloads, id: \.self) { downloadResponse in
ForEach(debridManager.realDebridCloudDownloads.filter {
searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased())
}, id: \.self) { downloadResponse in
Button(downloadResponse.filename) {
navModel.resultFromCloud = true
navModel.selectedTitle = downloadResponse.filename
@ -46,7 +50,9 @@ struct RealDebridCloudView: View {
}
DisclosureGroup("Torrents") {
ForEach(debridManager.realDebridCloudTorrents, id: \.self) { torrentResponse in
ForEach(debridManager.realDebridCloudTorrents.filter {
searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased())
}, id: \.self) { torrentResponse in
Button {
if torrentResponse.status == "downloaded" && !torrentResponse.links.isEmpty {
navModel.resultFromCloud = true
@ -69,8 +75,9 @@ struct RealDebridCloudView: View {
}
}
} else {
debridManager.clearIAValues()
let magnet = Magnet(hash: torrentResponse.hash, link: nil)
// Do not clear old IA values
await debridManager.populateDebridIA([magnet])
if debridManager.selectDebridResult(magnet: magnet) {
@ -119,9 +126,3 @@ struct RealDebridCloudView: View {
}
}
}
struct RealDebridCloudView_Previews: PreviewProvider {
static var previews: some View {
RealDebridCloudView()
}
}

View file

@ -10,30 +10,21 @@ import SwiftUI
struct DebridCloudView: View {
@EnvironmentObject var debridManager: DebridManager
@Binding var searchText: String
var body: some View {
NavView {
VStack {
List {
switch debridManager.selectedDebridType {
case .realDebrid:
RealDebridCloudView()
case .premiumize:
PremiumizeCloudView()
case .allDebrid:
AllDebridCloudView()
case .none:
EmptyView()
}
}
.inlinedList()
.listStyle(.grouped)
List {
switch debridManager.selectedDebridType {
case .realDebrid:
RealDebridCloudView(searchText: $searchText)
case .premiumize:
PremiumizeCloudView(searchText: $searchText)
case .allDebrid:
AllDebridCloudView(searchText: $searchText)
case .none:
EmptyView()
}
}
}
}
struct DebridCloudView_Previews: PreviewProvider {
static var previews: some View {
DebridCloudView()
.listStyle(.plain)
}
}

View file

@ -36,7 +36,7 @@ struct HistoryButtonView: View {
toastModel.updateToastDescription("URL invalid. Cannot load this history entry. Please delete it.")
}
} label: {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 10) {
VStack(alignment: .leading) {
Text(entry.name ?? "Unknown title")
.font(entry.subName == nil ? .body : .subheadline)

View file

@ -10,44 +10,89 @@ import SwiftUI
struct HistoryView: View {
@EnvironmentObject var navModel: NavigationViewModel
let backgroundContext = PersistenceController.shared.backgroundContext
var history: FetchedResults<History>
var formatter: DateFormatter = .init()
@State private var historyIndex = 0
@Binding var searchText: String
init(history: FetchedResults<History>) {
self.history = history
formatter.dateStyle = .medium
formatter.timeStyle = .none
}
func groupedEntries(_ result: FetchedResults<History>) -> [[History]] {
Dictionary(grouping: result) { (element: History) in
element.dateString ?? ""
}.values.sorted { $0[0].date ?? Date() > $1[0].date ?? Date() }
}
@State private var historyPredicate: NSPredicate?
var body: some View {
if !history.isEmpty {
DynamicFetchRequest(predicate: historyPredicate) { (allEntries: FetchedResults<HistoryEntry>) in
List {
ForEach(groupedEntries(history), id: \.self) { (section: [History]) in
Section(header: Text(formatter.string(from: section[0].date ?? Date()))) {
ForEach(section, id: \.self) { history in
ForEach(history.entryArray) { entry in
HistoryButtonView(entry: entry)
}
.onDelete { offsets in
removeEntry(at: offsets, from: history)
}
}
if !history.isEmpty {
ForEach(groupedHistory(history), id: \.self) { historyGroup in
HistorySectionView(allEntries: allEntries, historyGroup: historyGroup)
}
}
}
.listStyle(.insetGrouped)
}
.onAppear {
applyPredicate()
}
.onChange(of: searchText) { _ in
applyPredicate()
}
}
func applyPredicate() {
if searchText.isEmpty {
historyPredicate = nil
} else {
let namePredicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText.lowercased())
let subNamePredicate = NSPredicate(format: "subName CONTAINS[cd] %@", searchText.lowercased())
historyPredicate = NSCompoundPredicate(type: .or, subpredicates: [namePredicate, subNamePredicate])
}
}
func groupedHistory(_ result: FetchedResults<History>) -> [[History]] {
return Dictionary(grouping: result) { (element: History) in
element.dateString ?? ""
}
.values
.sorted { $0[0].date ?? Date() > $1[0].date ?? Date() }
}
}
struct HistorySectionView: View {
let backgroundContext = PersistenceController.shared.backgroundContext
var formatter: DateFormatter = .init()
var allEntries: FetchedResults<HistoryEntry>
var historyGroup: [History]
init(allEntries: FetchedResults<HistoryEntry>, historyGroup: [History]) {
self.allEntries = allEntries
self.historyGroup = historyGroup
formatter.dateStyle = .medium
formatter.timeStyle = .none
}
var body: some View {
if compareGroup(historyGroup) > 0 {
Section(header: Text(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)
}
.onDelete { offsets in
removeEntry(at: offsets, from: history)
}
}
}
}
}
func compareGroup(_ group: [History]) -> Int {
var totalCount = 0
for history in group {
totalCount += history.entryArray.reduce(0, { result, item in
result + (allEntries.contains { $0.name == item.name || (item.subName.map { return !$0.isEmpty } ?? false && $0.subName == item.subName) } ? 1 : 0)
})
}
return totalCount
}
func removeEntry(at offsets: IndexSet, from history: History) {

View file

@ -19,6 +19,8 @@ struct ContentView: View {
sortDescriptors: []
) var sources: FetchedResults<Source>
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
@State private var selectedSource: Source? {
didSet {
scrapingModel.filteredSource = selectedSource
@ -72,7 +74,9 @@ struct ContentView: View {
SearchResultsView()
}
.navigationTitle("Search")
.navigationBarTitleDisplayMode(navModel.isSearching ? .inline : .large)
.navigationBarTitleDisplayMode(
navModel.isSearching && Application.shared.osVersion.majorVersion > 14 ? .inline : .large
)
.navigationSearchBar {
SearchBar("Search",
text: $scrapingModel.searchText,
@ -114,8 +118,8 @@ struct ContentView: View {
}
.introspectSearchController { searchController in
searchController.hidesNavigationBarDuringPresentation = false
searchController.searchBar.autocorrectionType = .no
searchController.searchBar.autocapitalizationType = .none
searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no
searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {

View file

@ -6,6 +6,7 @@
//
import SwiftUI
import SwiftUIX
struct LibraryView: View {
enum LibraryPickerSegment {
@ -19,9 +20,7 @@ struct LibraryView: View {
@FetchRequest(
entity: Bookmark.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Bookmark.orderNum, ascending: true)
]
sortDescriptors: []
) var bookmarks: FetchedResults<Bookmark>
@FetchRequest(
@ -31,11 +30,15 @@ struct LibraryView: View {
]
) var history: FetchedResults<History>
@State private var historyEmpty = true
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
@State private var selectedSegment: LibraryPickerSegment = .bookmarks
@State private var editMode: EditMode = .inactive
@State private var searchText: String = ""
@State private var isEditingSearch = false
@State private var isSearching = false
var body: some View {
NavView {
VStack {
@ -48,19 +51,34 @@ struct LibraryView: View {
}
}
.pickerStyle(.segmented)
.padding()
.padding(.horizontal)
.padding(.vertical, 5)
switch selectedSegment {
case .bookmarks:
BookmarksView(bookmarks: bookmarks)
BookmarksView(searchText: $searchText)
case .history:
HistoryView(history: history)
HistoryView(history: history, searchText: $searchText)
case .debridCloud:
DebridCloudView()
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 {
case .bookmarks:
@ -80,7 +98,8 @@ struct LibraryView: View {
.navigationTitle("Library")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
HStack(spacing: Application.shared.osVersion.majorVersion > 14 ? 10 : 18) {
Spacer()
EditButton()
switch selectedSegment {
@ -90,6 +109,7 @@ struct LibraryView: View {
HistoryActionsView()
}
}
.animation(.none)
}
}
.environment(\.editMode, $editMode)

View file

@ -15,6 +15,8 @@ struct SettingsView: View {
let backgroundContext = PersistenceController.shared.backgroundContext
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
@AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true
@AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none
@ -76,6 +78,12 @@ struct SettingsView: View {
}
}
Section(header: Text("Behavior")) {
Toggle(isOn: $autocorrectSearch) {
Text("Autocorrect search")
}
}
Section(header: Text("Source management")) {
NavigationLink("Source lists", destination: SettingsSourceListView())
}

View file

@ -15,13 +15,11 @@ struct SourcesView: View {
let backgroundContext = PersistenceController.shared.backgroundContext
@FetchRequest(
entity: Source.entity(),
sortDescriptors: []
) var sources: FetchedResults<Source>
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
@State private var checkedForSources = false
@State private var isEditing = false
@State private var isEditingSearch = false
@State private var isSearching = false
@State private var viewTask: Task<Void, Never>? = nil
@State private var searchText: String = ""
@ -35,7 +33,7 @@ struct SourcesView: View {
ZStack {
if !checkedForSources {
ProgressView()
} else if sources.isEmpty, sourceManager.availableSources.isEmpty {
} else if installedSources.isEmpty, sourceManager.availableSources.isEmpty {
EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings")
} else {
List {
@ -119,11 +117,18 @@ struct SourcesView: View {
}
.navigationTitle("Sources")
.navigationSearchBar {
SearchBar("Search", text: $searchText, isEditing: $isEditing)
.showsCancelButton(isEditing)
.onCancel {
searchText = ""
}
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
}
}
}