From 31aea522671b5407a1d8142227f342b560155e63 Mon Sep 17 00:00:00 2001 From: marcoschmickler Date: Sat, 7 Jan 2023 20:08:48 +0100 Subject: [PATCH] power --- kplayer.xcodeproj/project.pbxproj | 8 ++ kplayer/AppDelegate.swift | 3 + kplayer/core/DatabaseManager.swift | 37 ++++-- kplayer/core/MediaItem.swift | 2 +- kplayer/core/NetworkManager.swift | 119 +++++++++++++++++- kplayer/detail/EditItemView.swift | 2 +- kplayer/detail/ItemCell.swift | 6 + kplayer/master/KSettingsView.swift | 10 ++ kplayer/master/NetworkDelegate.swift | 2 +- kplayer/master/SearchItemView.swift | 2 +- kplayer/server/kplayer.js | 76 ++++++++--- kplayer/server/raspberrypi.js | 54 ++++++++ kplayer/video/SRangeSlider.swift | 135 ++++++++++++++++++++ kplayer/video/SVideoPlayer.swift | 180 ++++++++++++++++++--------- kplayer/video/VideoPlayerView.swift | 22 ++++ 15 files changed, 567 insertions(+), 91 deletions(-) create mode 100644 kplayer/server/raspberrypi.js create mode 100644 kplayer/video/SRangeSlider.swift diff --git a/kplayer.xcodeproj/project.pbxproj b/kplayer.xcodeproj/project.pbxproj index 9e010d7..b3ba4ee 100644 --- a/kplayer.xcodeproj/project.pbxproj +++ b/kplayer.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ 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 */; }; + 1C73670151CCBC714795807F /* SRangeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736E59A0C8DB9AC4D98F7B /* SRangeSlider.swift */; }; 1C7367084839D2E8B180DB74 /* UIViewController+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7360295486647982CFEACF /* UIViewController+Alert.swift */; }; 1C73671FC2CCCACAA2FFC153 /* ThumbnailCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736EA15A11AF7D57F85824 /* ThumbnailCache.swift */; }; 1C73675C34BE0990D44570BE /* ItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736253AB7A95EA41B605B7 /* ItemModel.swift */; }; @@ -68,6 +69,7 @@ 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 */; }; + 1C736E5C86EFC8E8C1ABA131 /* raspberrypi.js in Sources */ = {isa = PBXBuildFile; fileRef = 1C7367DB924D9AE21DD8D0F2 /* raspberrypi.js */; }; 1C736EB38B780CA47B50772F /* SEmbeddedVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7367B39F09CCD1760A345A /* SEmbeddedVideo.swift */; }; 1C736EC45EE7DA5F7FCE63DA /* LocalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73659CC9B523B957E58DC6 /* LocalManager.swift */; }; 1C736EFF1E09988625FF770C /* hints.md in Sources */ = {isa = PBXBuildFile; fileRef = 1C736BF48D5CE855B6E75BE6 /* hints.md */; }; @@ -142,6 +144,7 @@ 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 = ""; }; + 1C7367DB924D9AE21DD8D0F2 /* raspberrypi.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = raspberrypi.js; 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 = ""; }; @@ -168,6 +171,7 @@ 1C736DCCE3AA9993E15F7652 /* UIImageExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = ""; }; 1C736DCD945ABAE984FF43EF /* KNetworkProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KNetworkProtocol.swift; sourceTree = ""; }; 1C736E32C8574BFE3536F1C2 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; + 1C736E59A0C8DB9AC4D98F7B /* SRangeSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRangeSlider.swift; sourceTree = ""; }; 1C736EA15A11AF7D57F85824 /* ThumbnailCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailCache.swift; sourceTree = ""; }; 1C736EEC570C71AAC50F2E95 /* SVideoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVideoModel.swift; sourceTree = ""; }; 1C736EF64DE56AD058A4F612 /* KBrowserView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KBrowserView.swift; sourceTree = ""; }; @@ -238,6 +242,7 @@ 1C73615FFA2AA98BD1C56CD4 /* links.html */, 1C73645DBD6499A726D34973 /* links.html */, 1C73619BBFA9295A11C9ACBA /* grabber.js */, + 1C7367DB924D9AE21DD8D0F2 /* raspberrypi.js */, ); path = server; sourceTree = ""; @@ -331,6 +336,7 @@ 1C73661C3F9F4E53645551AD /* KToggleButton.swift */, 1C7360F9835128FC0A198ED0 /* SVideoLoopPlayer.swift */, 1C7367B39F09CCD1760A345A /* SEmbeddedVideo.swift */, + 1C736E59A0C8DB9AC4D98F7B /* SRangeSlider.swift */, ); path = video; sourceTree = ""; @@ -672,6 +678,8 @@ 1C736F2334FE0F946FD7CABE /* ImageCache.swift in Sources */, 1C7366BEA68D6E4CEE43417E /* ImageLoader.swift in Sources */, 1C736EFF1E09988625FF770C /* hints.md in Sources */, + 1C73670151CCBC714795807F /* SRangeSlider.swift in Sources */, + 1C736E5C86EFC8E8C1ABA131 /* raspberrypi.js in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/kplayer/AppDelegate.swift b/kplayer/AppDelegate.swift index eca8e0d..80cd0d5 100644 --- a/kplayer/AppDelegate.swift +++ b/kplayer/AppDelegate.swift @@ -29,6 +29,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele LocalManager.sharedInstance.loadSettings() + NetworkManager.sharedInstance.wakeLinkstation() + let url = URL(string: NetworkManager.sharedInstance.vidurl)?.appendingPathComponent("ren").appendingPathComponent("kplayer.txt") var roots = [MediaItem]() @@ -61,6 +63,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele roots.append(LocalManager.sharedInstance.favorites) roots.append(MediaItem(name: "extern", path:"", root: "", type: ItemType.FAVROOT)) roots.append(MediaItem(name: "tags", path:"", root: "", type: ItemType.TAGROOT)) + roots.append(MediaItem(name: "models", path:"models", root: "", type: ItemType.TAGROOT)) roots.append(web) controller.model.items = roots diff --git a/kplayer/core/DatabaseManager.swift b/kplayer/core/DatabaseManager.swift index 7d63d81..1b86435 100644 --- a/kplayer/core/DatabaseManager.swift +++ b/kplayer/core/DatabaseManager.swift @@ -13,7 +13,7 @@ class DatabaseManager { let managedObjectContext : NSManagedObjectContext let persistentContainer : NSPersistentContainer - var allTags = [String]() + var allTags = [String : [String]]() init() { self.persistentContainer = KPersistentContainer(defaultContainerWithName: "kplayer") @@ -49,12 +49,14 @@ class DatabaseManager { c.rating = Int(s.rating) c.options = s.options ?? "" - for t in s.tags as! Set { - c.tags.append(t.name!) + if let tags = s.tags as? Set { + for t in tags { + c.tags.append(t.name!) + } } } } - print(s) + // print(s) } return @@ -251,9 +253,10 @@ rollback() } - func createTag(_ answer: String) { + func createTag(_ answer: String, path: String = "") { let tag = KTag(context: managedObjectContext) tag.name = answer + tag.path = path save() @@ -268,7 +271,17 @@ rollback() allTags.removeAll() for t in results { - allTags.append(t.name!) + var path = "" + if t.path != nil { + path = t.path! + } + var tags = allTags[path] + + if tags == nil { + tags = [String]() + } + tags!.append(t.name!) + allTags[path] = tags } } @@ -310,7 +323,7 @@ rollback() } } - func loadTags(completionHandler: @escaping Weiter) -> Void { + func loadTags(path: String, completionHandler: @escaping Weiter) -> Void { var res = [MediaItem]() let fetchRequest = KTag.fetchRequest() @@ -318,6 +331,16 @@ rollback() let results = try! managedObjectContext.fetch(fetchRequest) for t in results { + if let p = t.path { + if path != p { + continue + } + } + else { + if path != "" { + continue + } + } let tag = MediaItem(name: t.name!, path: t.name!, root: "tags", type: ItemType.TAG) tag.loaded = true let snapshots = t.tagged as! Set diff --git a/kplayer/core/MediaItem.swift b/kplayer/core/MediaItem.swift index 12c9753..b0f8e68 100644 --- a/kplayer/core/MediaItem.swift +++ b/kplayer/core/MediaItem.swift @@ -220,7 +220,7 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable { } let enc = thumbUrl!.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)! - return NetworkManager.sharedInstance.baseurl + "/service/download/srv/samba" + enc.substringStartingFrom(10) + return NetworkManager.sharedInstance.vidurl + enc.substringStartingFrom(10) } var nameWithoutExtension: String { diff --git a/kplayer/core/NetworkManager.swift b/kplayer/core/NetworkManager.swift index bf0aad0..ebe271d 100644 --- a/kplayer/core/NetworkManager.swift +++ b/kplayer/core/NetworkManager.swift @@ -18,6 +18,9 @@ class NetworkManager { var currentDownloads = [String : MediaItem]() + var black = [String]() + var blackLoaded = false + lazy var operationQueue: OperationQueue = { var queue = OperationQueue() queue.name = "Backup queue" @@ -46,7 +49,7 @@ class NetworkManager { if root.endsWith("/") { root = String(root[root.startIndex.. Bool { + let url = URL(string: NetworkManager.sharedInstance.vidurl)?.appendingPathComponent("ren").appendingPathComponent("black.txt") + + if !blackLoaded { + do { + let remoteroots = try String(contentsOf: url!) + let components = remoteroots.split(separator: "\n") + for c in components { + if c.starts(with: "rm ") { + let s = String(c.suffix(c.count - 3)) + black.append(s) + } + } + } catch { + print("Exception") + } + blackLoaded = true + } + + let p = item.fullPath + + for c in black { + if p == c { + return true + } + } + + return false + } + + func blackItem(_ item: MediaItem) { + let p = item.fullPath + let url = nodeurl + "black" + p + black.append(p) + print(url) + AF.request(url).responseString { response in + print("black \(response)") + } + } + + func cutItem(_ item: MediaItem) { + var file = item.fullPath + var start = "\(item.time)" + var length = "\(item.length)" + + var newfile = file.replacingOccurrences(of: "/srv/samba/ren", with: "/srv/samba/ren/cut") + newfile = newfile.replacingOccurrences(of: ".mp4", with: "_" + start + ".mp4") + let furl = URL(fileURLWithPath: newfile) + let dirUrl = furl.deletingLastPathComponent().path + + var line = "mkdir -p " + dirUrl + "; ffmpeg -i '" + file + line += "' -ss " + start + " -t " + length + " -vcodec copy -acodec copy " + newfile + print(line) + let url = nodeurl + "cut?line=" + line.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)! + AF.request(url).responseString { response in + print("cut \(response)") + } + } + + func truncateItem(_ item: MediaItem) { + var file = item.fullPath + var start = "\(item.time)" + + print(file) + let url = nodeurl + "truncate?line=" + file.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!+"&start="+start + AF.request(url).responseString { response in + print("truncate \(response)") + } + } + func saveItem(_ item: MediaItem) { var ch = item.children // print(ch) diff --git a/kplayer/detail/EditItemView.swift b/kplayer/detail/EditItemView.swift index f8526bd..7a1ca68 100644 --- a/kplayer/detail/EditItemView.swift +++ b/kplayer/detail/EditItemView.swift @@ -27,7 +27,7 @@ struct TagEditor: View { var body: some View { FlexibleView( - data: DatabaseManager.sharedInstance.allTags, + data: DatabaseManager.sharedInstance.allTags[""]!, spacing: 15, alignment: .leading ) { tag in diff --git a/kplayer/detail/ItemCell.swift b/kplayer/detail/ItemCell.swift index 4d7c342..7375c6f 100644 --- a/kplayer/detail/ItemCell.swift +++ b/kplayer/detail/ItemCell.swift @@ -8,6 +8,7 @@ import UIKit import Haneke var defaultImage = UIImage(named: "Kirschkeks-256x256.png") +var blackImage = UIImage(systemName: "0.circle") class ItemCell: UICollectionViewCell { var item: MediaItem? @@ -71,6 +72,11 @@ class ItemCell: UICollectionViewCell { loop.isHidden = item.length == 0.0 + if NetworkManager.sharedInstance.isBlack(item) { + image.image = blackImage + return + } + if let _ = item.thumbUrl, let nsurl = URL(string: item.thumbUrlAbsolute) { image.hnk_setImageFromURL(nsurl, placeholder: defaultImage) } else { diff --git a/kplayer/master/KSettingsView.swift b/kplayer/master/KSettingsView.swift index b6aee1c..8b7bf2e 100644 --- a/kplayer/master/KSettingsView.swift +++ b/kplayer/master/KSettingsView.swift @@ -38,6 +38,16 @@ struct KSettingsView: View { }) } } + Button(action: { + NetworkManager.sharedInstance.suspendLinkstation() + }, label: { + Text("suspend") + }); + Button(action: { + NetworkManager.sharedInstance.wakeLinkstation() + }, label: { + Text("wake") + }); Button(action: { LocalManager.sharedInstance.saveSettings() self.completionHandler?() diff --git a/kplayer/master/NetworkDelegate.swift b/kplayer/master/NetworkDelegate.swift index 101debb..d4ad9d8 100644 --- a/kplayer/master/NetworkDelegate.swift +++ b/kplayer/master/NetworkDelegate.swift @@ -53,7 +53,7 @@ class NetworkDelegate: MasterDelegate, DetailDelegate { if selectedItem.type == ItemType.TAGROOT { - DatabaseManager.sharedInstance.loadTags(completionHandler: { + DatabaseManager.sharedInstance.loadTags(path: selectedItem.path, completionHandler: { c in selectedItem.children = c completionHandler(selectedItem) diff --git a/kplayer/master/SearchItemView.swift b/kplayer/master/SearchItemView.swift index debbff8..11453ad 100644 --- a/kplayer/master/SearchItemView.swift +++ b/kplayer/master/SearchItemView.swift @@ -15,7 +15,7 @@ struct SearchItemView: View { var body: some View { VStack { FlexibleView( - data: DatabaseManager.sharedInstance.allTags, + data: DatabaseManager.sharedInstance.allTags[""]!, spacing: 15, alignment: .leading ) { tag in diff --git a/kplayer/server/kplayer.js b/kplayer/server/kplayer.js index cbb39fc..25ea7fc 100644 --- a/kplayer/server/kplayer.js +++ b/kplayer/server/kplayer.js @@ -135,28 +135,35 @@ app.get('/listpicdirs/*', function (req, res) { if ((file.isDirectory() || file.isSymbolicLink()) && !filename.startsWith(".")) { var folders = getFiles(address + "/" + filename) + var hasFav = true - 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" + if (hasFav && f.name.toLowerCase().endsWith(".jpg")) { + var p = address + "/" + filename + p = p.replace("/srv/samba/ren", "/srv/samba/ren/favpic") + var fav = getFiles(p) + fav.forEach(function (g) { + result.push(address + "/" + filename + "/" + g.name) + console.log("f--" + g.name) + hasFav = false + }) + + if (hasFav) { + console.log("--" + f.name) + + var n = address + "/" + filename + "/" + f.name + var r = n.replace("/srv/samba/ren", "/srv/samba/ren/thumbs") + + if (fs.existsSync(r)) { + n = r + } + result.push(n) + hasFav = false } } }) - } console.log(filename) - - result.push([filename, filetype]) } }) @@ -167,7 +174,7 @@ app.get('/listvideos/*', function (req, res) { var address = decodeURIComponent(req.path.substr(11)) console.log("listdirs " + address); - if (!req.path.startsWith("/listfiles/srv/samba/ren/")) { + if (!req.path.startsWith("/listvideos/srv/samba/ren/")) { res.end("Access denied") return } @@ -432,6 +439,43 @@ app.get('/deleteThumb/*', function (req, res) { res.send("ok") }) +app.get('/black/*', function (req, res) { + if (req.path.startsWith("/black/srv/samba/ren/")) { + var p = req.path.substr(6) + + fs.appendFileSync('/srv/samba/ren/black.txt', 'rm '+p+'\ntouch '+p+'.black\n'); + } + res.send("ok") +}) + +app.get('/cut', function (req, res) { + var p = req.query.line + fs.appendFileSync('/srv/samba/ren/cut.txt', p+'\n'); + + res.send("ok") +}) + +app.get('/truncate', function (req, res) { + var p = req.query.line + var s = req.query.start + var n = p.replace(".mp4", ".orig.mp4") + + var cmd = 'mv "'+p+'" "' + n + '"; ffmpeg -i "'+n+'" -ss ' + s + ' -codec copy -movflags faststart "'+p+'"' + console.log(cmd) + exec(cmd, (err, stdout, stderr) => { + if (err) { + console.log(err) + // node couldn't execute the command + return; + } + + // the *entire* stdout and stderr (buffered) + console.log(`stdout: ${stdout}`); + console.log(`stderr: ${stderr}`); + }); + res.send("ok") +}) + //createDirectories(pathname) {} app.post('/upload', upload.any() , (req, res, next) => { diff --git a/kplayer/server/raspberrypi.js b/kplayer/server/raspberrypi.js new file mode 100644 index 0000000..61fdd10 --- /dev/null +++ b/kplayer/server/raspberrypi.js @@ -0,0 +1,54 @@ +// pm2 restart kplayer +// /srv/samba/daten/node + +var express = require('express'); +var fs = require("fs"); +var path = require("path"); + +const { exec, execSync} = require('child_process'); + +var app = express(); + +app.get('/', function(req, res) { + res.json({ message: 'WELCOME' }); +}); + +app.get('/wakelinkstation', function (req, res) { + var cmd = '/srv/raspi/bin/wakeup-linkstation.sh' + console.log(cmd) + exec(cmd, (err, stdout, stderr) => { + if (err) { + console.log(err) + // node couldn't execute the command + return; + } + + // the *entire* stdout and stderr (buffered) + console.log(`stdout: ${stdout}`); + console.log(`stderr: ${stderr}`); + }); + res.send("ok") +}) + +app.get('/suspendlinkstation', function (req, res) { + var cmd = '/srv/raspi/bin/suspend-linkstation.sh' + console.log(cmd) + exec(cmd, (err, stdout, stderr) => { + if (err) { + console.log(err) + // node couldn't execute the command + return; + } + + // the *entire* stdout and stderr (buffered) + console.log(`stdout: ${stdout}`); + console.log(`stderr: ${stderr}`); + }); + res.send("ok") +}) + +var server = app.listen(8081, function () { + var host = server.address().address + var port = server.address().port + console.log("Server listening at http://%s:%s", host, port) +}) diff --git a/kplayer/video/SRangeSlider.swift b/kplayer/video/SRangeSlider.swift new file mode 100644 index 0000000..3c59919 --- /dev/null +++ b/kplayer/video/SRangeSlider.swift @@ -0,0 +1,135 @@ +// +// Created by Marco Schmickler on 25.12.22. +// Copyright (c) 2022 Marco Schmickler. All rights reserved. +// + +import Foundation +import SwiftUI + +public struct SRangeSlider: View { + /// ` Slider` Binding min & max values + @Binding var minValue: Float + @Binding var maxValue: Float + + /// Set slider min & max Label values + let minLabel: String + let maxLabel: String + + /// Set slider width + let sliderWidth: Float + + /// `Slider` background track color + let backgroundTrackColor: Color + /// `Slider` selected track color + let selectedTrackColor: Color + + /// Globe background color + let globeColor: Color + /// Globe rounded boarder color + let globeBackgroundColor: Color + + /// Slider min & max static and dynamic labels value color + let sliderMinMaxValuesColor: Color + + /// `Slider` init + public init(minValue: Binding, + maxValue: Binding, + minLabel: String = "0", + maxLabel: String = "100", + sliderWidth: Float = 0, + backgroundTrackColor: Color = Color(UIColor.systemTeal).opacity(0.3), + selectedTrackColor: Color = Color.blue.opacity(25), + globeColor: Color = Color.orange, + globeBackgroundColor: Color = Color.black, + sliderMinMaxValuesColor: Color = Color.black) { + self._minValue = minValue + self._maxValue = maxValue + self.minLabel = minLabel + self.maxLabel = maxLabel + self.sliderWidth = sliderWidth + self.backgroundTrackColor = backgroundTrackColor + self.selectedTrackColor = selectedTrackColor + self.globeColor = globeColor + self.globeBackgroundColor = globeBackgroundColor + self.sliderMinMaxValuesColor = sliderMinMaxValuesColor + } + + /// `Slider` view setup + public var body: some View { + + VStack { + + /// `Slider` start & end static values show in view + HStack { + // start value + Text(minLabel) + .offset(x: 28, y: 20) + .frame(width: 30, height: 30, alignment: .leading) + .foregroundColor(sliderMinMaxValuesColor) + + Spacer() + // end value + Text(maxLabel) + .offset(x: -18, y: 20) + .frame(width: 30, height: 30, alignment: .trailing) + .foregroundColor(sliderMinMaxValuesColor) + } + + /// `Slider` track view with glob view + ZStack (alignment: Alignment(horizontal: .leading, vertical: .center), content: { + // background track view + Capsule() + .fill(backgroundTrackColor) + .frame(width: CGFloat(self.sliderWidth + 10), height: 30) + + // selected track view + Capsule() + .fill(selectedTrackColor) + .offset(x: CGFloat(self.minValue)) + .frame(width: CGFloat((self.maxValue) - self.minValue), height: 30) + + // minimum value glob view + Circle() + .fill(globeColor) + .frame(width: 30, height: 30) + .background(Circle().stroke(globeBackgroundColor, lineWidth: 2)) + .offset(x: CGFloat(self.minValue)) + .gesture(DragGesture().onChanged({ (value) in + /// drag validation + if value.location.x > 8 && Float(value.location.x) <= self.sliderWidth && + value.location.x < CGFloat(self.maxValue - 30) { + // set min value of slider + self.minValue = Float(value.location.x - 8) + } + })) + + // minimum value text draw inside minimum glob view + Text(String(format: "%.0f", (self.minValue / self.sliderWidth) * 100)) + .offset(x: CGFloat(self.minValue)) + .frame(width: 30, height: 30, alignment: .center) + .foregroundColor(sliderMinMaxValuesColor) + + // maximum value glob view + Circle() + .fill(globeColor) + .frame(width: 30, height: 30) + .background(Circle().stroke(globeBackgroundColor, lineWidth: 2)) + .offset(x: CGFloat(self.maxValue - 18)) + .gesture(DragGesture().onChanged({ (value) in + /// drag validation + if Float(value.location.x - 8) <= self.sliderWidth && value.location.x > CGFloat(self.minValue + 50) { + // set max value of slider + self.maxValue = Float(value.location.x - 8) + } + })) + + // maximum value text draw inside maximum glob view + Text(String(format: "%.0f", (self.maxValue / self.sliderWidth) * 100)) + .offset(x: CGFloat(self.maxValue - 18)) + .frame(width: 30, height: 30, alignment: .center) + .foregroundColor(sliderMinMaxValuesColor) + }) + .padding() + } + } +} diff --git a/kplayer/video/SVideoPlayer.swift b/kplayer/video/SVideoPlayer.swift index 5bbe682..a2e07a2 100644 --- a/kplayer/video/SVideoPlayer.swift +++ b/kplayer/video/SVideoPlayer.swift @@ -24,6 +24,8 @@ struct SVideoPlayer: View, EditItemDelegate { @State var savetext = "save" @State var confirmationShown = false @State var dirtyShown = false + @State var blackShown = false + @State var truncateShown = false @State var seekSmoothly = false @State var upsidedown = false @State var more = false @@ -62,8 +64,7 @@ struct SVideoPlayer: View, EditItemDelegate { do { player.removeTimeObserver(model.observer) model.observer = nil - } - catch { + } catch { print("exception") } } @@ -91,7 +92,22 @@ struct SVideoPlayer: View, EditItemDelegate { closePlayer(withSave: false) } } - + if !model.baseItem.compilation { + Button(action: { blackShown = true }) { + Text("black") + } + .foregroundColor(NetworkManager.sharedInstance.isBlack(model.baseItem) ? Color.yellow : Color.blue) + .frame(height: 30).buttonStyle(BorderlessButtonStyle()).confirmationDialog("Delete?", isPresented: $blackShown) { + Button("delete") { + black(); + closePlayer(withSave: false) + blackShown = false + } + Button("cancel", role: .cancel) { + blackShown = false + } + } + } KToggleButton(text: "\(relative())", binding: $more) Button(action: { @@ -123,29 +139,29 @@ struct SVideoPlayer: View, EditItemDelegate { } KToggleButton(text: "embd", binding: $embedded).frame(height: 30) - - Text(model.currentSnapshot.name).foregroundColor(Color.blue) - Text(""" - (\(model.codec) \(model.height), \(model.nominalFrameRate), \(model.bitRate)m)\n\(model.scale, specifier: "%.2f")x (\(model.dragOffset.width, specifier: "%.0f"),\(model.dragOffset.height, specifier: "%.0f")) - """).foregroundColor(Color.blue) - - ScrollView(.horizontal, showsIndicators: false) { - HStack { - ForEach(model.allItems) { item in - Button(action: { - gotoSnapshot(item) - }) { - AsyncImage(item: item, placeholder: { Text("Loading ...") }, - image: { Image(uiImage: $0).resizable() }).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)) + Group { + Text(model.currentSnapshot.name).foregroundColor(Color.blue) + Text(""" + (\(model.codec) \(model.height), \(model.nominalFrameRate), \(model.bitRate)m)\n\(model.scale, specifier: "%.2f")x (\(model.dragOffset.width, specifier: "%.0f"),\(model.dragOffset.height, specifier: "%.0f")) + """).foregroundColor(Color.blue) + + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(model.allItems) { item in + Button(action: { + gotoSnapshot(item) + }) { + AsyncImage(item: item, placeholder: { Text("Loading ...") }, + image: { Image(uiImage: $0).resizable() }).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)) + } } } } - } - Spacer() + Spacer() + - Group { Button(action: { model.edit.toggle() }, label: { Text("edit") }) @@ -194,7 +210,7 @@ struct SVideoPlayer: View, EditItemDelegate { 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) + model.dragOffset = CGSize(width: f * dragged.width + lastDragOffset.width, height: f * dragged.height + lastDragOffset.height) } } .onEnded { gesture in @@ -220,12 +236,10 @@ struct SVideoPlayer: View, EditItemDelegate { } else { if embedded && !more { v.overlay(SEmbeddedVideo(embedded: $embedded, down: $embDown), alignment: embDown ? .bottomLeading : .topLeading) - } else - if small && !more { + } 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 { + } else if more { v.overlay(VStack { Button(action: { @@ -287,6 +301,41 @@ struct SVideoPlayer: View, EditItemDelegate { .frame(height: 30) .foregroundColor(model.paused ? Color.yellow : Color.blue).buttonStyle(BorderlessButtonStyle()) + if !model.baseItem.compilation { + Button(action: { blackShown = true }) { + Text("black") + } + .foregroundColor(NetworkManager.sharedInstance.isBlack(model.baseItem) ? Color.yellow : Color.blue) + .frame(height: 30).buttonStyle(BorderlessButtonStyle()).confirmationDialog("Delete?", isPresented: $blackShown) { + Button("delete") { + black(); + closePlayer(withSave: false) + blackShown = false + } + Button("cancel", role: .cancel) { + blackShown = false + } + } + Button(action: { truncateShown = true }) { + Text("trunc") + } + .foregroundColor(Color.blue) + .frame(height: 30).buttonStyle(BorderlessButtonStyle()).confirmationDialog("Truncate?", isPresented: $truncateShown) { + Button("truncate") { + truncate(); + closePlayer(withSave: false) + truncateShown = false + } + Button("cancel", role: .cancel) { + truncateShown = false + } + } + Button(action: { cut() }) { + Text("cut") + } + .frame(height: 30).buttonStyle(BorderlessButtonStyle()) + } + Button(action: {}) { Text("start") } @@ -318,35 +367,35 @@ struct SVideoPlayer: View, EditItemDelegate { }) } .frame(width: 50, alignment: .top).offset(x: 0, y: 0), alignment: .topLeading) - } - else if frames && model.zoomed { + } 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() + 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 } - ) - } - }.frame(width: 70, alignment: .top).offset(x: 0, y: 0), alignment: .topTrailing) - } - else { + } + updateOptions() + } + ) + } + } + .frame(width: 70, alignment: .top).offset(x: 0, y: 0), alignment: .topTrailing) + } else { v } } @@ -402,7 +451,8 @@ struct SVideoPlayer: View, EditItemDelegate { } .onReceive(bitrateChanged) { notification in guard let playerItem = notification.object as? AVPlayerItem, - let lastEvent = playerItem.accessLog()?.events.last else { + let lastEvent = playerItem.accessLog()?.events.last + else { return } @@ -429,7 +479,7 @@ struct SVideoPlayer: View, EditItemDelegate { let relativeTime = time.seconds - model.currentSnapshot.time; for f in model.frames { - // print("\(f.time) \(relativeTime)") + // print("\(f.time) \(relativeTime)") if f.time > relativeTime && f.time < relativeTime + 1 { model.scale = CGFloat(f.scale) model.dragOffset.width = CGFloat(f.x) @@ -529,8 +579,7 @@ struct SVideoPlayer: View, EditItemDelegate { } else { if model.scale != 1.0 || small { return true - } - else { + } else { let delta = (dragged.width + dragged.height) / 400.0 seekTimeSmoothly(smoothTime + delta) @@ -589,8 +638,7 @@ struct SVideoPlayer: View, EditItemDelegate { private func relative() -> String { if let time = getCurrentTime() { return Utility.formatSecondsToHMS(time - model.currentSnapshot.time) - } - else { + } else { return "more" } } @@ -797,6 +845,18 @@ struct SVideoPlayer: View, EditItemDelegate { seekTimeSmoothly(v) } + func black() { + NetworkManager.sharedInstance.blackItem(model.baseItem) + } + + func cut() { + NetworkManager.sharedInstance.cutItem(model.currentSnapshot) + } + + func truncate() { + NetworkManager.sharedInstance.truncateItem(model.currentSnapshot) + } + func save(currentSnapshot c: MediaItem, name: String) { do { try FileHelper.createDir(name: name) @@ -812,7 +872,7 @@ struct SVideoPlayer: View, EditItemDelegate { return } player.pause() - VideoHelper.export(item: player.currentItem!, clipStart: c.time, clipDuration: dur, file: file, snapshot:c, zoomed: model.zoomed, + 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 a5dc2af..0a019bb 100644 --- a/kplayer/video/VideoPlayerView.swift +++ b/kplayer/video/VideoPlayerView.swift @@ -119,6 +119,14 @@ struct VideoPlayerControlsView : View { Image(systemName: model.paused ? "play" : "pause") .padding(.trailing, 30) } + Button(action: moveBack) { + Image(systemName: "backward") + .padding(.trailing, 20) + } + Button(action: moveForward) { + Image(systemName: "forward") + .padding(.trailing, 20) + } // Current video time let postime = model.videoPos * model.videoDuration let postext = Utility.formatSecondsToHMS(postime) @@ -135,6 +143,20 @@ struct VideoPlayerControlsView : View { .padding(.trailing, 10) } + private func moveBack() { + let time = player.currentTime().seconds - 5.0; + + player.seek(to: CMTime(seconds: time, preferredTimescale: 10000)) + pausePlayer(true) + } + + private func moveForward() { + let time = player.currentTime().seconds + 5.0; + + player.seek(to: CMTime(seconds: time, preferredTimescale: 10000)) + pausePlayer(true) + } + private func togglePlayPause() { pausePlayer(!model.paused) }