Browse Source

Embedded

master
marcoschmickler 3 years ago
parent
commit
e22df2436c
  1. 8
      kplayer.xcodeproj/project.pbxproj
  2. 41
      kplayer/core/DatabaseManager.swift
  3. 4
      kplayer/core/KSettings.swift
  4. 9
      kplayer/core/MediaItem.swift
  5. 72
      kplayer/core/NetworkManager.swift
  6. 9
      kplayer/detail/DetailViewController+Show.swift
  7. 25
      kplayer/detail/DetailViewController.swift
  8. 7
      kplayer/detail/EditItemView.swift
  9. 64
      kplayer/photo/SPhotoAlbumView.swift
  10. 124
      kplayer/photo/SPhotoModel.swift
  11. 20
      kplayer/photo/SPhotoScrubber.swift
  12. 80
      kplayer/photo/SPhotoView.swift
  13. 104
      kplayer/server/kplayer.js
  14. 11
      kplayer/util/AsyncImage.swift
  15. 113
      kplayer/util/ImageCache.swift
  16. 99
      kplayer/util/ImageLoader.swift
  17. 18
      kplayer/util/UIImageExtension.swift

8
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 = "<group>"; };
1C736C72CDF8902484856B3B /* SelfSizingHostingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelfSizingHostingController.swift; sourceTree = "<group>"; };
1C736C94157754DE1C808173 /* KSettingsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KSettingsModel.swift; sourceTree = "<group>"; };
1C736CC8D90375E86CD01964 /* ImageLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = "<group>"; };
1C736D27EC608FAAFDB4A68C /* WebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
1C736D50A22FC4553165199D /* FlexibleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlexibleView.swift; sourceTree = "<group>"; };
1C736D9BB5498E7E8F11C754 /* HeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderCell.swift; sourceTree = "<group>"; };
1C736DBB6986A8B62963FBB3 /* HtmlParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HtmlParser.swift; sourceTree = "<group>"; };
1C736DCCE3AA9993E15F7652 /* UIImageExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = "<group>"; };
1C736DCD945ABAE984FF43EF /* KNetworkProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KNetworkProtocol.swift; sourceTree = "<group>"; };
1C736E32C8574BFE3536F1C2 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
1C736EA15A11AF7D57F85824 /* ThumbnailCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailCache.swift; sourceTree = "<group>"; };
1C736EEC570C71AAC50F2E95 /* SVideoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVideoModel.swift; sourceTree = "<group>"; };
1C736EF64DE56AD058A4F612 /* KBrowserView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KBrowserView.swift; sourceTree = "<group>"; };
@ -278,6 +282,8 @@
1C7360295486647982CFEACF /* UIViewController+Alert.swift */,
1C736D50A22FC4553165199D /* FlexibleView.swift */,
1C736C72CDF8902484856B3B /* SelfSizingHostingController.swift */,
1C736E32C8574BFE3536F1C2 /* ImageCache.swift */,
1C736CC8D90375E86CD01964 /* ImageLoader.swift */,
);
path = util;
sourceTree = "<group>";
@ -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;
};

41
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<KSnapshot> {
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 {

4
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 {

9
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)!

72
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)
}
}
}

9
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()

25
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

7
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)

64
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 {

124
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
}
}
}
}
}

20
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)
// }
}
}
}

80
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
})

104
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<String> 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<String> listvideos(HttpServletRequest request, HttpServletResponse response) {
String address = (String) request.getAttribute(

11
kplayer/util/AsyncImage.swift

@ -43,12 +43,11 @@ struct AsyncImage<Placeholder: View>: 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)
}
}
}
}

113
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<AnyObject, AnyObject> = {
let cache = NSCache<AnyObject, AnyObject>()
cache.countLimit = config.countLimit
return cache
}()
// 2nd level cache, that contains decoded images
private lazy var decodedImageCache: NSCache<AnyObject, AnyObject> = {
let cache = NSCache<AnyObject, AnyObject>()
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
}
}

99
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<UIImage?, Never> {
loadImage(from: url, queue: backgroundQueue)
}
public func loadImageBackground(from url: URL) -> AnyPublisher<UIImage?, Never> {
loadImage(from: url, queue: backgroundQueue)
}
public func loadImage(from url: URL, queue: OperationQueue) -> AnyPublisher<UIImage?, Never> {
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<UIImage?, Never> {
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()
}
}

18
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? {

Loading…
Cancel
Save