From af7f2143c7ddbc8e8361bbe425c7fc4909c12a7d Mon Sep 17 00:00:00 2001 From: marcoschmickler Date: Tue, 1 Nov 2022 14:21:22 +0100 Subject: [PATCH] Embedded --- kplayer.xcodeproj/project.pbxproj | 12 +- .../AppIcon.appiconset/Contents.json | 79 ++--- kplayer/core/DatabaseManager.swift | 35 ++- kplayer/core/KFrame.swift | 13 + kplayer/core/KSettings.swift | 9 +- kplayer/core/KSettingsModel.swift | 1 + .../core/KSnapshot+CoreDataProperties.swift | 2 +- kplayer/core/LocalManager.swift | 54 ++++ kplayer/core/MediaItem.swift | 4 + kplayer/core/MediaModel.swift | 2 + .../detail/DetailViewController+Show.swift | 9 +- kplayer/detail/DetailViewController.swift | 5 +- .../kplayer.xcdatamodeld/.xccurrentversion | 2 +- .../kplayer 3.xcdatamodel/contents | 38 +++ .../kplayer.xcdatamodel/contents | 4 +- kplayer/master/KSettingsView.swift | 3 + kplayer/master/MasterViewController.swift | 63 ++-- kplayer/photo/SPhotoAlbumView.swift | 7 +- kplayer/photo/SPhotoModel.swift | 37 +++ kplayer/photo/SPhotoView.swift | 41 ++- kplayer/util/VideoHelper.swift | 28 +- kplayer/video/SEmbeddedVideo.swift | 150 ++++++++++ kplayer/video/SVideoModel.swift | 2 + kplayer/video/SVideoPlayer.swift | 275 +++++++++++++----- kplayer/video/VideoPlayerView.swift | 20 +- 25 files changed, 714 insertions(+), 181 deletions(-) create mode 100644 kplayer/core/KFrame.swift create mode 100644 kplayer/kplayer.xcdatamodeld/kplayer 3.xcdatamodel/contents create mode 100644 kplayer/video/SEmbeddedVideo.swift diff --git a/kplayer.xcodeproj/project.pbxproj b/kplayer.xcodeproj/project.pbxproj index f67af7d..c759868 100644 --- a/kplayer.xcodeproj/project.pbxproj +++ b/kplayer.xcodeproj/project.pbxproj @@ -63,9 +63,11 @@ 1C736D24B49451141CD4B64D /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7369F53095B7A4D65679C2 /* DetailViewController.swift */; }; 1C736D5A7C7CB9B14AF0ECA6 /* DetailViewController+Show.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736B17A8E9FB352B90A903 /* DetailViewController+Show.swift */; }; 1C736D89CF86841F4C98A1F7 /* KPersistentContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7362DE1D6BE634D7C2ACBF /* KPersistentContainer.swift */; }; + 1C736DAE7F2A530AACDA0D18 /* KFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7363743CD8120637AC1EDE /* KFrame.swift */; }; 1C736DFA8544C773E3C22F10 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73675F8DDFA82DEADB542E /* VideoPlayerView.swift */; }; 1C736DFD076D9CC30F0B9D58 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736677D4EF2437358B2387 /* Utility.swift */; }; 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 */; }; 1C736F6A223D4ADB2E1BA733 /* ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736069C214E9522BB1BD97 /* ItemCell.swift */; }; 1C736F7D29B76C7037CEF778 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73647019E6C2E822127BA3 /* DatabaseManager.swift */; }; @@ -118,6 +120,7 @@ 1C736260E748CF136FF37EA7 /* UploadOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadOperation.swift; sourceTree = ""; }; 1C7362DE1D6BE634D7C2ACBF /* KPersistentContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KPersistentContainer.swift; sourceTree = ""; }; 1C73631C96E6C860833052CA /* ItemType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemType.swift; sourceTree = ""; }; + 1C7363743CD8120637AC1EDE /* KFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KFrame.swift; sourceTree = ""; }; 1C73645DBD6499A726D34973 /* links.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = links.html; sourceTree = ""; }; 1C73647019E6C2E822127BA3 /* DatabaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = ""; }; 1C7364709899FF62774B0199 /* VideoHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoHelper.swift; sourceTree = ""; }; @@ -133,6 +136,7 @@ 1C73673DC671535E3A049F54 /* PhotoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoController.swift; sourceTree = ""; }; 1C73675F8DDFA82DEADB542E /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; }; 1C736777456388CA571DA17B /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; }; + 1C7367B39F09CCD1760A345A /* SEmbeddedVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SEmbeddedVideo.swift; sourceTree = ""; }; 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 = ""; }; @@ -179,6 +183,7 @@ C98AF5E91B124D6A00D196CC /* kplayerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = kplayerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C98AF5EE1B124D6A00D196CC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C98AF5EF1B124D6A00D196CC /* kplayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = kplayerTests.swift; sourceTree = ""; }; + C9B052432905C56F002F65C0 /* kplayer 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "kplayer 3.xcdatamodel"; sourceTree = ""; }; DF1DF76780D9CDC55209D1CE /* Pods-kplayer.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-kplayer.release.xcconfig"; path = "Pods/Target Support Files/Pods-kplayer/Pods-kplayer.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -297,6 +302,7 @@ 1C736C94157754DE1C808173 /* KSettingsModel.swift */, 1C73659CC9B523B957E58DC6 /* LocalManager.swift */, 1C73647019E6C2E822127BA3 /* DatabaseManager.swift */, + 1C7363743CD8120637AC1EDE /* KFrame.swift */, ); path = core; sourceTree = ""; @@ -309,6 +315,7 @@ 1C73675F8DDFA82DEADB542E /* VideoPlayerView.swift */, 1C73661C3F9F4E53645551AD /* KToggleButton.swift */, 1C7360F9835128FC0A198ED0 /* SVideoLoopPlayer.swift */, + 1C7367B39F09CCD1760A345A /* SEmbeddedVideo.swift */, ); path = video; sourceTree = ""; @@ -642,6 +649,8 @@ 1C7368DBC47F0CC152504141 /* SPhotoScrubber.swift in Sources */, 1C736C76C1D80B649474F0A5 /* SPhotoAlbumView.swift in Sources */, 1C736D0A14C365F3E874420C /* SelfSizingHostingController.swift in Sources */, + 1C736DAE7F2A530AACDA0D18 /* KFrame.swift in Sources */, + 1C736EB38B780CA47B50772F /* SEmbeddedVideo.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -904,9 +913,10 @@ C98AF5D61B124D6A00D196CC /* kplayer.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + C9B052432905C56F002F65C0 /* kplayer 3.xcdatamodel */, C98AF5D71B124D6A00D196CC /* kplayer.xcdatamodel */, ); - currentVersion = C98AF5D71B124D6A00D196CC /* kplayer.xcdatamodel */; + currentVersion = C9B052432905C56F002F65C0 /* kplayer 3.xcdatamodel */; path = kplayer.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/kplayer/Images.xcassets/AppIcon.appiconset/Contents.json b/kplayer/Images.xcassets/AppIcon.appiconset/Contents.json index 1d060ed..9221b9b 100644 --- a/kplayer/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/kplayer/Images.xcassets/AppIcon.appiconset/Contents.json @@ -2,92 +2,97 @@ "images" : [ { "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" + "scale" : "2x", + "size" : "20x20" }, { "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" + "scale" : "3x", + "size" : "20x20" }, { "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" + "scale" : "3x", + "size" : "29x29" }, { "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" + "scale" : "2x", + "size" : "40x40" }, { "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" + "scale" : "3x", + "size" : "40x40" }, { "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" + "scale" : "2x", + "size" : "60x60" }, { "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" + "scale" : "3x", + "size" : "60x60" }, { "idiom" : "ipad", - "size" : "20x20", - "scale" : "1x" + "scale" : "1x", + "size" : "20x20" }, { "idiom" : "ipad", - "size" : "20x20", - "scale" : "2x" + "scale" : "2x", + "size" : "20x20" }, { "idiom" : "ipad", - "size" : "29x29", - "scale" : "1x" + "scale" : "1x", + "size" : "29x29" }, { "idiom" : "ipad", - "size" : "29x29", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" + "scale" : "1x", + "size" : "40x40" }, { "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" + "scale" : "2x", + "size" : "40x40" }, { "idiom" : "ipad", - "size" : "76x76", - "scale" : "1x" + "scale" : "1x", + "size" : "76x76" }, { "idiom" : "ipad", - "size" : "76x76", - "scale" : "2x" + "scale" : "2x", + "size" : "76x76" }, { "idiom" : "ipad", - "size" : "83.5x83.5", - "scale" : "2x" + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/kplayer/core/DatabaseManager.swift b/kplayer/core/DatabaseManager.swift index 53d211e..0dcd0c3 100644 --- a/kplayer/core/DatabaseManager.swift +++ b/kplayer/core/DatabaseManager.swift @@ -47,6 +47,7 @@ class DatabaseManager { c.offset.y = CGFloat(s.offy) c.scale = s.scale c.rating = Int(s.rating) + c.options = s.options ?? "" for t in s.tags as! Set { c.tags.append(t.name!) @@ -194,6 +195,7 @@ class DatabaseManager { snap.rating = Int16(c.rating) snap.thumb = c.thumbUrl + snap.options = c.options var ct = [String](c.tags) @@ -336,6 +338,7 @@ class DatabaseManager { sitem.loaded = true sitem.compilation = true sitem.objectID = i.objectID + sitem.local = i.local let c = MediaItem(name: i.name!, path: i.path!, root: i.root!, type: ctype) c.time = s.time @@ -349,6 +352,10 @@ class DatabaseManager { c.rating = Int(s.rating) c.objectID = s.objectID + if s.options != nil { + c.options = s.options! + } + if s.thumb != nil { c.thumbUrl = s.thumb } @@ -370,7 +377,33 @@ class DatabaseManager { return sitem } - func addTag(_ name: String, snapshot: KSnapshot) { + func loadTag(_ name: String) -> [MediaItem] { + var items = [MediaItem]() + let kFetch = KTag.fetchRequest() + kFetch.predicate = NSPredicate(format: "name == %@", name) + let tags = try! managedObjectContext.fetch(kFetch) + + if tags.isEmpty { + return items + } + + let tag = tags[0] + for s in tag.tagged! as! Set { + let item = loadSnapshot(s: s) + /* + let i = s.item! + let sitem = MediaItem(name: i.name!, path: i.path!, root: i.root!, type: ItemType.VIDEO) + sitem.loaded = true + sitem.local = i.local + + print(sitem.playerURL!)*/ + items.append(item) + } + + return items + } + + func addTag(_ name: String, snapshot: KSnapshot) { let kFetch = KTag.fetchRequest() kFetch.predicate = NSPredicate(format: "name == %@", name) let tags = try! managedObjectContext.fetch(kFetch) diff --git a/kplayer/core/KFrame.swift b/kplayer/core/KFrame.swift new file mode 100644 index 0000000..ca73289 --- /dev/null +++ b/kplayer/core/KFrame.swift @@ -0,0 +1,13 @@ +// +// Created by Marco Schmickler on 23.10.22. +// Copyright (c) 2022 Marco Schmickler. All rights reserved. +// + +import Foundation + +class KFrame : Codable, Identifiable { + var time = 0.0 + var scale = 1.0 + var x = 0 + var y = 0 +} \ No newline at end of file diff --git a/kplayer/core/KSettings.swift b/kplayer/core/KSettings.swift index 1836854..fb87854 100644 --- a/kplayer/core/KSettings.swift +++ b/kplayer/core/KSettings.swift @@ -7,7 +7,7 @@ import Foundation class KSettings: ObservableObject { @Published - var scale = Float(0.5) + var scale = Float(1.5) @Published var autoloop = false @@ -18,17 +18,24 @@ class KSettings: ObservableObject { @Published var zoomed = false + @Published + var jump = false + @Published var edit = false @Published var automaticallyWaitsToMinimizeStalling = true + @Published + var embeddedVideoUrl : URL? + convenience init(model: KSettingsModel) { self.init() scale = model.scale autoloop = model.autoloop zoomed = model.zoomed + jump = model.jump slow = model.slow } diff --git a/kplayer/core/KSettingsModel.swift b/kplayer/core/KSettingsModel.swift index 9dbbcff..6cecdc5 100644 --- a/kplayer/core/KSettingsModel.swift +++ b/kplayer/core/KSettingsModel.swift @@ -9,5 +9,6 @@ struct KSettingsModel: Codable { var scale = Float(0.5) var autoloop = false var zoomed = false + var jump = false var slow = false } diff --git a/kplayer/core/KSnapshot+CoreDataProperties.swift b/kplayer/core/KSnapshot+CoreDataProperties.swift index 6481e3c..4440a90 100644 --- a/kplayer/core/KSnapshot+CoreDataProperties.swift +++ b/kplayer/core/KSnapshot+CoreDataProperties.swift @@ -31,7 +31,7 @@ extension KSnapshot { @NSManaged public var index: Int32 @NSManaged public var item: KItem? @NSManaged public var tags: NSSet? - + @NSManaged public var options: String? } // MARK: Generated accessors for tags diff --git a/kplayer/core/LocalManager.swift b/kplayer/core/LocalManager.swift index 9a54ea9..1dc9315 100644 --- a/kplayer/core/LocalManager.swift +++ b/kplayer/core/LocalManager.swift @@ -100,6 +100,58 @@ class LocalManager { } } + func loadDir(path: String) -> [MediaItem] { + var res = [MediaItem]() + var items = [String:MediaItem]() + let url = FileHelper.getDocumentsDirectory().appendingPathComponent(path) + + if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) { + for case let fileURL as URL in enumerator { + do { + let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey]) + if fileAttributes.isRegularFile! { + if (fileURL.pathExtension == "mp4") { + let fi = MediaItem(name: fileURL.lastPathComponent, path: "", root: "fav", type: ItemType.PICFOLDER) + fi.local = true + fi.loaded = true + fi.externalURL = fileURL.absoluteString + // fi.thumbUrl = fileURL.absoluteString + ".jpg" + + items[fileURL.lastPathComponent] = fi + res.append(fi) + print(fileURL.absoluteString) + } + } + } + catch { + print(error) + } + } + } + if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) { + for case let fileURL as URL in enumerator { + do { + let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey]) + if fileAttributes.isRegularFile! { + if (fileURL.pathExtension == "jpg") { + let name = fileURL.lastPathComponent + let end = name.substringBefore(".mp4") + if let i = items[end + ".mp4"] { + if i.thumbUrl == nil { + i.thumbUrl = fileURL.absoluteString + } + } + } + } + } + catch { + print(error) + } + } + } + return res + } + func loadFavDirs(_ url: URL, completionHandler: @escaping Weiter) -> Void { var res = [MediaItem]() @@ -196,6 +248,8 @@ class LocalManager { i.externalURL = fileURL.absoluteString i.local = true } + let pathComponents = fileURL.pathComponents + mediaItem.path = pathComponents[pathComponents.count-2] DatabaseManager.sharedInstance.saveItemMetaData(mediaItem) res.append(mediaItem) } catch { diff --git a/kplayer/core/MediaItem.swift b/kplayer/core/MediaItem.swift index 6c836ee..8266836 100644 --- a/kplayer/core/MediaItem.swift +++ b/kplayer/core/MediaItem.swift @@ -94,6 +94,8 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable { var cookies = "" + var options = "" + convenience init(model: MediaModel) { self.init(name: model.name, path: model.path, root: model.root, type: model.type) @@ -110,6 +112,7 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable { self.local = true self.indexId = model.indexId self.tags = model.tags + self.options = model.options for m in model.children { let item = MediaItem(model: m) @@ -141,6 +144,7 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable { model.favorite = favorite model.indexId = indexId model.tags = tags + model.options = options return model } diff --git a/kplayer/core/MediaModel.swift b/kplayer/core/MediaModel.swift index b1433ef..fc13c41 100644 --- a/kplayer/core/MediaModel.swift +++ b/kplayer/core/MediaModel.swift @@ -26,4 +26,6 @@ public struct MediaModel : Codable { var children: [MediaModel] let type: ItemType var tags = [String]() + + var options = "" } diff --git a/kplayer/detail/DetailViewController+Show.swift b/kplayer/detail/DetailViewController+Show.swift index 7fc4ba8..1bfc3ac 100644 --- a/kplayer/detail/DetailViewController+Show.swift +++ b/kplayer/detail/DetailViewController+Show.swift @@ -9,11 +9,11 @@ import SwiftUI extension DetailViewController { - func showDetails(sectionItem: MediaItem, selectedItem: MediaItem) { + func showDetails(sectionItem: MediaItem, selectedItem: MediaItem, section: Int) { if sectionItem.isVideo() { showVideo(selectedItem: selectedItem) } else if sectionItem.isPic() { - showPhotos(sectionItem.children, selectedItem: selectedItem) + showPhotos(sectionItem.children, selectedItem: selectedItem, section: section) } else if sectionItem.isWeb() { showWeb(selectedItem: selectedItem) } else if sectionItem.type == ItemType.DOWNLOAD { @@ -21,7 +21,7 @@ extension DetailViewController { } } - func showPhotos(_ im: [MediaItem], selectedItem: MediaItem) { + func showPhotos(_ im: [MediaItem], selectedItem: MediaItem, section: Int) { let base = MediaItem(name: "", path: "", root: "", type: ItemType.PICFOLDER) base.children = im if im.count < 1 { @@ -29,6 +29,8 @@ extension DetailViewController { } let model = SPhotoModel(allItems: base.clone().children) model.selectItem(selectedItem) + model.folderItems = detailItem!.children + model.folderIndex = section let view = SPhotoAlbumView(completionHandler: { saved in self.collectionView.reloadData() @@ -128,6 +130,7 @@ extension DetailViewController { model.edit = delegate!.settings().edit model.slow = delegate!.settings().slow model.zoomed = delegate!.settings().zoomed + model.jump = delegate!.settings().jump let player = SVideoPlayer(completionHandler: { saved in if saved { diff --git a/kplayer/detail/DetailViewController.swift b/kplayer/detail/DetailViewController.swift index 8b123b5..0f67fdc 100644 --- a/kplayer/detail/DetailViewController.swift +++ b/kplayer/detail/DetailViewController.swift @@ -363,10 +363,9 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout print(selectedItem.name) } // } - self.showDetails(sectionItem: self.currentItem!, selectedItem: selectedItem) + self.showDetails(sectionItem: self.currentItem!, selectedItem: selectedItem, section: indexPath.section) } - - delegate!.loadDetails(selectedItem: currentItem!, completionHandler: weiter) + delegate!.loadDetails(selectedItem: currentItem!, completionHandler: weiter) } } diff --git a/kplayer/kplayer.xcdatamodeld/.xccurrentversion b/kplayer/kplayer.xcdatamodeld/.xccurrentversion index 79a3f68..3a28ba0 100644 --- a/kplayer/kplayer.xcdatamodeld/.xccurrentversion +++ b/kplayer/kplayer.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - kplayer.xcdatamodel + kplayer 3.xcdatamodel diff --git a/kplayer/kplayer.xcdatamodeld/kplayer 3.xcdatamodel/contents b/kplayer/kplayer.xcdatamodeld/kplayer 3.xcdatamodel/contents new file mode 100644 index 0000000..a03bd05 --- /dev/null +++ b/kplayer/kplayer.xcdatamodeld/kplayer 3.xcdatamodel/contents @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/kplayer/kplayer.xcdatamodeld/kplayer.xcdatamodel/contents b/kplayer/kplayer.xcdatamodeld/kplayer.xcdatamodel/contents index 33522f2..22b2e84 100644 --- a/kplayer/kplayer.xcdatamodeld/kplayer.xcdatamodel/contents +++ b/kplayer/kplayer.xcdatamodeld/kplayer.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -31,7 +31,7 @@ - + \ No newline at end of file diff --git a/kplayer/master/KSettingsView.swift b/kplayer/master/KSettingsView.swift index 98600a8..b6aee1c 100644 --- a/kplayer/master/KSettingsView.swift +++ b/kplayer/master/KSettingsView.swift @@ -24,6 +24,9 @@ struct KSettingsView: View { Toggle(isOn: $kSettings.zoomed, label: { Text("Zoomed") }) + Toggle(isOn: $kSettings.jump, label: { + Text("Jump") + }) Toggle(isOn: $kSettings.edit, label: { Text("Edit") }) diff --git a/kplayer/master/MasterViewController.swift b/kplayer/master/MasterViewController.swift index 99621d5..2e640af 100644 --- a/kplayer/master/MasterViewController.swift +++ b/kplayer/master/MasterViewController.swift @@ -13,7 +13,7 @@ import SwiftUI typealias Weiter = ([MediaItem]) -> Void -protocol MasterDelegate : DetailDelegate { +protocol MasterDelegate: DetailDelegate { func loadFolder(selectedItem: MediaItem, completionHandler: @escaping (MediaItem) -> Void) func loadItemDetails(selectedItem: MediaItem, completionHandler: @escaping (MediaItem) -> Void) @@ -22,13 +22,13 @@ protocol MasterDelegate : DetailDelegate { class MasterViewController: UITableViewController, UISearchResultsUpdating, UITableViewDropDelegate, UIDocumentPickerDelegate { let searchController = UISearchController(searchResultsController: nil) let model = ItemModel() - var delegate : MasterDelegate? - var detailDelegate : DetailDelegate? - var detailController : DetailViewController? + var delegate: MasterDelegate? + var detailDelegate: DetailDelegate? + var detailController: DetailViewController? var authenticated = false - var currentSelection : MediaItem? + var currentSelection: MediaItem? override func awakeFromNib() { super.awakeFromNib() @@ -57,7 +57,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa let str = search.searchBar.text! if str.hasSuffix("*") { - let txt = str[str.startIndex ..< str.index(str.endIndex, offsetBy: -1)] + let txt = str[str.startIndex.. UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) let item = model.items[indexPath.row] @@ -253,6 +254,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa } // Document Picker + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { print("hello") @@ -286,6 +288,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa } // Drag and drop + func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { if tableView.indexPathForSelectedRow == destinationIndexPath { @@ -305,8 +308,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa return UITableViewDropProposal(operation: .forbidden) } } - } - else { + } else { return UITableViewDropProposal(operation: .forbidden) } @@ -329,7 +331,9 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa // attempt to load strings from the drop coordinator let progress = coordinator.session.loadObjects(ofClass: NSString.self) { items in // convert the item provider array to a string array or bail out - guard let strings = items as? [String] else { return } + guard let strings = items as? [String] else { + return + } let r = destinationIndexPath.row if r < self.model.items.count { @@ -342,6 +346,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa let items = try! JSONDecoder().decode(MediaModel.self, from: string.data(using: .utf8)!) let m = MediaItem(model: items) m.local = true + m.externalURL = nil let at = m.playerURL! if destinationItem.root == "/tags" { @@ -350,20 +355,34 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa } m.path = destinationItem.path + m.externalURL = nil var to = m.playerURL! print(destinationItem.name) do { - try FileManager.default.moveItem(at: at, to: to) - - try FileHelper.setExcludeFromiCloudBackup(&to, isExcluded: true) - try FileManager.default.moveItem(at: at.appendingPathExtension("json"), to: to.appendingPathExtension("json")) + do { + try FileManager.default.moveItem(at: at, to: to) + try FileHelper.setExcludeFromiCloudBackup(&to, isExcluded: true) + } catch { + print(error) + } + do { + try FileManager.default.moveItem(at: at.appendingPathExtension("json"), to: to.appendingPathExtension("json")) + } catch { + print(error) + } + do { + try FileManager.default.moveItem(at: at.appendingPathExtension("orig"), to: to.appendingPathExtension("orig")) + } catch { + print(error) + } do { try FileManager.default.moveItem(at: at.appendingPathExtension("jpg"), to: to.appendingPathExtension("jpg")) } catch { + print(error) } - let weiter:Weiter = { + let weiter: Weiter = { (g) in let fav = LocalManager.sharedInstance.favorites ItemModel().sortItems(selectedItem: fav, children: g) @@ -374,7 +393,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa let refreshed = self.model.items[s.row] self.detailController!.detailItem = refreshed } - } + } LocalManager.sharedInstance.loadFavDirs(FileHelper.getDocumentsDirectory(), completionHandler: weiter) } catch { @@ -387,10 +406,11 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa } - // Authentication +// Authentication + func doAuthenticate() { let authenticationContext = LAContext() - var error : NSError? + var error: NSError? guard authenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { showAlert(title: "Error", message: "This device does not have a TouchID sensor.") @@ -404,5 +424,6 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa LocalManager.sharedInstance.authenticated = success }) } + } diff --git a/kplayer/photo/SPhotoAlbumView.swift b/kplayer/photo/SPhotoAlbumView.swift index 97914c0..015f414 100644 --- a/kplayer/photo/SPhotoAlbumView.swift +++ b/kplayer/photo/SPhotoAlbumView.swift @@ -13,6 +13,8 @@ struct SPhotoAlbumView: View { @State var more = false @State var edit = false + @State var embedded = false + @State var embDown = false init(completionHandler: ((Bool) -> ())?, model: SPhotoModel) { self.completionHandler = completionHandler @@ -38,6 +40,7 @@ struct SPhotoAlbumView: View { Text("\(model.index)").frame(width: 70) })//.foregroundColor(update ? Color.yellow : Color.blue) .buttonStyle(BorderlessButtonStyle()) + KToggleButton(text: "embd", binding: $embedded) Spacer() SPhotoScrubber(model: model) } @@ -45,7 +48,9 @@ struct SPhotoAlbumView: View { .frame(height: 50) SPhotoView(model: model) } - if more { + 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: { diff --git a/kplayer/photo/SPhotoModel.swift b/kplayer/photo/SPhotoModel.swift index 7fc9182..86905b8 100644 --- a/kplayer/photo/SPhotoModel.swift +++ b/kplayer/photo/SPhotoModel.swift @@ -7,6 +7,9 @@ import Foundation import SwiftUI class SPhotoModel : ObservableObject { + @Published var folderItems = [MediaItem]() + @Published var folderIndex = 0 + @Published var allItems : [MediaItem] @Published var indexItems : [MediaItem] @Published var selectedItem : MediaItem @@ -21,6 +24,13 @@ class SPhotoModel : ObservableObject { init(allItems: [MediaItem]) { self.allItems = allItems selectedItem = allItems[0] + indexItems = allItems + update(allItems: allItems) + } + + private func update(allItems: [MediaItem]) { + self.allItems = allItems + selectedItem = allItems[0] let max = 17 @@ -61,4 +71,31 @@ class SPhotoModel : ObservableObject { } } } + + func back() { + if (folderIndex > 0) { + folderIndex -= 1 + } else { + folderIndex = folderItems.count - 1 + } + show() + } + + func next() { + if (folderIndex < folderItems.count - 1) { + folderIndex += 1 + } else { + folderIndex = 0 + } + show() + } + + func show() { + var selectedItem = folderItems[folderIndex] + + NetworkManager.sharedInstance.loadPicDetails(items: selectedItem, result: { (im: [MediaItem]) in + self.index = 0 + self.update(allItems: im) + }) + } } diff --git a/kplayer/photo/SPhotoView.swift b/kplayer/photo/SPhotoView.swift index 963f811..018917a 100644 --- a/kplayer/photo/SPhotoView.swift +++ b/kplayer/photo/SPhotoView.swift @@ -36,6 +36,7 @@ struct SPhotoView: View { @State var startindex = -1 @State var animateX = 0 @State var dampen = 0.05 + @State var jump = LocalManager.sharedInstance.settings.jump init(model: SPhotoModel) { self.model = model @@ -45,18 +46,20 @@ struct SPhotoView: View { GeometryReader { geo in // AsyncImage(item: model.selectedItem, thumb: false, placeholder: { Text("Loading ...") }, image: { Image(uiImage: $0).resizable() }) - 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("...") + ZStack { + SwiftUI.AsyncImage(url: URL(string: model.allItems[model.index].imageUrlAbsolute)) { image in + image.resizable().scaledToFit() } - } - .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)) + 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)) + } .onTapGesture(count: 2) { print("3 tapped!") if model.scale == 1 { @@ -76,10 +79,20 @@ struct SPhotoView: View { if startindex < 0 { startindex = model.index } + print("\(startindex) \(dragged.width)") + if model.scale == 1 { //&& gesture.startLocation.x < 400 { + if (jump && dragged.height > 120) { + model.next() + return + } + + if (jump && dragged.height < -120) { + model.back() + return + } + + model.index = startindex + Int(dragged.width / 10.0) - if gesture.startLocation.y < 200 { //&& gesture.startLocation.x < 400 { - model.index = startindex + Int(dragged.width / 30.0) - print(dragged.width) if model.index >= model.allItems.count { model.index = model.allItems.count - 1 } diff --git a/kplayer/util/VideoHelper.swift b/kplayer/util/VideoHelper.swift index a288685..0aa31b7 100644 --- a/kplayer/util/VideoHelper.swift +++ b/kplayer/util/VideoHelper.swift @@ -7,7 +7,7 @@ import Foundation import AVFoundation class VideoHelper { - public static func export(item: AVPlayerItem, clipStart: Double, clipDuration: Double, file: URL, progress: @escaping (Float) -> (), completion: @escaping (URL?) -> ()) { + public static func export(item: AVPlayerItem, clipStart: Double, clipDuration: Double, file: URL, snapshot: MediaItem, zoomed: Bool, progress: @escaping (Float) -> (), completion: @escaping (URL?) -> ()) { guard item.asset.isExportable else { completion(nil) return @@ -31,7 +31,7 @@ class VideoHelper { } let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: composition) - var preset: String = AVAssetExportPresetPassthrough + var preset: String = zoomed ? AVAssetExportPresetHEVC1920x1080 : AVAssetExportPresetPassthrough // if compatiblePresets.contains(AVAssetExportPreset3840x2160) { preset = AVAssetExportPreset3840x2160 } guard @@ -41,6 +41,30 @@ class VideoHelper { return } + if zoomed { + let vc = AVMutableVideoComposition() + vc.renderSize = CGSize(width: 1920, height: 1080) + vc.renderScale = 1.0 + vc.frameDuration = CMTimeMake(value: 1, timescale: 29) + + let instruction = AVMutableVideoCompositionInstruction() + instruction.timeRange = CMTimeRangeMake(start: CMTime.zero, duration: item.duration) + let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: sourceVideoTrack) + + let h = 0 //sourceVideoTrack.naturalSize.height / 2; + let w = 0 // sourceVideoTrack.naturalSize.height / 2; + let y = snapshot.offset.y - 70 + let f = -1.36 * 2.0 + let rotation: CGAffineTransform = CGAffineTransform(scaleX: 1.0, y: 1.0).translatedBy(x: f * snapshot.offset.x , y: f * y ) + + //CGAffineTransformMakeScale(-1, 1) + layerInstruction.setTransform(rotation, at: CMTime.zero) + instruction.layerInstructions.append(layerInstruction) + vc.instructions = [instruction] + + exportSession.videoComposition = vc + } + exportSession.outputURL = file exportSession.outputFileType = AVFileType.mp4 let startTime = CMTime(seconds: 0.0, preferredTimescale: 100); diff --git a/kplayer/video/SEmbeddedVideo.swift b/kplayer/video/SEmbeddedVideo.swift new file mode 100644 index 0000000..1d827f4 --- /dev/null +++ b/kplayer/video/SEmbeddedVideo.swift @@ -0,0 +1,150 @@ +// +// Created by Marco Schmickler on 30.10.22. +// Copyright (c) 2022 Marco Schmickler. All rights reserved. +// + +import Foundation +import AVKit +import UIKit +import SwiftUI + +struct SEmbeddedVideo: View { + @State var player = AVQueuePlayer(items: [AVPlayerItem]()) + @State var embSmall = false + @State var half = false + @State var slow = false + @State var controls = false + @State var emLooper: AVPlayerLooper? + private let embedded: Binding + private let down: Binding + @State + var model: SVideoModel = SVideoModel(allItems: [], currentSnapshot: MediaItem(name: "", path: "", root: "", type: .VIDEO), baseItem: MediaItem(name: "", path: "", root: "", type: .VIDEO)) + + @State private var lastScaleValue: CGFloat = 1.0 + @State private var lastDragOffset: CGSize = CGSize.zero + + init(embedded: Binding, down: Binding) { + self.embedded = embedded + self.down = down + } + + var body: some View { + VStack { + VideoPlayerView(model: model, player: player).onAppear{ +// if let url = LocalManager.sharedInstance.settings.embeddedVideoUrl { +// playUrl(url: url) +// } + //else { + let items = LocalManager.sharedInstance.loadDir(path: "loop") + model.allItems = items + + if items.isEmpty { + embedded.wrappedValue = false + } + else { + playUrl(url: items[0].playerURL!) + } + // } + }.scaleEffect(half ? model.scale * 2.0 : model.scale).offset(model.dragOffset).onDisappear { + player.pause() // + }.gesture( + DragGesture() + .onChanged { gesture in + let dragged = gesture.translation + + //if move(dragged, start: gesture.startLocation) { + let f = 1.5 + model.dragOffset = CGSize(width: f*dragged.width + lastDragOffset.width, height: f*dragged.height + lastDragOffset.height) + // } + } + .onEnded { gesture in + lastDragOffset = model.dragOffset + } + ) + .gesture(MagnificationGesture() + .onChanged { val in + let delta = val / self.lastScaleValue + self.lastScaleValue = val + self.model.scale = self.model.scale * delta + } + .onEnded { val in + // without this the next gesture will be broken + self.lastScaleValue = 1.0 + }).onTapGesture(count: 2) { + controls = !controls + }.contentShape(Rectangle()) + let c = VStack { + VideoPlayerControlsView(model: model, player: player) + HStack { + Button(action: { + embedded.wrappedValue = false + }, label: { + Text("cancel") + }) + .buttonStyle(BorderlessButtonStyle()) + KToggleButton(text: "small", binding: $embSmall).frame(height: 30) + KToggleButton(text: "half", binding: $half).frame(height: 30) + KToggleButton(text: "down", binding: down).frame(height: 30) + Button(action: { + slow.toggle() + player.rate = slow ? 0.5 : 1.0 + + }, label: { + Text("slow") + }).foregroundColor(slow ? Color.yellow : Color.blue) + .frame(height: 30) + .buttonStyle(BorderlessButtonStyle()) + } + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(model.allItems) { item in + Button(action: { + model.currentSnapshot = item + playUrl(url: item.playerURL!) + }) { + AsyncImage(item: item, placeholder: { Text(item.name) }, + image: { Image(uiImage: $0).resizable() }).frame(width: 80, height: 60).border(.yellow, width: (item===model.currentSnapshot) ? 1 : 0) + .overlay(Image(systemName: "repeat.circle").offset(x: 20, y: -20).opacity((item.length > 0.0) ? 1 : 0)) + } + } + } + } + } + if (controls){ + c + } + else { + c.hidden() + } + }.frame(width: computeWidth(), height: computeHeight()).clipped() + } + + private func playUrl(url: URL) { + let item = AVPlayerItem(asset: AVURLAsset(url: url)) + player.replaceCurrentItem(with: item) + emLooper = AVPlayerLooper(player: player, templateItem: item) + + player.play() + } + + func computeWidth() -> CGFloat { + var width = 600 + if embSmall { + width = 400 + } + if half { + width /= 2 + } + return CGFloat(width) + } + + func computeHeight() -> CGFloat { + var height = 400 + if embSmall { + height = 300 + } + + return CGFloat(height) + } + +} diff --git a/kplayer/video/SVideoModel.swift b/kplayer/video/SVideoModel.swift index 900a97b..1a8dd14 100644 --- a/kplayer/video/SVideoModel.swift +++ b/kplayer/video/SVideoModel.swift @@ -11,6 +11,7 @@ class SVideoModel : ObservableObject { @Published var allItems : [MediaItem] @Published var currentSnapshot: MediaItem @Published var baseItem: MediaItem + @Published var frames = [KFrame]() // The progress through the video, as a percentage (from 0 to 1) @Published var videoPos: Double = 0 @@ -26,6 +27,7 @@ class SVideoModel : ObservableObject { @Published var loop = false @Published var slow = false @Published var zoomed = false + @Published var jump = false @Published var favorite = false @Published var speed: Float = 1.0 diff --git a/kplayer/video/SVideoPlayer.swift b/kplayer/video/SVideoPlayer.swift index 609caba..5bbe682 100644 --- a/kplayer/video/SVideoPlayer.swift +++ b/kplayer/video/SVideoPlayer.swift @@ -27,8 +27,11 @@ struct SVideoPlayer: View, EditItemDelegate { @State var seekSmoothly = false @State var upsidedown = false @State var more = false + @State var frames = false + @State var small = false + @State var embedded = false + @State var embDown = false @State var tilt = false - @State var jump = false @State var smoothTime = -1.0 @State var smoothSeekTime = -1.0 @State var timeCounter = 0 @@ -89,7 +92,7 @@ struct SVideoPlayer: View, EditItemDelegate { } } - KToggleButton(text: "more", binding: $more) + KToggleButton(text: "\(relative())", binding: $more) Button(action: { model.favorite.toggle() @@ -119,6 +122,7 @@ struct SVideoPlayer: View, EditItemDelegate { } } + KToggleButton(text: "embd", binding: $embedded).frame(height: 30) Text(model.currentSnapshot.name).foregroundColor(Color.blue) Text(""" @@ -141,15 +145,26 @@ struct SVideoPlayer: View, EditItemDelegate { Spacer() - Button(action: { model.edit.toggle() }, label: { - Text("edit") - }) - .buttonStyle(BorderlessButtonStyle()); - if !model.baseItem.compilation { - Button(action: doSnapshot, label: { - Text("snap") + Group { + Button(action: { model.edit.toggle() }, label: { + Text("edit") }) .buttonStyle(BorderlessButtonStyle()); + + if !model.baseItem.compilation { + if model.currentSnapshot != nil { + Button(action: addFrame, label: { + Text("frame") + }) + .buttonStyle(BorderlessButtonStyle()) + } + + Button(action: doSnapshot, label: { + Text("snap") + }) + .buttonStyle(BorderlessButtonStyle()); + + } } } .frame(height: 50) @@ -159,9 +174,9 @@ struct SVideoPlayer: View, EditItemDelegate { VStack { let v = VideoPlayerView(model: model, player: player) - .scaleEffect(model.scale) + .scaleEffect(small ? 1 : model.scale) .rotation3DEffect(.degrees(upsidedown ? 180 : 0), axis: (x: 1, y: 0, z: 0)) - .offset(model.dragOffset).offset(x: xoffs, y: 0) + .offset(small ? CGSize.zero : model.dragOffset).modifier(KAnimate(dragOffset: model.dragOffset, spring: false)).offset(x: xoffs, y: 0) .onTapGesture(count: 2) { print("3 tapped!") if model.scale == 1 { @@ -178,7 +193,8 @@ struct SVideoPlayer: View, EditItemDelegate { let dragged = gesture.translation if move(dragged, start: gesture.startLocation) { - model.dragOffset = CGSize(width: dragged.width + lastDragOffset.width, height: dragged.height + lastDragOffset.height) + let f = 1.5 + model.dragOffset = CGSize(width: f*dragged.width + lastDragOffset.width, height: f*dragged.height + lastDragOffset.height) } } .onEnded { gesture in @@ -202,7 +218,14 @@ struct SVideoPlayer: View, EditItemDelegate { v.overlay(EditItemView(item: model.currentSnapshot, len: model.videoDuration, delegate: self) .frame(width: 420, alignment: .top).offset(x: 0, y: -50), alignment: .topTrailing) } else { - if more { + if embedded && !more { + v.overlay(SEmbeddedVideo(embedded: $embedded, down: $embDown), alignment: embDown ? .bottomLeading : .topLeading) + } else + if small && !more { + let normal = 385.0 + v.overlay(Rectangle().stroke(Color.black, lineWidth: 2.0).opacity(0.3).frame(width: normal * 16.0 / 9.0, height: normal).offset(model.dragOffset), alignment: .topLeading) + } + else if more { v.overlay(VStack { Button(action: { @@ -218,17 +241,15 @@ struct SVideoPlayer: View, EditItemDelegate { .frame(height: 30) .foregroundColor(tilt ? Color.yellow : Color.blue).buttonStyle(BorderlessButtonStyle()) - KToggleButton(text: "slow", binding: $model.slow).frame(height: 30) - KToggleButton(text: "zoom", binding: $model.zoomed).frame(height: 30) - KToggleButton(text: "jump", binding: $jump).frame(height: 30) - KToggleButton(text: "loop", binding: $model.loop).frame(height: 30) -// .fullScreenCover(isPresented: $model.loop) { -// SVideoLoopPlayer(completionHandler: { -// model.loop = false -// }, baseModel: model) -// } + Group { + KToggleButton(text: "slow", binding: $model.slow).frame(height: 30) + KToggleButton(text: "zoom", binding: $model.zoomed).frame(height: 30) + KToggleButton(text: "jump", binding: $model.jump).frame(height: 30) + KToggleButton(text: "small", binding: $small).frame(height: 30) - KToggleButton(text: "flip", binding: $upsidedown).frame(height: 30) + KToggleButton(text: "loop", binding: $model.loop).frame(height: 30) + KToggleButton(text: "flip", binding: $upsidedown).frame(height: 30) + } Button(action: { if model.speed == 1.0 && timeSlomoCounter == 0 { @@ -297,7 +318,35 @@ struct SVideoPlayer: View, EditItemDelegate { }) } .frame(width: 50, alignment: .top).offset(x: 0, y: 0), alignment: .topLeading) - } else { + } + else if frames && model.zoomed { + v.overlay(VStack { + ForEach(model.frames) { f in + Button(action: { + seekTime(model.currentSnapshot.time + f.time) + model.dragOffset.width = CGFloat(f.x) + model.dragOffset.height = CGFloat(f.y) + model.scale = f.scale + }, label: { + Text("\(f.time, specifier: "%.2f")") + }).simultaneousGesture( + LongPressGesture() + .onEnded { _ in + print("Loooong") + + for i in 0...model.frames.count - 1 { + if model.frames[i].time == f.time { + model.frames.remove(at: i) + break + } + } + updateOptions() + } + ) + } + }.frame(width: 70, alignment: .top).offset(x: 0, y: 0), alignment: .topTrailing) + } + else { v } } @@ -320,49 +369,7 @@ struct SVideoPlayer: View, EditItemDelegate { } .onAppear() { model.observed = player - model.observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.02, preferredTimescale: 600), queue: nil) { time in - if timeCounter >= 1 { - timeCounter -= 1 - } - if timeSlomoCounter >= 1 { - timeSlomoCounter -= 1 - } - if model.loop && model.currentSnapshot.length > 0 { - if time.seconds > model.currentSnapshot.time + model.currentSnapshot.length { - seekTime(model.currentSnapshot.time) - } - } - if tilt { - if let data = motionManager.deviceMotion { - var rotation = atan2(data.gravity.x, - data.gravity.y) - .pi - - if rotation < (-1 * .pi) { - rotation += .pi + .pi - } - rotation *= 100.0 - if rotazero == -1000 { - rotazero = rotation - } - - rotation -= rotazero - - // let roll = accelerometerData.attitude.yaw - if rotation > 30 { - xoffs += 3; - // print(xoffs) - } - if rotation < -30 { - xoffs -= 3; - // print(xoffs) - } - // print(Int(rotation * 100.0) ) - } - } else { - xoffs = 0.0 - rotazero = -1000 - } - } + model.observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.02, preferredTimescale: 600), queue: nil, using: timeObserver()) player.automaticallyWaitsToMinimizeStalling = LocalManager.sharedInstance.settings.automaticallyWaitsToMinimizeStalling @@ -404,7 +411,71 @@ struct SVideoPlayer: View, EditItemDelegate { } + private func timeObserver() -> (CMTime) -> () { + { time in + if timeCounter >= 1 { + timeCounter -= 1 + } + if timeSlomoCounter >= 1 { + timeSlomoCounter -= 1 + } + if model.loop && model.currentSnapshot.length > 0 { + if time.seconds > model.currentSnapshot.time + model.currentSnapshot.length { + seekTime(model.currentSnapshot.time) + } + } + if model.zoomed { + if time.seconds > model.currentSnapshot.time { + let relativeTime = time.seconds - model.currentSnapshot.time; + + for f in model.frames { + // print("\(f.time) \(relativeTime)") + if f.time > relativeTime && f.time < relativeTime + 1 { + model.scale = CGFloat(f.scale) + model.dragOffset.width = CGFloat(f.x) + model.dragOffset.height = CGFloat(f.y) + break + } + } + } + } + if tilt { + if let data = motionManager.deviceMotion { + var rotation = atan2(data.gravity.x, + data.gravity.y) - .pi + + if rotation < (-1 * .pi) { + rotation += .pi + .pi + } + rotation *= 100.0 + if rotazero == -1000 { + rotazero = rotation + } + + rotation -= rotazero + + // let roll = accelerometerData.attitude.yaw + if rotation > 30 { + xoffs += 3; + // print(xoffs) + } + if rotation < -30 { + xoffs -= 3; + // print(xoffs) + } + // print(Int(rotation * 100.0) ) + } + } else { + xoffs = 0.0 + rotazero = -1000 + } + } + } + private func closePlayer(withSave: Bool) { + if model.currentSnapshot.local { + LocalManager.sharedInstance.settings.embeddedVideoUrl = model.currentURL + } model.baseItem.favorite = model.favorite player.pause() player.replaceCurrentItem(with: nil) @@ -456,7 +527,7 @@ struct SVideoPlayer: View, EditItemDelegate { return false } else { - if model.scale != 1.0 { + if model.scale != 1.0 || small { return true } else { @@ -471,7 +542,7 @@ struct SVideoPlayer: View, EditItemDelegate { } } - if dragged.height > 100 && jump { + if dragged.height > 100 && model.jump { if timeCounter == 0 { if let i = model.allItems.index(where: { m in m === model.currentSnapshot }) { if i + 1 < model.allItems.count { @@ -489,7 +560,7 @@ struct SVideoPlayer: View, EditItemDelegate { if let time = getCurrentTime() { let dragWidth = 20.0 if !model.seeking { - if model.scale > 1.0 && start.y > 200 { + if (model.scale > 1.0 || small) && start.y > 200 { return true } @@ -515,6 +586,15 @@ struct SVideoPlayer: View, EditItemDelegate { return false } + private func relative() -> String { + if let time = getCurrentTime() { + return Utility.formatSecondsToHMS(time - model.currentSnapshot.time) + } + else { + return "more" + } + } + private func getCurrentTime() -> Double? { player.currentItem?.currentTime().seconds } @@ -534,9 +614,6 @@ struct SVideoPlayer: View, EditItemDelegate { } player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(600))) { _ in -// player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(10000)), -// toleranceBefore: CMTime(seconds: 0.3, preferredTimescale: CMTimeScale(10000)), -// toleranceAfter: CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(10000))){ _ in if !model.paused { player.play() player.rate = model.speed @@ -574,6 +651,26 @@ struct SVideoPlayer: View, EditItemDelegate { func gotoSnapshot(_ currentSnapshot: MediaItem) { model.currentSnapshot = currentSnapshot + let lines = currentSnapshot.options.split(whereSeparator: \.isNewline) + model.frames.removeAll() + + frames = false + + for l in lines { + let values = l.components(separatedBy: CharacterSet.whitespacesAndNewlines) ?? [] + if (values.count == 4) { + var f = KFrame() + f.time = Double(values[0]) ?? 0.0 + f.scale = Double(values[1]) ?? 1.0 + f.x = Int(values[2]) ?? 0 + f.y = Int(values[3]) ?? 0 + model.frames.append(f) + + frames = true + } + print(l) + } + if currentSnapshot.playerURL != model.currentURL { model.currentURL = currentSnapshot.playerURL @@ -617,6 +714,8 @@ struct SVideoPlayer: View, EditItemDelegate { f = f * CGFloat(LocalManager.sharedInstance.settings.scale) } model.scale = f + model.dragOffset.width = 0 + model.dragOffset.height = 0 } } @@ -648,6 +747,32 @@ struct SVideoPlayer: View, EditItemDelegate { } } + func addFrame() { + if let time = getCurrentTime() { + var f = KFrame() + f.time = time - model.currentSnapshot.time + f.scale = model.scale + f.x = Int(model.dragOffset.width) + f.y = Int(model.dragOffset.height) + + model.frames.append(f) + updateOptions() + + frames = true + } + } + + private func updateOptions() { + var options = "" + + for f in model.frames { + options.append("\(String(format: "%.2f", f.time))\t\(String(format: "%.2f", f.scale))\t\(f.x)\t\(f.y)\n") + } + + model.currentSnapshot.options = options + model.dirty = true + } + func setEnd() { if let time = getCurrentTime() { let snapTime = model.currentSnapshot.time @@ -665,7 +790,7 @@ struct SVideoPlayer: View, EditItemDelegate { func okEdit() { DatabaseManager.sharedInstance.saveItemMetaData(model.currentSnapshot) model.edit = false - model.dirty = false + model.dirty = true } func seek(_ v: Double) { @@ -687,7 +812,7 @@ struct SVideoPlayer: View, EditItemDelegate { return } player.pause() - VideoHelper.export(item: player.currentItem!, clipStart: c.time, clipDuration: dur, file: file, + VideoHelper.export(item: player.currentItem!, clipStart: c.time, clipDuration: dur, file: file, snapshot:c, zoomed: model.zoomed, progress: { p in let percent = Int(p * 100) savetext = "\(percent)" diff --git a/kplayer/video/VideoPlayerView.swift b/kplayer/video/VideoPlayerView.swift index a7eb603..a5dc2af 100644 --- a/kplayer/video/VideoPlayerView.swift +++ b/kplayer/video/VideoPlayerView.swift @@ -11,7 +11,7 @@ import AVFoundation // This is the UIView that contains the AVPlayerLayer for rendering the video class VideoPlayerUIView: UIView { private let player: AVPlayer - private let playerLayer = AVPlayerLayer() + public let playerLayer = AVPlayerLayer() private let videoPos: Binding private let videoDuration: Binding private let seeking: Binding @@ -26,7 +26,7 @@ class VideoPlayerUIView: UIView { super.init(frame: .zero) - backgroundColor = .black + // backgroundColor = .black playerLayer.player = player layer.addSublayer(playerLayer) @@ -54,22 +54,6 @@ class VideoPlayerUIView: UIView { } } } -// -// override func viewDidLayoutSubviews() { -// playerLayer.frame = view.bounds -// let deviceOrientation = UIDevice.current.orientation -// switch deviceOrientation { -// case .landscapeLeft: -// playerLayer.connection!.videoOrientation = .landscapeRight -// case .landscapeRight: -// playerLayer.connection!.videoOrientation = .landscapeLeft -// case .portraitUpsideDown: -// playerLayer.connection!.videoOrientation = .portraitUpsideDown -// case .portrait: -// playerLayer.connection!.videoOrientation = .portrait -// default: -// player.connection!.videoOrientation = .portrait -// } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented")