From 92e559d638c44b3ec78b67761d35deb8398f27c1 Mon Sep 17 00:00:00 2001 From: marcoschmickler Date: Fri, 28 Jan 2022 23:04:13 +0100 Subject: [PATCH] Search Tags --- kplayer.xcodeproj/project.pbxproj | 8 ++ kplayer/core/DatabaseManager.swift | 144 +++++++++++++++++++--- kplayer/core/MediaItem.swift | 2 + kplayer/detail/EditItemView.swift | 26 ++++ kplayer/master/MasterViewController.swift | 19 +++ kplayer/master/SearchItemView.swift | 50 ++++++++ kplayer/util/FlexibleView.swift | 98 +++++++++++++++ 7 files changed, 327 insertions(+), 20 deletions(-) create mode 100644 kplayer/master/SearchItemView.swift create mode 100644 kplayer/util/FlexibleView.swift diff --git a/kplayer.xcodeproj/project.pbxproj b/kplayer.xcodeproj/project.pbxproj index 654a324..df9bcf8 100644 --- a/kplayer.xcodeproj/project.pbxproj +++ b/kplayer.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 1C73631EACF56BABD3B2BCFB /* LayoutTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736BC4450890C45F8FBC63 /* LayoutTools.swift */; }; 1C73633C00C18FDA2E9F0A2F /* KNetworkProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736DCD945ABAE984FF43EF /* KNetworkProtocol.swift */; }; 1C73635138BBD2BB480A308F /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C736777456388CA571DA17B /* MediaPlayer.framework */; }; + 1C7363C2E744C2318879127D /* FlexibleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736D50A22FC4553165199D /* FlexibleView.swift */; }; 1C7363D4C34EBBD5C7AAD0A8 /* scratch.txt in Resources */ = {isa = PBXBuildFile; fileRef = 1C7363E0DDA5854D55F8836E /* scratch.txt */; }; 1C73640D928DE56D35175D39 /* UploadOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736260E748CF136FF37EA7 /* UploadOperation.swift */; }; 1C73646F87B495A47D7943C7 /* NetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7369EC16B19B32B515169E /* NetData.swift */; }; @@ -44,6 +45,7 @@ 1C736A622876405F3EE2D043 /* EditItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7366C09381DC0052B52B69 /* EditItemView.swift */; }; 1C736A7B6221A1D50FB3904C /* ItemType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73631C96E6C860833052CA /* ItemType.swift */; }; 1C736B4B0889BD35DC566124 /* nspersistentcontainer-defaults-swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7361F01841F546FA7AFD58 /* nspersistentcontainer-defaults-swift.swift */; }; + 1C736C8DAD6C2FBB9A2EA625 /* SearchItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73654AB95A2D629833BEC5 /* SearchItemView.swift */; }; 1C736D16E81BA1FB325200E0 /* HanekeFetchOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360744ABACC3557D05760 /* HanekeFetchOperation.swift */; }; 1C736D24891597F2728230EE /* ImageLoadOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360A94DBECA685ED8602F /* ImageLoadOperation.swift */; }; 1C736D24B49451141CD4B64D /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7369F53095B7A4D65679C2 /* DetailViewController.swift */; }; @@ -106,6 +108,7 @@ 1C7364709899FF62774B0199 /* VideoHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoHelper.swift; sourceTree = ""; }; 1C73648CEC974A2500172064 /* ViewControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerExtensions.swift; sourceTree = ""; }; 1C7364F10BED5DA0F1C0423C /* NetworkDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkDelegate.swift; sourceTree = ""; }; + 1C73654AB95A2D629833BEC5 /* SearchItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchItemView.swift; sourceTree = ""; }; 1C736595533B56039C417E0D /* ServerDownloadDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDownloadDelegate.swift; sourceTree = ""; }; 1C73659CC9B523B957E58DC6 /* LocalManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalManager.swift; sourceTree = ""; }; 1C7365B06FA66294E99AC2D3 /* NetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; @@ -127,6 +130,7 @@ 1C736BC4450890C45F8FBC63 /* LayoutTools.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutTools.swift; sourceTree = ""; }; 1C736C94157754DE1C808173 /* KSettingsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KSettingsModel.swift; sourceTree = ""; }; 1C736CF935C2A6AB916BE494 /* scratch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = scratch.txt; sourceTree = ""; }; + 1C736D50A22FC4553165199D /* FlexibleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlexibleView.swift; sourceTree = ""; }; 1C736D9BB5498E7E8F11C754 /* HeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderCell.swift; sourceTree = ""; }; 1C736DBB6986A8B62963FBB3 /* HtmlParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HtmlParser.swift; sourceTree = ""; }; 1C736DCCE3AA9993E15F7652 /* UIImageExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = ""; }; @@ -214,6 +218,7 @@ 1C7364F10BED5DA0F1C0423C /* NetworkDelegate.swift */, 1C736A6E8396EE306B1AD3A8 /* KSettingsView.swift */, 1C736595533B56039C417E0D /* ServerDownloadDelegate.swift */, + 1C73654AB95A2D629833BEC5 /* SearchItemView.swift */, ); path = master; sourceTree = ""; @@ -239,6 +244,7 @@ 1C7361F01841F546FA7AFD58 /* nspersistentcontainer-defaults-swift.swift */, 1C7362DE1D6BE634D7C2ACBF /* KPersistentContainer.swift */, 1C7360295486647982CFEACF /* UIViewController+Alert.swift */, + 1C736D50A22FC4553165199D /* FlexibleView.swift */, ); path = util; sourceTree = ""; @@ -582,6 +588,8 @@ 1C736DFA8544C773E3C22F10 /* VideoPlayerView.swift in Sources */, 1C736D5A7C7CB9B14AF0ECA6 /* DetailViewController+Show.swift in Sources */, 1C7367084839D2E8B180DB74 /* UIViewController+Alert.swift in Sources */, + 1C7363C2E744C2318879127D /* FlexibleView.swift in Sources */, + 1C736C8DAD6C2FBB9A2EA625 /* SearchItemView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/kplayer/core/DatabaseManager.swift b/kplayer/core/DatabaseManager.swift index 6c3d214..c6e80aa 100644 --- a/kplayer/core/DatabaseManager.swift +++ b/kplayer/core/DatabaseManager.swift @@ -13,9 +13,13 @@ class DatabaseManager { let managedObjectContext : NSManagedObjectContext let persistentContainer : NSPersistentContainer + var allTags = [String]() + init() { self.persistentContainer = KPersistentContainer(defaultContainerWithName: "kplayer") self.managedObjectContext = self.persistentContainer.viewContext + + loadAllTags() } func enrichItem(_ item: MediaItem) { @@ -42,6 +46,10 @@ class DatabaseManager { c.offset.y = CGFloat(s.offy) c.scale = s.scale c.rating = Int(s.rating) + + for t in s.tags as! Set { + c.tags.append(t.name!) + } } } print(s) @@ -133,6 +141,23 @@ class DatabaseManager { snap.rating = Int16(c.rating) snap.thumb = c.thumbUrl + var ct = [String](c.tags) + + for t in snap.tags as! Set { + if !c.tags.contains(t.name!) { + snap.removeFromTags(t) + print("remove \(t.name!)") + } + else { + ct.removeAll(where: { e in e == t.name!}) + } + } + + for a in ct { + addTag(a, snapshot: snap) + print("add \(a)") + } + print("DB -- Update snapshot at \(c.indexId)") } } @@ -148,6 +173,58 @@ class DatabaseManager { tag.name = answer save() + + loadAllTags() + } + + func loadAllTags() { + let fetchRequest = KTag.fetchRequest() + + let results = try! managedObjectContext.fetch(fetchRequest) + + allTags.removeAll() + + for t in results { + allTags.append(t.name!) + } + } + + func searchSnapshots(item: MediaItem) -> Void { + let fetch = KSnapshot.fetchRequest() + + if item.tags.isEmpty { + return + } + + let firstTag = item.tags[0] + item.children.removeAll() + + fetch.predicate = NSPredicate(format: "ANY tags.name == %@", firstTag) + + let results = try! managedObjectContext.fetch(fetch) + + for s in results { + var accept = true + + for t in item.tags { + var hasTag = false + for has in s.tags as! Set { + if has.name == t { + hasTag = true + break + } + } + if !hasTag { + accept = false + } + } + + if accept { + let n = loadSnapshot(s: s) + n.loaded = true + item.children.append(n) + } + } } func loadTags(completionHandler: @escaping Weiter) -> Void { @@ -163,26 +240,8 @@ class DatabaseManager { let snapshots = t.tagged as! Set for s in snapshots { - let i = s.item! - let sitem = MediaItem(name: i.name!, path: i.path!, root: i.root!, type: ItemType.VIDEO) - sitem.loaded = true + let sitem = loadSnapshot(s: s) sitem.parent = tag - - let c = MediaItem(name: i.name!, path: i.path!, root: i.root!, type: ItemType.SNAPSHOT) - c.time = s.time - c.length = s.length - c.loop = s.loop - c.size.width = CGFloat(s.sizex) - c.size.height = CGFloat(s.sizey) - c.offset.x = CGFloat(s.offx) - c.offset.y = CGFloat(s.offy) - c.scale = s.scale - c.rating = Int(s.rating) - c.thumbUrl = s.thumb - c.indexId = Int(s.index) - c.parent = sitem - - sitem.children.append(c) tag.children.append(sitem) } @@ -191,12 +250,57 @@ class DatabaseManager { let m = MediaItem(name: "new", path: "new", root: "tags", type: ItemType.TAG) m.local = true - res.append(m) + let c = MediaItem(name: "combine", path: "combine", root: "tags", type: ItemType.TAG) + c.local = true + res.append(c) + completionHandler(res) } + private func loadSnapshot(s: KSnapshot) -> MediaItem { + let i = s.item! + let sitem = MediaItem(name: i.name!, path: i.path!, root: i.root!, type: ItemType.VIDEO) + sitem.loaded = true + + let c = MediaItem(name: i.name!, path: i.path!, root: i.root!, type: ItemType.SNAPSHOT) + c.time = s.time + c.length = s.length + c.loop = s.loop + c.size.width = CGFloat(s.sizex) + c.size.height = CGFloat(s.sizey) + c.offset.x = CGFloat(s.offx) + c.offset.y = CGFloat(s.offy) + c.scale = s.scale + c.rating = Int(s.rating) + c.thumbUrl = s.thumb + c.indexId = Int(s.index) + c.parent = sitem + + for t in s.tags as! Set { + c.tags.append(t.name!) + } + + sitem.children.append(c) + + return sitem + } + + func addTag(_ name: String, snapshot: KSnapshot) { + let kFetch = KTag.fetchRequest() + kFetch.predicate = NSPredicate(format: "name == %@", name) + let tags = try! managedObjectContext.fetch(kFetch) + + if tags.isEmpty { + return + } + + let tag = tags[0] + + snapshot.addToTags(tag) + } + func addTag(_ name: String, _ item: MediaItem) { let kFetch = KTag.fetchRequest() kFetch.predicate = NSPredicate(format: "name == %@", name) diff --git a/kplayer/core/MediaItem.swift b/kplayer/core/MediaItem.swift index 4d42fd0..81f2c0e 100644 --- a/kplayer/core/MediaItem.swift +++ b/kplayer/core/MediaItem.swift @@ -84,6 +84,8 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable { var size = CGSize() + var tags = [String]() + convenience init(model: MediaModel) { self.init(name: model.name, path: model.path, root: model.root, type: model.type) diff --git a/kplayer/detail/EditItemView.swift b/kplayer/detail/EditItemView.swift index c7cf262..fe21f44 100644 --- a/kplayer/detail/EditItemView.swift +++ b/kplayer/detail/EditItemView.swift @@ -89,6 +89,32 @@ struct EditItemView: View { Text("cancel") }).padding(10).buttonStyle(BorderlessButtonStyle()); } + FlexibleView( + data: DatabaseManager.sharedInstance.allTags, + spacing: 15, + alignment: .leading + ) { tag in + Button(action: { + if item.tags.contains(tag) { + item.tags.removeAll(where: { toRemove in toRemove == tag }) + } + else { + item.tags.append(tag) + } + print(tag) + }, label: { + Text(verbatim: tag) + .padding(8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill( item.tags.contains(tag) ? + Color.yellow.opacity(0.4) : + Color.gray.opacity(0.2) + ) + ) + }).buttonStyle(BorderlessButtonStyle()) + } + }.background(Color.clear) }.background(Color.clear) } diff --git a/kplayer/master/MasterViewController.swift b/kplayer/master/MasterViewController.swift index 6befdc6..99621d5 100644 --- a/kplayer/master/MasterViewController.swift +++ b/kplayer/master/MasterViewController.swift @@ -9,6 +9,7 @@ import UIKit import CoreData import LocalAuthentication +import SwiftUI typealias Weiter = ([MediaItem]) -> Void @@ -80,6 +81,11 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa let selectedItem = model.items[indexPath.row] currentSelection = selectedItem + if (selectedItem.local && selectedItem.name == "combine") { + searchView(item: selectedItem) + // gotoDetails(selectedItem) + } + if (selectedItem.local && selectedItem.name == "new") { createFolder(selectedItem) return @@ -118,6 +124,19 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa } } + func searchView(item: MediaItem) { + let kv = SearchItemView(item: item, completionHandler: { + self.dismiss(animated: true, completion: nil); + DatabaseManager.sharedInstance.searchSnapshots(item: item) + self.gotoDetails(item) + } ) + let pc = UIHostingController(rootView: kv) + let navController = UINavigationController(rootViewController: pc) // Creating a navigation controller with pc at the root of the navigation stack. + navController.modalPresentationStyle = .automatic + + present(navController, animated: false, completion: nil) + } + func gotoNextFolder(_ selectedItem: MediaItem) { let mainStoryboard = UIStoryboard(name: "Main", bundle: nil) let vc = mainStoryboard.instantiateViewController(withIdentifier: "mastertable") as! MasterViewController diff --git a/kplayer/master/SearchItemView.swift b/kplayer/master/SearchItemView.swift new file mode 100644 index 0000000..debbff8 --- /dev/null +++ b/kplayer/master/SearchItemView.swift @@ -0,0 +1,50 @@ +// +// Created by Marco Schmickler on 26.01.22. +// Copyright (c) 2022 Marco Schmickler. All rights reserved. +// + +import Foundation +import SwiftUI + +struct SearchItemView: View { + @ObservedObject + var item: MediaItem + + var completionHandler: (() -> Void)? + + var body: some View { + VStack { + FlexibleView( + data: DatabaseManager.sharedInstance.allTags, + spacing: 15, + alignment: .leading + ) { tag in + Button(action: { + if item.tags.contains(tag) { + item.tags.removeAll(where: { toRemove in toRemove == tag }) + } else { + item.tags.append(tag) + } + item.objectWillChange.send() + print(tag) + }, label: { + Text(verbatim: tag) + .padding(8) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(item.tags.contains(tag) ? + Color.yellow.opacity(0.4) : + Color.gray.opacity(0.2) + ) + ) + }) + .buttonStyle(BorderlessButtonStyle()) + } + Button(action: { + self.completionHandler?() + }, label: { + Text("ok") + }) + } + } +} \ No newline at end of file diff --git a/kplayer/util/FlexibleView.swift b/kplayer/util/FlexibleView.swift new file mode 100644 index 0000000..3b1d2bf --- /dev/null +++ b/kplayer/util/FlexibleView.swift @@ -0,0 +1,98 @@ +// +// Created by Marco Schmickler on 26.01.22. +// Copyright (c) 2022 Marco Schmickler. All rights reserved. +// + +import Foundation +import SwiftUI + +struct _FlexibleView: View where Data.Element: Hashable { + let availableWidth: CGFloat + let data: Data + let spacing: CGFloat + let alignment: HorizontalAlignment + let content: (Data.Element) -> Content + @State var elementsSize: [Data.Element: CGSize] = [:] + + var body : some View { + VStack(alignment: alignment, spacing: spacing) { + ForEach(computeRows(), id: \.self) { rowElements in + HStack(spacing: spacing) { + ForEach(rowElements, id: \.self) { element in + content(element) + .fixedSize() + .readSize { size in + elementsSize[element] = size + } + } + } + } + } + } + + func computeRows() -> [[Data.Element]] { + var rows: [[Data.Element]] = [[]] + var currentRow = 0 + var remainingWidth = availableWidth + + for element in data { + let elementSize = elementsSize[element, default: CGSize(width: availableWidth, height: 1)] + + if remainingWidth - (elementSize.width + spacing) >= 0 { + rows[currentRow].append(element) + } else { + currentRow = currentRow + 1 + rows.append([element]) + remainingWidth = availableWidth + } + + remainingWidth = remainingWidth - (elementSize.width + spacing) + } + + return rows + } +} + +struct FlexibleView: View where Data.Element: Hashable { + let data: Data + let spacing: CGFloat + let alignment: HorizontalAlignment + let content: (Data.Element) -> Content + @State private var availableWidth: CGFloat = 0 + + var body: some View { + ZStack(alignment: Alignment(horizontal: alignment, vertical: .center)) { + Color.clear + .frame(height: 1) + .readSize { size in + availableWidth = size.width + } + + _FlexibleView( + availableWidth: availableWidth, + data: data, + spacing: spacing, + alignment: alignment, + content: content + ) + } + } +} + + +extension View { + func readSize(onChange: @escaping (CGSize) -> Void) -> some View { + background( + GeometryReader { geometryProxy in + Color.clear + .preference(key: SizePreferenceKey.self, value: geometryProxy.size) + } + ) + .onPreferenceChange(SizePreferenceKey.self, perform: onChange) + } +} + +private struct SizePreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} +}