From cfb72bae671bff865dbd124cee150454b3a67562 Mon Sep 17 00:00:00 2001 From: marcoschmickler Date: Sat, 25 Jun 2022 18:12:15 +0200 Subject: [PATCH] Photos --- kplayer.xcodeproj/project.pbxproj | 4 + kplayer/core/LocalManager.swift | 2 +- kplayer/core/MediaItem.swift | 2 +- kplayer/photo/SPhotoModel.swift | 20 ++++ kplayer/photo/SPhotoScrubber.swift | 89 +++++++++++++++++ kplayer/photo/SPhotoView.swift | 151 ++++++++++++++++++----------- kplayer/util/AsyncImage.swift | 11 +-- 7 files changed, 215 insertions(+), 64 deletions(-) create mode 100644 kplayer/photo/SPhotoScrubber.swift diff --git a/kplayer.xcodeproj/project.pbxproj b/kplayer.xcodeproj/project.pbxproj index 942ef99..6f37fb0 100644 --- a/kplayer.xcodeproj/project.pbxproj +++ b/kplayer.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 1C736821D6DF2237A3EABCC1 /* ViewControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73648CEC974A2500172064 /* ViewControllerExtensions.swift */; }; 1C7368242038C0FF6C9631E7 /* VideoHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7364709899FF62774B0199 /* VideoHelper.swift */; }; 1C73688D13E5A804880C8768 /* UIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736DCCE3AA9993E15F7652 /* UIImageExtension.swift */; }; + 1C7368DBC47F0CC152504141 /* SPhotoScrubber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7368EFFBCD688E0D425117 /* SPhotoScrubber.swift */; }; 1C73691A9C7174E0C6B57267 /* stringutil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736B794396F2E50387B8F2 /* stringutil.swift */; }; 1C73693A1334A7792856FC58 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73611D226B48C24DB37535 /* MasterViewController.swift */; }; 1C736953BDBBAFC40884132A /* BrowserController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73602350ACE2436736F981 /* BrowserController.swift */; }; @@ -132,6 +133,7 @@ 1C7367ECBD369A2A0C94C499 /* FileHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHelper.swift; sourceTree = ""; }; 1C73685B4BBFDAFBF08C032C /* readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = readme.md; sourceTree = ""; }; 1C73688DAB88F9360FB62A49 /* MediaItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = ""; }; + 1C7368EFFBCD688E0D425117 /* SPhotoScrubber.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SPhotoScrubber.swift; sourceTree = ""; }; 1C7369BED02028D8564E82D5 /* pathfinder.scpt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.scpt; path = pathfinder.scpt; sourceTree = ""; }; 1C7369EC16B19B32B515169E /* NetData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetData.swift; sourceTree = ""; }; 1C7369F53095B7A4D65679C2 /* DetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; @@ -226,6 +228,7 @@ 1C73673DC671535E3A049F54 /* PhotoController.swift */, 1C736AD9F0A39AD543FC1176 /* SPhotoView.swift */, 1C736F83C6190A3A0D5BD1F0 /* SPhotoModel.swift */, + 1C7368EFFBCD688E0D425117 /* SPhotoScrubber.swift */, ); path = photo; sourceTree = ""; @@ -630,6 +633,7 @@ 1C736C00693A05747578DF87 /* grabber.js in Sources */, 1C73620BC7F5A1F35FFFF9FC /* SPhotoView.swift in Sources */, 1C736BEC4C4263EF6A89E9E3 /* SPhotoModel.swift in Sources */, + 1C7368DBC47F0CC152504141 /* SPhotoScrubber.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/kplayer/core/LocalManager.swift b/kplayer/core/LocalManager.swift index 9a54ea9..46562f0 100644 --- a/kplayer/core/LocalManager.swift +++ b/kplayer/core/LocalManager.swift @@ -10,7 +10,7 @@ class LocalManager { var settings = KSettings() - var authenticated = false + var authenticated = true var favorites = MediaItem(name: "fav", path: "", root: "", type: ItemType.FAVROOT) diff --git a/kplayer/core/MediaItem.swift b/kplayer/core/MediaItem.swift index 2157319..cf66f27 100644 --- a/kplayer/core/MediaItem.swift +++ b/kplayer/core/MediaItem.swift @@ -207,7 +207,7 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable { } let enc = thumbUrl!.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)! - return NetworkManager.sharedInstance.vidurl + enc.substringStartingFrom(10) + return NetworkManager.sharedInstance.baseurl + "/service/download/srv/samba" + enc.substringStartingFrom(10) } var nameWithoutExtension: String { diff --git a/kplayer/photo/SPhotoModel.swift b/kplayer/photo/SPhotoModel.swift index f0987ff..eebd2a1 100644 --- a/kplayer/photo/SPhotoModel.swift +++ b/kplayer/photo/SPhotoModel.swift @@ -7,11 +7,31 @@ import Foundation class SPhotoModel : ObservableObject { @Published var allItems : [MediaItem] + @Published var indexItems : [MediaItem] @Published var selectedItem : MediaItem @Published var index = 0 + @Published var thumbIndex = 0 + @Published var scrub = false init(allItems: [MediaItem]) { self.allItems = allItems selectedItem = allItems[0] + + let max = 17 + + if allItems.count < max { + indexItems = allItems + } + else { + indexItems = [MediaItem]() + var nj = -1 + for i in 0...allItems.count-1 { + let j = Int((Double(i) / Double(allItems.count)) * 17.0) + if j > nj { + nj = j + indexItems.append(allItems[i]) + } + } + } } } diff --git a/kplayer/photo/SPhotoScrubber.swift b/kplayer/photo/SPhotoScrubber.swift new file mode 100644 index 0000000..3f2b175 --- /dev/null +++ b/kplayer/photo/SPhotoScrubber.swift @@ -0,0 +1,89 @@ +// +// Created by Marco Schmickler on 25.06.22. +// Copyright (c) 2022 Marco Schmickler. All rights reserved. +// + +import Foundation +import SwiftUI +import Haneke + +struct SPhotoScrubber: View { + @ObservedObject var model: SPhotoModel + @State var proxy: GeometryProxy? + + init(model: SPhotoModel) { + self.model = model + } + + var body: some View { + GeometryReader { geometry in + HStack { + ForEach(model.indexItems) { item in + AsyncImage(item: item, placeholder: { Text("Loading ...") }, + image: { Image(uiImage: $0).resizable() }) + } + .frame(height: 50) + } + .onAppear() { + proxy = geometry + } + .gesture( + DragGesture() + .onChanged { gesture in + let dragged = gesture.translation + + let xpos = Double(gesture.startLocation.x + dragged.width) + let width = Double(proxy!.size.width - 30) + + let i = Int(Double(model.allItems.count - 1) * (xpos / width)) + + setIndex(i: i) + + + print("size \(width) xpos \(xpos) i \(i)") + + model.scrub = true + } + .onEnded { gesture in + let xpos = gesture.predictedEndLocation.x + let width = Double(proxy!.size.width - 30) + + let i = Int(Double(model.allItems.count - 1) * (xpos / width)) + + setIndex(i: i) + + model.scrub = false + + print("size \(width) xpos \(xpos) i \(i)") + } + ) + } + } + + private func setIndex(i: Int) { + if (i < model.allItems.count && i >= 0) { + model.index = i + + let item = model.allItems[i] + + if item === model.selectedItem { + return + } + + model.selectedItem = item + + if item.thumbUrl != nil && item.thumbImage == nil { + item.thumbImage = UIImage(systemName: "repeat") + let URL = Foundation.URL(string: item.thumbUrlAbsolute)! + + // print("fetch \(item.thumbUrlAbsolute)") + + Shared.imageCache.fetch(URL: URL).onSuccess { + i in + //newItem.image = i + item.thumbImage = i + } + } + } + } +} diff --git a/kplayer/photo/SPhotoView.swift b/kplayer/photo/SPhotoView.swift index 21120eb..ccfc2d3 100644 --- a/kplayer/photo/SPhotoView.swift +++ b/kplayer/photo/SPhotoView.swift @@ -6,26 +6,61 @@ import Foundation import SwiftUI -struct SPhotoView : View { +struct KAnimate: ViewModifier { + var dragOffset: CGSize + var spring: Bool + + init(dragOffset: CGSize, spring: Bool) { + self.dragOffset = dragOffset + self.spring = spring + } + + func body(content: Content) -> some View { + if (spring) { + content.animation(.spring(response: 10, dampingFraction: 0.05), value: dragOffset) + } + else { + content.animation(.easeOut(duration: 3), value: dragOffset) + } + } +} + +struct SPhotoView: View { var completionHandler: ((Bool) -> Void)? @ObservedObject var model: SPhotoModel - @State var scrub = false @State var scale: CGFloat = 1.0 @State var lastScaleValue: CGFloat = 1.0 - @State var proxy: GeometryProxy? + @State var lastDragOffset: CGSize = CGSize.zero + @State var dragOffset: CGSize = CGSize.zero + @State var lastDrag: CGSize = CGSize.zero + @State var skip = false + @State var more = false + @State var spring = false + @State var animateX = 0 + @State var dampen = 0.05 var body: some View { - VStack { + let v = VStack { HStack { Group { Button(action: { + cleanup() completionHandler!(true) }, label: { - Text("cancel") + //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()) Spacer() + SPhotoScrubber(model: model) } }.frame(height: 50) GeometryReader { geo in @@ -35,12 +70,44 @@ struct SPhotoView : View { } placeholder: { if let i = model.selectedItem.thumbImage { - Image(uiImage: i) .resizable().scaledToFill() + Image(uiImage: i).resizable().scaledToFill() } else { - ProgressView() + Text("...") } - }.frame(height: geo.size.height - 120) - .scaleEffect(scale) + } + .frame(height: geo.size.height - 50) + .scaleEffect(scale).offset(dragOffset).modifier(KAnimate(dragOffset: dragOffset, spring: spring)) + .gesture( + DragGesture() + .onChanged { gesture in + let dragged = gesture.translation + let multi = (spring) ? 2.0 : 3.0 + + if gesture.startLocation.y < 100 && gesture.startLocation.x < 400 { + if dragged.width > 50 && model.index < model.allItems.count - 1 && !skip { + model.index += 1 + skip = true + } + if dragged.width < -50 && model.index > 0 && !skip { + model.index -= 1 + skip = true + } + } else { + if (abs(dragged.width - lastDrag.width) > 20) { + + } else { + dragOffset = CGSize(width: (dragged.width * multi) + lastDragOffset.width, height: (dragged.height * multi) + lastDragOffset.height) + } + } + dampen = 0.05 + lastDrag = dragged + } + .onEnded { gesture in + lastDragOffset = dragOffset + dampen = 0.05 + skip = false + } + ) .gesture(MagnificationGesture() .onChanged { val in let delta = val / self.lastScaleValue @@ -53,55 +120,31 @@ struct SPhotoView : View { // without this the next gesture will be broken self.lastScaleValue = 1.0 }) - if scale <= 1.0 { - GeometryReader { geometry in - HStack { - ForEach(model.indexItems) { item in - AsyncImage(item: item, placeholder: { Text("Loading ...") }, - image: { Image(uiImage: $0).resizable() }) - } - .frame(height: 70) - }.onAppear() { - proxy = geometry - } - .gesture( - DragGesture() - .onChanged { gesture in - let dragged = gesture.translation - - let xpos = Double(gesture.startLocation.x + dragged.width) - let width = Double(proxy!.size.width - 30) - - let i = Int(Double(model.allItems.count - 1) * (xpos / width)) - if (i < model.allItems.count) { - model.thumbIndex = i - } - - print("size \(width) xpos \(xpos) i \(i)") - - scrub = true - } - .onEnded { gesture in - let xpos = gesture.predictedEndLocation.x - let width = Double(proxy!.size.width - 30) - let i = Int(Double(model.allItems.count - 1) * (xpos / width)) + } + .contentShape(Rectangle()).clipped(); - if (i < model.allItems.count) { - model.index = i - - } - - scrub = false + } - print("size \(width) xpos \(xpos) i \(i)") - } - ) - }.overlay( - (model.allItems[model.thumbIndex].thumbImage != nil) ? Image(uiImage: model.allItems[model.thumbIndex].thumbImage!).frame(width: 200, height: 200, alignment: .top).offset(x: 0, y: -150) : nil) - } - }.contentShape(Rectangle()).clipped(); + if more { + v.overlay(VStack { + Button(action: { + spring.toggle() + }, label: { + Text("spring") + }).foregroundColor(spring ? Color.yellow : Color.blue) + .buttonStyle(BorderlessButtonStyle()) + }.frame(width: 50, alignment: .top).offset(x: 0, y: 70), alignment: .topLeading) + } + else { + v + } + } + func cleanup() { + for i in model.allItems { + i.thumbImage = nil + i.image = nil } } } diff --git a/kplayer/util/AsyncImage.swift b/kplayer/util/AsyncImage.swift index 32d7659..5a3d4cc 100644 --- a/kplayer/util/AsyncImage.swift +++ b/kplayer/util/AsyncImage.swift @@ -7,17 +7,14 @@ struct AsyncImage: View { @StateObject private var item: MediaItem private let placeholder: Placeholder private let image: (UIImage) -> Image - private let thumb: Bool init( item: MediaItem, - thumb: Bool = true, @ViewBuilder placeholder: () -> Placeholder, @ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:) ) { self.placeholder = placeholder() self.image = image - self.thumb = thumb _item = StateObject(wrappedValue: item) setImage(newItem: item) @@ -42,17 +39,15 @@ struct AsyncImage: View { } func setImage(newItem: MediaItem) { - if thumb && newItem.thumbImage != nil { - return - } - if newItem.image != nil { newItem.thumbImage = newItem.image!.scaleToSize(66.0, height: 44.0) newItem.image = nil - } else { + } else if newItem.thumbImage == nil { if newItem.thumbUrl != nil { let URL = Foundation.URL(string: newItem.thumbUrlAbsolute)! + print("fetch \(newItem.thumbUrlAbsolute)") + Shared.imageCache.fetch(URL: URL).onSuccess { i in