From e22df2436c29eea9520ae0132dd7ae1d2d5a3698 Mon Sep 17 00:00:00 2001 From: marcoschmickler Date: Thu, 10 Nov 2022 22:46:18 +0100 Subject: [PATCH] Embedded --- kplayer.xcodeproj/project.pbxproj | 8 ++ kplayer/core/DatabaseManager.swift | 41 +++++- kplayer/core/KSettings.swift | 4 +- kplayer/core/MediaItem.swift | 9 ++ kplayer/core/NetworkManager.swift | 72 ++++++---- .../detail/DetailViewController+Show.swift | 9 +- kplayer/detail/DetailViewController.swift | 25 ++++ kplayer/detail/EditItemView.swift | 7 +- kplayer/photo/SPhotoAlbumView.swift | 64 +++++---- kplayer/photo/SPhotoModel.swift | 124 +++++++++++++++++- kplayer/photo/SPhotoScrubber.swift | 20 ++- kplayer/photo/SPhotoView.swift | 80 ++++++++--- kplayer/server/kplayer.js | 104 +++++++++++++++ kplayer/util/AsyncImage.swift | 11 +- kplayer/util/ImageCache.swift | 113 ++++++++++++++++ kplayer/util/ImageLoader.swift | 99 ++++++++++++++ kplayer/util/UIImageExtension.swift | 18 ++- 17 files changed, 718 insertions(+), 90 deletions(-) create mode 100644 kplayer/util/ImageCache.swift create mode 100644 kplayer/util/ImageLoader.swift diff --git a/kplayer.xcodeproj/project.pbxproj b/kplayer.xcodeproj/project.pbxproj index c759868..73a5068 100644 --- a/kplayer.xcodeproj/project.pbxproj +++ b/kplayer.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 1C73666A07CF2416B1B8D3F0 /* KSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736C94157754DE1C808173 /* KSettingsModel.swift */; }; 1C736690D123BD4B24874394 /* pathfinder.scpt in Sources */ = {isa = PBXBuildFile; fileRef = 1C7369BED02028D8564E82D5 /* pathfinder.scpt */; }; 1C7366A0CFD2B55BF8C3BAF0 /* NetworkDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7364F10BED5DA0F1C0423C /* NetworkDelegate.swift */; }; + 1C7366BEA68D6E4CEE43417E /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736CC8D90375E86CD01964 /* ImageLoader.swift */; }; 1C7366FE5C760C8D5117207F /* SVideoLoopPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360F9835128FC0A198ED0 /* SVideoLoopPlayer.swift */; }; 1C7366FF0651A802F09936D6 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736C17C40DAE162AF8DDE3 /* WebViewModel.swift */; }; 1C7367084839D2E8B180DB74 /* UIViewController+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360295486647982CFEACF /* UIViewController+Alert.swift */; }; @@ -69,6 +70,7 @@ 1C736E21B246C0BE7E123FD3 /* MediaModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736B41C6AC33F3FA592C63 /* MediaModel.swift */; }; 1C736EB38B780CA47B50772F /* SEmbeddedVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7367B39F09CCD1760A345A /* SEmbeddedVideo.swift */; }; 1C736EC45EE7DA5F7FCE63DA /* LocalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73659CC9B523B957E58DC6 /* LocalManager.swift */; }; + 1C736F2334FE0F946FD7CABE /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736E32C8574BFE3536F1C2 /* ImageCache.swift */; }; 1C736F6A223D4ADB2E1BA733 /* ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736069C214E9522BB1BD97 /* ItemCell.swift */; }; 1C736F7D29B76C7037CEF778 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73647019E6C2E822127BA3 /* DatabaseManager.swift */; }; 1C736FAE5D3E5D3BA3C1FAE5 /* links.html in Resources */ = {isa = PBXBuildFile; fileRef = 1C73645DBD6499A726D34973 /* links.html */; }; @@ -154,12 +156,14 @@ 1C736C17C40DAE162AF8DDE3 /* WebViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = ""; }; 1C736C72CDF8902484856B3B /* SelfSizingHostingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelfSizingHostingController.swift; sourceTree = ""; }; 1C736C94157754DE1C808173 /* KSettingsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KSettingsModel.swift; sourceTree = ""; }; + 1C736CC8D90375E86CD01964 /* ImageLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; }; 1C736D27EC608FAAFDB4A68C /* WebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebView.swift; 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 = ""; }; 1C736DCD945ABAE984FF43EF /* KNetworkProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KNetworkProtocol.swift; sourceTree = ""; }; + 1C736E32C8574BFE3536F1C2 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; 1C736EA15A11AF7D57F85824 /* ThumbnailCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailCache.swift; sourceTree = ""; }; 1C736EEC570C71AAC50F2E95 /* SVideoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVideoModel.swift; sourceTree = ""; }; 1C736EF64DE56AD058A4F612 /* KBrowserView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KBrowserView.swift; sourceTree = ""; }; @@ -278,6 +282,8 @@ 1C7360295486647982CFEACF /* UIViewController+Alert.swift */, 1C736D50A22FC4553165199D /* FlexibleView.swift */, 1C736C72CDF8902484856B3B /* SelfSizingHostingController.swift */, + 1C736E32C8574BFE3536F1C2 /* ImageCache.swift */, + 1C736CC8D90375E86CD01964 /* ImageLoader.swift */, ); path = util; sourceTree = ""; @@ -651,6 +657,8 @@ 1C736D0A14C365F3E874420C /* SelfSizingHostingController.swift in Sources */, 1C736DAE7F2A530AACDA0D18 /* KFrame.swift in Sources */, 1C736EB38B780CA47B50772F /* SEmbeddedVideo.swift in Sources */, + 1C736F2334FE0F946FD7CABE /* ImageCache.swift in Sources */, + 1C7366BEA68D6E4CEE43417E /* ImageLoader.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/kplayer/core/DatabaseManager.swift b/kplayer/core/DatabaseManager.swift index 0dcd0c3..7d63d81 100644 --- a/kplayer/core/DatabaseManager.swift +++ b/kplayer/core/DatabaseManager.swift @@ -61,13 +61,40 @@ class DatabaseManager { } } + func migrate() { + return; + + let path = "2017" + let fetchRequest = KItem.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "root contains[cd] '2017'") + + let results = try! managedObjectContext.fetch(fetchRequest) + + for r in results { + print("\(r.root) \(r.name)") + for s in r.snapshots as! Set { + print("\(s.thumb)") + let t = s.thumb?.replacingOccurrences(of: "/srv/samba/ren/heg", with: "http://linkstation:8089/ren/thumbs/heg") + print(t) + s.thumb = t + } + let n = r.root!.replacingOccurrences(of: "6000/6000", with: "6000") + print(n) + // r.root = n + } +rollback() +// save() + } + func getKItem(_ item: MediaItem) -> KItem { if let oid = item.objectID { do { let i = try managedObjectContext.existingObject(with: oid) - if i != nil { - return i as! KItem + if let kitem = i as? KItem { + return kitem + } else if let ksnapshot = i as? KSnapshot { + return ksnapshot.item! } } catch { } @@ -99,7 +126,7 @@ class DatabaseManager { return new; } - private func save() { + func save() { do { try managedObjectContext.save() } catch { @@ -107,6 +134,14 @@ class DatabaseManager { } } + func rollback() { + do { + try managedObjectContext.rollback() + } catch { + print("Error") + } + } + func saveItemMetaData(_ item: MediaItem) { if (item.type == ItemType.PICS) { if let oid = item.objectID { diff --git a/kplayer/core/KSettings.swift b/kplayer/core/KSettings.swift index fb87854..f2c4bff 100644 --- a/kplayer/core/KSettings.swift +++ b/kplayer/core/KSettings.swift @@ -19,7 +19,7 @@ class KSettings: ObservableObject { var zoomed = false @Published - var jump = false + var jump = true @Published var edit = false @@ -40,7 +40,7 @@ class KSettings: ObservableObject { } func toModel() -> KSettingsModel { - KSettingsModel(scale: scale, autoloop: autoloop, zoomed: zoomed, slow: slow) + KSettingsModel(scale: scale, autoloop: autoloop, zoomed: zoomed, jump: jump, slow: slow ) } func toJSON() -> String { diff --git a/kplayer/core/MediaItem.swift b/kplayer/core/MediaItem.swift index 8266836..12c9753 100644 --- a/kplayer/core/MediaItem.swift +++ b/kplayer/core/MediaItem.swift @@ -215,6 +215,9 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable { if thumbUrl!.starts(with: "file:") { return thumbUrl! } + if thumbUrl!.starts(with: "http") { + return thumbUrl! + } let enc = thumbUrl!.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)! return NetworkManager.sharedInstance.baseurl + "/service/download/srv/samba" + enc.substringStartingFrom(10) @@ -233,6 +236,12 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable { } var imageUrl = thumbUrl!.replacingOccurrences(of: "_thumb.", with: ".") + imageUrl = thumbUrl!.replacingOccurrences(of: "/ren/thumbs/", with: "/ren/") + + if imageUrl.starts(with: "http") { + return imageUrl + } + imageUrl = imageUrl.replacingOccurrences(of: "?preview=true", with: "") imageUrl = imageUrl.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)! diff --git a/kplayer/core/NetworkManager.swift b/kplayer/core/NetworkManager.swift index 966d643..bf0aad0 100644 --- a/kplayer/core/NetworkManager.swift +++ b/kplayer/core/NetworkManager.swift @@ -124,7 +124,7 @@ class NetworkManager { let fpath = (path as NSString).substring(with: NSMakeRange(0, pfl)) let i = MediaItem(name: folderName, path: fpath, root: root, type: ItemType.PICS) - i.thumbUrl = "\(s)?preview=true" + i.thumbUrl = "\(s)" i.loaded = true if !name.hasPrefix(".") { if let picfolder = items[path] { @@ -138,6 +138,7 @@ class NetworkManager { iff.children.append(i) } } + DatabaseManager.sharedInstance.enrichItem(i) } } } @@ -438,48 +439,71 @@ class NetworkManager { func loadPicDetails(items: MediaItem, result: @escaping ([MediaItem]) -> () ) { let len = items.root.count let url = nodeurl + "listfiles" + items.fullPath + let url_thumbs = nodeurl + "listfiles" + items.fullPath.replacingOccurrences(of: "/srv/samba/ren/", with: "/srv/samba/ren/thumbs/") // print(items) print(url) + print(url_thumbs) - AF.request(url).responseJSON { + var thumbs : [String:String] = [:] + + AF.request(url_thumbs).responseJSON { (response) in if let json = response.value { - var im = [MediaItem]() - for s in json as! [String] { - if s.lowercased().hasSuffix(".jpg") { - let l = s.count let name = NSURL(fileURLWithPath: s).lastPathComponent! - var pathlen = l - len - name.count + thumbs[name] = s + } + } + } - print(pathlen) - print(name) - print(s) + AF.request(url).responseJSON { + (response) in - if (pathlen < 2) { - pathlen = 2 - } + if let json = response.value { + var im = [MediaItem]() - let path = (s as NSString).substring(with: NSMakeRange(len + 1, pathlen - 2)) + for s in json as! [String] { - let folderName = NSURL(fileURLWithPath: path).lastPathComponent! - let fl = path.count - let pfl = fl - folderName.count + if s.lowercased().hasSuffix(".jpg") { + let l = s.count + let name = NSURL(fileURLWithPath: s).lastPathComponent! + var pathlen = l - len - name.count - print("\(folderName) \(pfl)") - let fpath = s.substringLeft(pfl) + print(thumbs[name]) + print(s) - let i = MediaItem(name: folderName, path: fpath, root: items.root, type: ItemType.PICS) - i.thumbUrl = "\(s)?preview=true" - if !name.hasPrefix(".") { - im.append(i) + if (pathlen < 2) { + pathlen = 2 + } + + let path = (s as NSString).substring(with: NSMakeRange(len + 1, pathlen - 2)) + + let folderName = NSURL(fileURLWithPath: path).lastPathComponent! + let fl = path.count + let pfl = fl - folderName.count + + print("\(folderName) \(pfl)") + let fpath = s.substringLeft(pfl) + + let i = MediaItem(name: folderName, path: fpath, root: items.root, type: ItemType.PICS) + + if let t = thumbs[name] { + i.thumbUrl = self.vidurl + t.substringStartingFrom(10) + } + else { + i.thumbUrl = self.vidurl + s.substringStartingFrom(10) + } + //i.thumbUrl = "\(s)?preview=true" + if !name.hasPrefix(".") { + im.append(i) + } } } + result(im) } - result(im) } } } diff --git a/kplayer/detail/DetailViewController+Show.swift b/kplayer/detail/DetailViewController+Show.swift index 1bfc3ac..74561c2 100644 --- a/kplayer/detail/DetailViewController+Show.swift +++ b/kplayer/detail/DetailViewController+Show.swift @@ -21,7 +21,7 @@ extension DetailViewController { } } - func showPhotos(_ im: [MediaItem], selectedItem: MediaItem, section: Int) { + func showPhotos(_ im: [MediaItem], selectedItem: MediaItem, section: Int, compilation: Bool = false) { let base = MediaItem(name: "", path: "", root: "", type: ItemType.PICFOLDER) base.children = im if im.count < 1 { @@ -31,8 +31,15 @@ extension DetailViewController { model.selectItem(selectedItem) model.folderItems = detailItem!.children model.folderIndex = section + model.favorite = selectedItem.favorite + model.compilation = compilation let view = SPhotoAlbumView(completionHandler: { saved in +// if (saved) { +// selectedItem.favorite = model.favorite +// self.delegate!.saveItem(selectedItem: selectedItem) +// } + self.collectionView.reloadData() self.collectionView.collectionViewLayout.invalidateLayout() diff --git a/kplayer/detail/DetailViewController.swift b/kplayer/detail/DetailViewController.swift index 0f67fdc..3d310d4 100644 --- a/kplayer/detail/DetailViewController.swift +++ b/kplayer/detail/DetailViewController.swift @@ -6,6 +6,12 @@ // Copyright (c) 2015 Marco Schmickler. All rights reserved. // +import UIKit +import Alamofire +import FileBrowser +import AVFoundation +import ZIPFoundation +import SwiftUI import UIKit import Alamofire import FileBrowser @@ -114,6 +120,7 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout // https://github.com/marmelroy/FileBrowser @objc func fileBrowser() { + DatabaseManager.sharedInstance.migrate() let d = FileHelper.getDocumentsDirectory() LocalManager.sharedInstance.prepareLocalFolder() @@ -147,6 +154,24 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout var i = [MediaItem]() if let d = detailItem { + if d.children.isEmpty { + return + } + if d.children[0].type == .PICS { + for c in d.children { + if c.thumbUrl != nil { + i.append(c) + } + for c1 in c.children { + if c1.thumbUrl != nil { + i.append(c1) + } + } + } + showPhotos(i, selectedItem: i[0], section: 0, compilation: true) + return + } + showAllVideos(d) return diff --git a/kplayer/detail/EditItemView.swift b/kplayer/detail/EditItemView.swift index ab6134c..f8526bd 100644 --- a/kplayer/detail/EditItemView.swift +++ b/kplayer/detail/EditItemView.swift @@ -18,9 +18,11 @@ protocol EditItemDelegate { struct TagEditor: View { @ObservedObject var item: MediaItem + var completionHandler: ((MediaItem) -> Void)? - init(item: MediaItem) { + init(item: MediaItem, completionHandler: ((MediaItem) -> Void)? = nil) { self.item = item + self.completionHandler = completionHandler } var body: some View { @@ -36,6 +38,9 @@ struct TagEditor: View { else { item.tags.append(tag) } + if let c = self.completionHandler { + c(item) + } print(tag) }, label: { Text(verbatim: tag) diff --git a/kplayer/photo/SPhotoAlbumView.swift b/kplayer/photo/SPhotoAlbumView.swift index 015f414..e0cfc70 100644 --- a/kplayer/photo/SPhotoAlbumView.swift +++ b/kplayer/photo/SPhotoAlbumView.swift @@ -25,43 +25,61 @@ struct SPhotoAlbumView: View { let v = VStack { HStack { Group { - Button(action: { - cleanup() - completionHandler!(true) - }, label: { - //Text("cancel") - Text("X").frame(width: 30) - })//.foregroundColor(update ? Color.yellow : Color.blue) - .buttonStyle(BorderlessButtonStyle()) - Button(action: { - more.toggle() - }, label: { - //Text("cancel") - Text("\(model.index)").frame(width: 70) - })//.foregroundColor(update ? Color.yellow : Color.blue) - .buttonStyle(BorderlessButtonStyle()) - KToggleButton(text: "embd", binding: $embedded) + VStack { + HStack { + Button(action: { + cleanup() + completionHandler!(true) + }, label: { + //Text("cancel") + Text("X").frame(width: 30) + })//.foregroundColor(update ? Color.yellow : Color.blue) + .buttonStyle(BorderlessButtonStyle()) + Button(action: { + more.toggle() + }, label: { + Text("\(model.index)(\(model.folderIndex))").frame(width: 70) + }) + .foregroundColor(model.scale > model.maxScale ? Color.yellow : Color.blue) + .buttonStyle(BorderlessButtonStyle()) + Button(action: { + model.favorite.toggle() + model.dirty = true + let kitem = DatabaseManager.sharedInstance.getKItem(model.selectedItem) + kitem.favorite = model.favorite + DatabaseManager.sharedInstance.save() + }, label: { + Image(systemName: "heart.fill") + }) + .foregroundColor(model.favorite ? Color.yellow : Color.blue).buttonStyle(BorderlessButtonStyle()) + KToggleButton(text: "embd", binding: $embedded) + KToggleButton(text: "go", binding: $model.go) + } + Text("\(model.selectedItem.name)").foregroundColor(.blue) + } Spacer() SPhotoScrubber(model: model) } } .frame(height: 50) SPhotoView(model: model) - } + }.task() { + model.preload() + } if embedded && !more { v.overlay(SEmbeddedVideo(embedded: $embedded, down: $embDown).offset(y: embDown ? 0: 70), alignment: embDown ? .bottomLeading : .topLeading) } else if more { v.overlay(VStack { KToggleButton(text: "spring", binding: $model.spring).frame(height: 30) - Button(action: { - saveSelectedItem() - }, label: { - Text("save") - }) +// Button(action: { +// saveSelectedItem() +// }, label: { +// Text("save") +// }) .buttonStyle(BorderlessButtonStyle()) } .frame(width: 60, alignment: .top).offset(x: 0, y: 70), alignment: .topLeading) - .overlay(TagEditor(item: model.allItems[model.index]) + .overlay(TagEditor(item: model.allItems[model.index], completionHandler: DatabaseManager.sharedInstance.saveItemMetaData) .frame(width: 60, alignment: .top).offset(x: 0, y: 70), alignment: .topTrailing) } else { diff --git a/kplayer/photo/SPhotoModel.swift b/kplayer/photo/SPhotoModel.swift index 86905b8..04404d7 100644 --- a/kplayer/photo/SPhotoModel.swift +++ b/kplayer/photo/SPhotoModel.swift @@ -10,25 +10,87 @@ class SPhotoModel : ObservableObject { @Published var folderItems = [MediaItem]() @Published var folderIndex = 0 + @Published var compilationItems : [MediaItem] @Published var allItems : [MediaItem] @Published var indexItems : [MediaItem] @Published var selectedItem : MediaItem @Published var index = 0 @Published var thumbIndex = 0 @Published var scrub = false + @Published var favorite = false + @Published var dirty = false + @Published var compilation = false + @Published var go = false + @Published var locked = false + @Published var deadline = Date() + @Published var image = UIImage() @Published var scale: CGFloat = 1.0 + @Published var maxScale: CGFloat = 1.0 @Published var dragOffset: CGSize = CGSize.zero @Published var spring = false + @Published var timer : Timer? init(allItems: [MediaItem]) { self.allItems = allItems + self.compilationItems = allItems selectedItem = allItems[0] indexItems = allItems update(allItems: allItems) } + func loadImage() { + if selectedItem.thumbUrl != nil { + loadImage(for: selectedItem.imageUrlAbsolute) + } + } + + func loadImage(for urlString: String) { + guard let url = URL(string: urlString) else { return } + + if (!go) { + if let thumb = ImageLoader.shared.cache[url] { + image = thumb + + print("cache \(url)") + } + } + if let t = timer { + t.invalidate() + } + timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { timer in + + print("load \(urlString)") + + ImageLoader.shared.loadFullImage(from: url).sink { img in + if let i = img { + if self.go || self.selectedItem.imageUrlAbsolute == url.absoluteString { + Thread.sleep(until: self.deadline) + self.image = i + } + + if (self.go) { + self.nextItem() + } + } + } + } + +// let task = URLSession.shared.dataTask(with: url) { data, response, error in +// guard let data = data else { return } +// DispatchQueue.main.async { +// self.image = UIImage(data: data) ?? UIImage(systemName: "repeat")! +// print("loaded \(urlString)\(self.image)") +// } +// } +// task.resume() + } + private func update(allItems: [MediaItem]) { + if allItems.isEmpty { + return + } + self.allItems = allItems selectedItem = allItems[0] @@ -56,6 +118,7 @@ class SPhotoModel : ObservableObject { DatabaseManager.sharedInstance.enrichItem(m) selectedItem.indexId = 0 + loadImage() } func selectItem(_ item: MediaItem) { @@ -65,6 +128,7 @@ class SPhotoModel : ObservableObject { selectedItem = item selectedItem.indexId = i index = i + loadImage() break; } @@ -73,6 +137,11 @@ class SPhotoModel : ObservableObject { } func back() { + if locked { + return + } + locked = true + if (folderIndex > 0) { folderIndex -= 1 } else { @@ -82,6 +151,11 @@ class SPhotoModel : ObservableObject { } func next() { + if locked { + return + } + locked = true + if (folderIndex < folderItems.count - 1) { folderIndex += 1 } else { @@ -90,12 +164,60 @@ class SPhotoModel : ObservableObject { show() } + func nextItem() { + if (index < allItems.count - 1) { + index += 1 + } + else { + index = 0 + } + + selectedItem = allItems[index] + deadline = Date().advanced(by: 0.4) + loadImage() + } + func show() { - var selectedItem = folderItems[folderIndex] + if (compilation) { + if folderIndex == 0 { + update(allItems: compilationItems) + locked = false + return + } + else { + folderIndex = 1 + } + } + + var selectedItem = compilation ? selectedItem : folderItems[folderIndex] NetworkManager.sharedInstance.loadPicDetails(items: selectedItem, result: { (im: [MediaItem]) in self.index = 0 self.update(allItems: im) + + let kitem = DatabaseManager.sharedInstance.getKItem(selectedItem) + self.favorite = kitem.favorite + + Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in + self.locked = false + } }) } + + func preload() { + for item in allItems { + if item.thumbImage == nil && item.thumbUrlAbsolute.contains("/ren/thumbs/"){ + if let URL = Foundation.URL(string: item.thumbUrlAbsolute) { + //try! await Task.sleep(nanoseconds: UInt64(1 * Double(NSEC_PER_SEC))) + print("preload \(item.thumbUrlAbsolute)") + ImageLoader.shared.loadImageBackground(from: URL).sink { image in + if let i = image { + item.thumbImage = i + } + } + // break + } + } + } + } } diff --git a/kplayer/photo/SPhotoScrubber.swift b/kplayer/photo/SPhotoScrubber.swift index 652b0f3..f5166ae 100644 --- a/kplayer/photo/SPhotoScrubber.swift +++ b/kplayer/photo/SPhotoScrubber.swift @@ -40,7 +40,7 @@ struct SPhotoScrubber: View { setIndex(i: i) - print("size \(width) xpos \(xpos) i \(i)") + // print("size \(width) xpos \(xpos) i \(i)") model.scrub = true } @@ -54,7 +54,7 @@ struct SPhotoScrubber: View { model.scrub = false - print("size \(width) xpos \(xpos) i \(i)") + // print("size \(width) xpos \(xpos) i \(i)") } ) } @@ -72,18 +72,24 @@ struct SPhotoScrubber: View { model.selectedItem = item item.indexId = i + model.loadImage() if item.thumbUrl != nil && item.thumbImage == nil { item.thumbImage = UIImage(systemName: "repeat") let URL = Foundation.URL(string: item.thumbUrlAbsolute)! + ImageLoader.shared.loadImage(from: URL).sink { image in + if let i = image { + item.thumbImage = i + } + } // print("fetch \(item.thumbUrlAbsolute)") - Shared.imageCache.fetch(URL: URL).onSuccess { - i in - //newItem.image = i - item.thumbImage = i - } +// Shared.imageCache.fetch(URL: URL).onSuccess { +// i in +// //newItem.image = i +// item.thumbImage = i.scaleToSize(800, height: 600) +// } } } } diff --git a/kplayer/photo/SPhotoView.swift b/kplayer/photo/SPhotoView.swift index 018917a..04eda4b 100644 --- a/kplayer/photo/SPhotoView.swift +++ b/kplayer/photo/SPhotoView.swift @@ -9,14 +9,18 @@ import SwiftUI struct KAnimate: ViewModifier { var dragOffset: CGSize var spring: Bool + var off: Bool - init(dragOffset: CGSize, spring: Bool) { + init(dragOffset: CGSize, spring: Bool, off: Bool = false) { self.dragOffset = dragOffset self.spring = spring + self.off = off } func body(content: Content) -> some View { - if (spring) { + if (off) { + content + } else if (spring) { content.animation(.spring(response: 10, dampingFraction: 0.05), value: dragOffset) } else { content.animation(.easeOut(duration: 3), value: dragOffset) @@ -28,11 +32,12 @@ struct SPhotoView: View { @ObservedObject var model: SPhotoModel - @State var lastScale: CGFloat = 1.75 + @State var lastScale: CGFloat = 2 @State var lastScaleValue: CGFloat = 1.0 @State var lastDragOffset: CGSize = CGSize.zero @State var lastDrag: CGSize = CGSize.zero @State var skip = false + @State var zooming = false @State var startindex = -1 @State var animateX = 0 @State var dampen = 0.05 @@ -47,20 +52,25 @@ struct SPhotoView: View { GeometryReader { geo in // AsyncImage(item: model.selectedItem, thumb: false, placeholder: { Text("Loading ...") }, image: { Image(uiImage: $0).resizable() }) ZStack { - SwiftUI.AsyncImage(url: URL(string: model.allItems[model.index].imageUrlAbsolute)) { image in - image.resizable().scaledToFit() - } - placeholder: { - if let i = model.selectedItem.thumbImage { - Image(uiImage: i).resizable().scaledToFit() - } else { - Text("...") - } - } + Image(uiImage: model.image) + .resizable().scaledToFit() + // SwiftUI.AsyncImage(url: URL(string: model.allItems[model.index].imageUrlAbsolute)) { image in + // image.resizable().scaledToFit() + // } + // placeholder: { + // if let i = model.selectedItem.thumbImage { + // Image(uiImage: i).resizable().scaledToFit() + // } else { + // Text("...") + // } + // } .frame(height: geo.size.height).frame(width: geo.size.width, alignment: .leading) - .scaleEffect(model.scale).offset(model.dragOffset).modifier(KAnimate(dragOffset: model.dragOffset, spring: model.spring)) + .scaleEffect(model.scale).offset(model.dragOffset).modifier(KAnimate(dragOffset: model.dragOffset, spring: model.spring, off: zooming)) } .onTapGesture(count: 2) { + let h1 = geo.size.height + let h2 = model.image.size.height + model.maxScale = h2 / h1 print("3 tapped!") if model.scale == 1 { model.scale = lastScale @@ -79,33 +89,43 @@ struct SPhotoView: View { if startindex < 0 { startindex = model.index } - print("\(startindex) \(dragged.width)") + // print("\(startindex) \(dragged.width)") if model.scale == 1 { //&& gesture.startLocation.x < 400 { if (jump && dragged.height > 120) { + print("next") model.next() return } if (jump && dragged.height < -120) { + print("back") model.back() return } - model.index = startindex + Int(dragged.width / 10.0) + var i = startindex + Int(dragged.width / 15.0) - if model.index >= model.allItems.count { - model.index = model.allItems.count - 1 + if i >= model.allItems.count { + i = model.allItems.count - 1 } - if model.index < 0 { - model.index = 0 + if i < 0 { + i = 0 + } + + if (i != model.index) { + ImageLoader.shared.backgroundQueue.cancelAllOperations() + model.index = i + model.selectedItem = model.allItems[model.index] + model.loadImage() } } else { - if (dragged.height * multi) + lastDragOffset.height > -1000 { + if (dragged.height * multi) + lastDragOffset.height > -500 * model.scale { model.dragOffset = CGSize(width: (dragged.width * multi) + lastDragOffset.width, height: (dragged.height * multi) + lastDragOffset.height) } } dampen = 0.05 lastDrag = dragged + print("scale \(model.scale) h \(model.dragOffset.height) w \(model.dragOffset.width)") } .onEnded { gesture in startindex = -1 @@ -116,11 +136,29 @@ struct SPhotoView: View { ) .gesture(MagnificationGesture() .onChanged { val in + if !zooming { + zooming = true + lastDragOffset = model.dragOffset + } + let delta = val / self.lastScaleValue self.lastScaleValue = val model.scale = model.scale * delta + + let h1 = geo.size.height + let h2 = model.image.size.height + model.maxScale = h2 / h1 + + let width = geo.size.width / 2; + model.dragOffset.width = lastDragOffset.width + (width - (width / model.scale)) + + let height = geo.size.height / 2; + model.dragOffset.height = lastDragOffset.height + (height - (height / model.scale)) + print("scale \(model.scale) h \(model.dragOffset.height) w \(model.dragOffset.width) lh \(lastDragOffset.height) lw \(lastDragOffset.width)") + // print("scale \(model.scale) frame \(h1) image \(h2) \(h2/h1)") } .onEnded { val in + zooming = false // without this the next gesture will be broken self.lastScaleValue = 1.0 }) diff --git a/kplayer/server/kplayer.js b/kplayer/server/kplayer.js index 2ad8c56..cbb39fc 100644 --- a/kplayer/server/kplayer.js +++ b/kplayer/server/kplayer.js @@ -115,6 +115,54 @@ app.get('/listdirs/*', function (req, res) { res.end(JSON.stringify(result)) }) +app.get('/listpicdirs/*', function (req, res) { + var address = req.path.substr(12) + console.log("listdirs " + address); + + if (!req.path.startsWith("/listpicdirs/srv/samba/ren/")) { + res.end("Access denied") + return + } + + var files = getFiles(address) + + var result = []; + + files.forEach(function(file){ + var filename = file.name + var filetype = "folder" + var pics = false + + if ((file.isDirectory() || file.isSymbolicLink()) && !filename.startsWith(".")) { + var folders = getFiles(address + "/" + filename) + + if (folders.filter(dirent => dirent.name.endsWith(".mp4")).length > 0) { + filetype = "video" + } + else if (folders.filter(dirent => dirent.name.endsWith(".html")).length > 0) { + filetype = "web" + } +// else if (folders.filter(dirent => dirent.name.endsWith(".jpg")).length > 0) { + else { + folders.forEach(function (f) { + if (f.isDirectory()) { + + if (hasPics(address + "/" + filename + "/" + f.name)) { + filetype = "pictures" + } + } + }) + } + + console.log(filename) + + result.push([filename, filetype]) + } + }) + + res.end(JSON.stringify(result)) +}) + app.get('/listvideos/*', function (req, res) { var address = decodeURIComponent(req.path.substr(11)) console.log("listdirs " + address); @@ -147,6 +195,62 @@ app.get('/listvideos/*', function (req, res) { }) /* +@RequestMapping (method = RequestMethod.GET, value = "/listpicdirs/**") + @ResponseBody List listpicdirs(HttpServletRequest request, HttpServletResponse response) { + String address = (String) request.getAttribute( + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + + address = address["/service/listpicdirs".length()..-1] + String filter = null + + if (address.indexOf("*") > 0) { + def param = address.split(Pattern.quote("*")) + address = param[0] + filter = param[1].toLowerCase() + + println "filter !" + filter + "!" + } + + println "listpicdirs " + address; + + def b = [] + + new File(address).eachDirRecurse() { file -> + if (filter && !file.name.toLowerCase().contains(filter)) { + // ignore + } + else { + def f = file.list() + + for (String fi : f) { + if (fi.toLowerCase().endsWith(".jpg")) { + boolean hasFav = false + def p = file.absolutePath + if (p.indexOf("/favpic/") < 0) { + p = p.replace("/srv/samba/ren", "/srv/samba/ren/favpic") + + def l = new File(p).list() + + for (i in l) { + b << file.absolutePath + "/" + i + hasFav = true + } + + } + + if (!hasFav) { + b << file.absolutePath + "/" + fi + } + + break; + } + } + } + } + + return b + } + @RequestMapping (method = RequestMethod.GET, value = "/listvideos/**") @ResponseBody List listvideos(HttpServletRequest request, HttpServletResponse response) { String address = (String) request.getAttribute( diff --git a/kplayer/util/AsyncImage.swift b/kplayer/util/AsyncImage.swift index ca304da..5f25570 100644 --- a/kplayer/util/AsyncImage.swift +++ b/kplayer/util/AsyncImage.swift @@ -43,12 +43,11 @@ struct AsyncImage: View { } else { if newItem.thumbUrl != nil { let URL = Foundation.URL(string: newItem.thumbUrlAbsolute)! - - Shared.imageCache.fetch(URL: URL).onSuccess { - i in - - newItem.image = i - newItem.thumbImage = i // newItem.image!.scaleToSize(66.0, height: 44.0) + ImageLoader.shared.loadImage(from: URL).sink { image in + if let i = image { + // newItem.image = i + newItem.thumbImage = i // newItem.image!.scaleToSize(66.0, height: 44.0) + } } } } diff --git a/kplayer/util/ImageCache.swift b/kplayer/util/ImageCache.swift new file mode 100644 index 0000000..03fa126 --- /dev/null +++ b/kplayer/util/ImageCache.swift @@ -0,0 +1,113 @@ +import Foundation +import UIKit.UIImage +import Combine + +// https://medium.com/@mshcheglov/reusable-image-cache-in-swift-9b90eb338e8d +// https://github.com/sgl0v/OnSwiftWings/blob/master/ImageCache.playground/Sources/MovieTableViewCell.swift + +// Declares in-memory image cache +public protocol ImageCacheType: class { + // Returns the image associated with a given url + func image(for url: URL) -> UIImage? + // Inserts the image of the specified url in the cache + func insertImage(_ image: UIImage?, for url: URL) + // Removes the image of the specified url in the cache + func removeImage(for url: URL) + // Removes all images from the cache + func removeAllImages() + // Accesses the value associated with the given key for reading and writing + subscript(_ url: URL) -> UIImage? { get set } +} + +public final class ImageCache: ImageCacheType { + + // 1st level cache, that contains encoded images + private lazy var imageCache: NSCache = { + let cache = NSCache() + cache.countLimit = config.countLimit + return cache + }() + // 2nd level cache, that contains decoded images + private lazy var decodedImageCache: NSCache = { + let cache = NSCache() + cache.totalCostLimit = config.memoryLimit + return cache + }() + private let lock = NSLock() + private let config: Config + + public struct Config { + public let countLimit: Int + public let memoryLimit: Int + + public static let defaultConfig = Config(countLimit: 100, memoryLimit: 1024 * 1024 * 100) // 100 MB + } + + public init(config: Config = Config.defaultConfig) { + self.config = config + } + + public func image(for url: URL) -> UIImage? { + lock.lock(); defer { lock.unlock() } + // the best case scenario -> there is a decoded image in memory + if let decodedImage = decodedImageCache.object(forKey: url as AnyObject) as? UIImage { + return decodedImage + } + // search for image data + if let image = imageCache.object(forKey: url as AnyObject) as? UIImage { + let decodedImage = image.decodedImage() + decodedImageCache.setObject(image as AnyObject, forKey: url as AnyObject, cost: decodedImage.diskSize) + return decodedImage + } + return nil + } + + public func insertImage(_ image: UIImage?, for url: URL) { + guard let image = image else { return removeImage(for: url) } + let decompressedImage = image.decodedImage() + + lock.lock(); defer { lock.unlock() } + imageCache.setObject(decompressedImage, forKey: url as AnyObject, cost: 1) + decodedImageCache.setObject(image as AnyObject, forKey: url as AnyObject, cost: decompressedImage.diskSize) + } + + public func removeImage(for url: URL) { + lock.lock(); defer { lock.unlock() } + imageCache.removeObject(forKey: url as AnyObject) + decodedImageCache.removeObject(forKey: url as AnyObject) + } + + public func removeAllImages() { + lock.lock(); defer { lock.unlock() } + imageCache.removeAllObjects() + decodedImageCache.removeAllObjects() + } + + public subscript(_ key: URL) -> UIImage? { + get { + return image(for: key) + } + set { + return insertImage(newValue, for: key) + } + } +} + +fileprivate extension UIImage { + + func decodedImage() -> UIImage { + guard let cgImage = cgImage else { return self } + let size = CGSize(width: cgImage.width, height: cgImage.height) + let colorSpace = CGColorSpaceCreateDeviceRGB() + let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: cgImage.bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) + context?.draw(cgImage, in: CGRect(origin: .zero, size: size)) + guard let decodedImage = context?.makeImage() else { return self } + return UIImage(cgImage: decodedImage) + } + + // Rough estimation of how much memory image uses in bytes + var diskSize: Int { + guard let cgImage = cgImage else { return 0 } + return cgImage.bytesPerRow * cgImage.height + } +} diff --git a/kplayer/util/ImageLoader.swift b/kplayer/util/ImageLoader.swift new file mode 100644 index 0000000..2c2d1e7 --- /dev/null +++ b/kplayer/util/ImageLoader.swift @@ -0,0 +1,99 @@ +import Foundation +import UIKit.UIImage +import Combine + +public final class ImageLoader { + public static let shared = ImageLoader() + + public let cache: ImageCacheType + public lazy var backgroundQueue: OperationQueue = { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 5 + queue.qualityOfService = .userInteractive + return queue + }() + + private lazy var backgroundQueue2: OperationQueue = { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 5 + queue.qualityOfService = .userInteractive + return queue + }() + + public init(cache: ImageCacheType = ImageCache()) { + self.cache = cache + } + + public func loadImage(from url: URL) -> AnyPublisher { + loadImage(from: url, queue: backgroundQueue) + } + + public func loadImageBackground(from url: URL) -> AnyPublisher { + loadImage(from: url, queue: backgroundQueue) + } + + public func loadImage(from url: URL, queue: OperationQueue) -> AnyPublisher { + let bigUrl = URL(string: url.absoluteString.replacingOccurrences(of: "/ren/thumbs/", with: "/ren/"))! + if let image = cache[bigUrl] { + return Just(image).eraseToAnyPublisher() + } + return URLSession.shared.dataTaskPublisher(for: url) + .map { (data, response) -> UIImage? in + if let i = UIImage(data: data) { + return i.scaleDown(3000) + } + return nil + } + .catch { error in return Just(nil) } + .handleEvents(receiveOutput: {[unowned self] image in + guard let image = image else { return } + self.cache[bigUrl] = image + + print("Thumb Image loaded \(url):") + }) + // .print("Image loading \(url):") + .subscribe(on: queue) + .receive(on: RunLoop.main) + .eraseToAnyPublisher() + } + + public func loadFullImage(from url: URL) -> AnyPublisher { + print("Full Image loading \(url):") + + backgroundQueue2.cancelAllOperations() + + return URLSession.shared.dataTaskPublisher(for: url) + .map { (data, response) -> UIImage? in + if let r = response as? HTTPURLResponse { + if r.statusCode >= 400 { + return nil + } + } + if let i = UIImage(data: data) { + + if i.size.height > 3000 { + let targetSize = CGSize(width: 3000, height: 3000) + +// Compute the scaling ratio for the width and height separately + let widthScaleRatio = targetSize.width / i.size.width + let heightScaleRatio = targetSize.height / i.size.height + +// To keep the aspect ratio, scale by the smaller scaling ratio + let scaleFactor = min(widthScaleRatio, heightScaleRatio) + self.cache[url] = i.scaleToSize(i.size.width * scaleFactor, height: i.size.height * scaleFactor) + + } else { + self.cache[url] = i + } + + return i + } + return nil + } + .catch { error in return Just(nil) } + // .print("Full Image loading \(url):") + .subscribe(on: backgroundQueue2) + .receive(on: RunLoop.main) + .eraseToAnyPublisher() + } +} \ No newline at end of file diff --git a/kplayer/util/UIImageExtension.swift b/kplayer/util/UIImageExtension.swift index c84d0c9..bc7cacf 100644 --- a/kplayer/util/UIImageExtension.swift +++ b/kplayer/util/UIImageExtension.swift @@ -20,7 +20,23 @@ extension UIImage { UIGraphicsEndImageContext(); - return scaledImage!; + return scaledImage ?? self; + } + + func scaleDown(_ max: CGFloat) -> UIImage { + if size.height > max { + let targetSize = CGSize(width: max, height: max) + +// Compute the scaling ratio for the width and height separately + let widthScaleRatio = targetSize.width / size.width + let heightScaleRatio = targetSize.height / size.height + +// To keep the aspect ratio, scale by the smaller scaling ratio + let scaleFactor = min(widthScaleRatio, heightScaleRatio) + return scaleToSize(size.width * scaleFactor, height: size.height * scaleFactor) + } else { + return self + } } func decodeImage() -> UIImage? {