Browse Source

Embedded

master
marcoschmickler 3 years ago
parent
commit
af7f2143c7
  1. 12
      kplayer.xcodeproj/project.pbxproj
  2. 77
      kplayer/Images.xcassets/AppIcon.appiconset/Contents.json
  3. 33
      kplayer/core/DatabaseManager.swift
  4. 13
      kplayer/core/KFrame.swift
  5. 9
      kplayer/core/KSettings.swift
  6. 1
      kplayer/core/KSettingsModel.swift
  7. 2
      kplayer/core/KSnapshot+CoreDataProperties.swift
  8. 54
      kplayer/core/LocalManager.swift
  9. 4
      kplayer/core/MediaItem.swift
  10. 2
      kplayer/core/MediaModel.swift
  11. 9
      kplayer/detail/DetailViewController+Show.swift
  12. 3
      kplayer/detail/DetailViewController.swift
  13. 2
      kplayer/kplayer.xcdatamodeld/.xccurrentversion
  14. 38
      kplayer/kplayer.xcdatamodeld/kplayer 3.xcdatamodel/contents
  15. 4
      kplayer/kplayer.xcdatamodeld/kplayer.xcdatamodel/contents
  16. 3
      kplayer/master/KSettingsView.swift
  17. 33
      kplayer/master/MasterViewController.swift
  18. 7
      kplayer/photo/SPhotoAlbumView.swift
  19. 37
      kplayer/photo/SPhotoModel.swift
  20. 19
      kplayer/photo/SPhotoView.swift
  21. 28
      kplayer/util/VideoHelper.swift
  22. 150
      kplayer/video/SEmbeddedVideo.swift
  23. 2
      kplayer/video/SVideoModel.swift
  24. 249
      kplayer/video/SVideoPlayer.swift
  25. 20
      kplayer/video/VideoPlayerView.swift

12
kplayer.xcodeproj/project.pbxproj

@ -63,9 +63,11 @@
1C736D24B49451141CD4B64D /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7369F53095B7A4D65679C2 /* DetailViewController.swift */; }; 1C736D24B49451141CD4B64D /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7369F53095B7A4D65679C2 /* DetailViewController.swift */; };
1C736D5A7C7CB9B14AF0ECA6 /* DetailViewController+Show.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736B17A8E9FB352B90A903 /* DetailViewController+Show.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 */; }; 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 */; }; 1C736DFA8544C773E3C22F10 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73675F8DDFA82DEADB542E /* VideoPlayerView.swift */; };
1C736DFD076D9CC30F0B9D58 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736677D4EF2437358B2387 /* Utility.swift */; }; 1C736DFD076D9CC30F0B9D58 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736677D4EF2437358B2387 /* Utility.swift */; };
1C736E21B246C0BE7E123FD3 /* MediaModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736B41C6AC33F3FA592C63 /* MediaModel.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 */; }; 1C736EC45EE7DA5F7FCE63DA /* LocalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73659CC9B523B957E58DC6 /* LocalManager.swift */; };
1C736F6A223D4ADB2E1BA733 /* ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736069C214E9522BB1BD97 /* ItemCell.swift */; }; 1C736F6A223D4ADB2E1BA733 /* ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736069C214E9522BB1BD97 /* ItemCell.swift */; };
1C736F7D29B76C7037CEF778 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73647019E6C2E822127BA3 /* DatabaseManager.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 = "<group>"; }; 1C736260E748CF136FF37EA7 /* UploadOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadOperation.swift; sourceTree = "<group>"; };
1C7362DE1D6BE634D7C2ACBF /* KPersistentContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KPersistentContainer.swift; sourceTree = "<group>"; }; 1C7362DE1D6BE634D7C2ACBF /* KPersistentContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KPersistentContainer.swift; sourceTree = "<group>"; };
1C73631C96E6C860833052CA /* ItemType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemType.swift; sourceTree = "<group>"; }; 1C73631C96E6C860833052CA /* ItemType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemType.swift; sourceTree = "<group>"; };
1C7363743CD8120637AC1EDE /* KFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KFrame.swift; sourceTree = "<group>"; };
1C73645DBD6499A726D34973 /* links.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = links.html; sourceTree = "<group>"; }; 1C73645DBD6499A726D34973 /* links.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = links.html; sourceTree = "<group>"; };
1C73647019E6C2E822127BA3 /* DatabaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; }; 1C73647019E6C2E822127BA3 /* DatabaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; };
1C7364709899FF62774B0199 /* VideoHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoHelper.swift; sourceTree = "<group>"; }; 1C7364709899FF62774B0199 /* VideoHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoHelper.swift; sourceTree = "<group>"; };
@ -133,6 +136,7 @@
1C73673DC671535E3A049F54 /* PhotoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoController.swift; sourceTree = "<group>"; }; 1C73673DC671535E3A049F54 /* PhotoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoController.swift; sourceTree = "<group>"; };
1C73675F8DDFA82DEADB542E /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; }; 1C73675F8DDFA82DEADB542E /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; };
1C736777456388CA571DA17B /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; }; 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 = "<group>"; };
1C7367ECBD369A2A0C94C499 /* FileHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHelper.swift; sourceTree = "<group>"; }; 1C7367ECBD369A2A0C94C499 /* FileHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHelper.swift; sourceTree = "<group>"; };
1C73685B4BBFDAFBF08C032C /* readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = readme.md; sourceTree = "<group>"; }; 1C73685B4BBFDAFBF08C032C /* readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = readme.md; sourceTree = "<group>"; };
1C73688DAB88F9360FB62A49 /* MediaItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = "<group>"; }; 1C73688DAB88F9360FB62A49 /* MediaItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = "<group>"; };
@ -179,6 +183,7 @@
C98AF5E91B124D6A00D196CC /* kplayerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = kplayerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; C98AF5EE1B124D6A00D196CC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C98AF5EF1B124D6A00D196CC /* kplayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = kplayerTests.swift; sourceTree = "<group>"; }; C98AF5EF1B124D6A00D196CC /* kplayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = kplayerTests.swift; sourceTree = "<group>"; };
C9B052432905C56F002F65C0 /* kplayer 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "kplayer 3.xcdatamodel"; sourceTree = "<group>"; };
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 = "<group>"; }; 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 = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -297,6 +302,7 @@
1C736C94157754DE1C808173 /* KSettingsModel.swift */, 1C736C94157754DE1C808173 /* KSettingsModel.swift */,
1C73659CC9B523B957E58DC6 /* LocalManager.swift */, 1C73659CC9B523B957E58DC6 /* LocalManager.swift */,
1C73647019E6C2E822127BA3 /* DatabaseManager.swift */, 1C73647019E6C2E822127BA3 /* DatabaseManager.swift */,
1C7363743CD8120637AC1EDE /* KFrame.swift */,
); );
path = core; path = core;
sourceTree = "<group>"; sourceTree = "<group>";
@ -309,6 +315,7 @@
1C73675F8DDFA82DEADB542E /* VideoPlayerView.swift */, 1C73675F8DDFA82DEADB542E /* VideoPlayerView.swift */,
1C73661C3F9F4E53645551AD /* KToggleButton.swift */, 1C73661C3F9F4E53645551AD /* KToggleButton.swift */,
1C7360F9835128FC0A198ED0 /* SVideoLoopPlayer.swift */, 1C7360F9835128FC0A198ED0 /* SVideoLoopPlayer.swift */,
1C7367B39F09CCD1760A345A /* SEmbeddedVideo.swift */,
); );
path = video; path = video;
sourceTree = "<group>"; sourceTree = "<group>";
@ -642,6 +649,8 @@
1C7368DBC47F0CC152504141 /* SPhotoScrubber.swift in Sources */, 1C7368DBC47F0CC152504141 /* SPhotoScrubber.swift in Sources */,
1C736C76C1D80B649474F0A5 /* SPhotoAlbumView.swift in Sources */, 1C736C76C1D80B649474F0A5 /* SPhotoAlbumView.swift in Sources */,
1C736D0A14C365F3E874420C /* SelfSizingHostingController.swift in Sources */, 1C736D0A14C365F3E874420C /* SelfSizingHostingController.swift in Sources */,
1C736DAE7F2A530AACDA0D18 /* KFrame.swift in Sources */,
1C736EB38B780CA47B50772F /* SEmbeddedVideo.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -904,9 +913,10 @@
C98AF5D61B124D6A00D196CC /* kplayer.xcdatamodeld */ = { C98AF5D61B124D6A00D196CC /* kplayer.xcdatamodeld */ = {
isa = XCVersionGroup; isa = XCVersionGroup;
children = ( children = (
C9B052432905C56F002F65C0 /* kplayer 3.xcdatamodel */,
C98AF5D71B124D6A00D196CC /* kplayer.xcdatamodel */, C98AF5D71B124D6A00D196CC /* kplayer.xcdatamodel */,
); );
currentVersion = C98AF5D71B124D6A00D196CC /* kplayer.xcdatamodel */;
currentVersion = C9B052432905C56F002F65C0 /* kplayer 3.xcdatamodel */;
path = kplayer.xcdatamodeld; path = kplayer.xcdatamodeld;
sourceTree = "<group>"; sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel; versionGroupType = wrapper.xcdatamodel;

77
kplayer/Images.xcassets/AppIcon.appiconset/Contents.json

@ -2,92 +2,97 @@
"images" : [ "images" : [
{ {
"idiom" : "iphone", "idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
"scale" : "2x",
"size" : "20x20"
}, },
{ {
"idiom" : "iphone", "idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
"scale" : "3x",
"size" : "20x20"
}, },
{ {
"idiom" : "iphone", "idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
"scale" : "2x",
"size" : "29x29"
}, },
{ {
"idiom" : "iphone", "idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
"scale" : "3x",
"size" : "29x29"
}, },
{ {
"idiom" : "iphone", "idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
"scale" : "2x",
"size" : "40x40"
}, },
{ {
"idiom" : "iphone", "idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
"scale" : "3x",
"size" : "40x40"
}, },
{ {
"idiom" : "iphone", "idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
"scale" : "2x",
"size" : "60x60"
}, },
{ {
"idiom" : "iphone", "idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
"scale" : "3x",
"size" : "60x60"
}, },
{ {
"idiom" : "ipad", "idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
"scale" : "1x",
"size" : "20x20"
}, },
{ {
"idiom" : "ipad", "idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
"scale" : "2x",
"size" : "20x20"
}, },
{ {
"idiom" : "ipad", "idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
"scale" : "1x",
"size" : "29x29"
}, },
{ {
"idiom" : "ipad", "idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
"scale" : "2x",
"size" : "29x29"
}, },
{ {
"idiom" : "ipad", "idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
"scale" : "1x",
"size" : "40x40"
}, },
{ {
"idiom" : "ipad", "idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
"scale" : "2x",
"size" : "40x40"
}, },
{ {
"idiom" : "ipad", "idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
"scale" : "1x",
"size" : "76x76"
}, },
{ {
"idiom" : "ipad", "idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
"scale" : "2x",
"size" : "76x76"
}, },
{ {
"idiom" : "ipad", "idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
} }
], ],
"info" : { "info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
} }
} }

33
kplayer/core/DatabaseManager.swift

@ -47,6 +47,7 @@ class DatabaseManager {
c.offset.y = CGFloat(s.offy) c.offset.y = CGFloat(s.offy)
c.scale = s.scale c.scale = s.scale
c.rating = Int(s.rating) c.rating = Int(s.rating)
c.options = s.options ?? ""
for t in s.tags as! Set<KTag> { for t in s.tags as! Set<KTag> {
c.tags.append(t.name!) c.tags.append(t.name!)
@ -194,6 +195,7 @@ class DatabaseManager {
snap.rating = Int16(c.rating) snap.rating = Int16(c.rating)
snap.thumb = c.thumbUrl snap.thumb = c.thumbUrl
snap.options = c.options
var ct = [String](c.tags) var ct = [String](c.tags)
@ -336,6 +338,7 @@ class DatabaseManager {
sitem.loaded = true sitem.loaded = true
sitem.compilation = true sitem.compilation = true
sitem.objectID = i.objectID sitem.objectID = i.objectID
sitem.local = i.local
let c = MediaItem(name: i.name!, path: i.path!, root: i.root!, type: ctype) let c = MediaItem(name: i.name!, path: i.path!, root: i.root!, type: ctype)
c.time = s.time c.time = s.time
@ -349,6 +352,10 @@ class DatabaseManager {
c.rating = Int(s.rating) c.rating = Int(s.rating)
c.objectID = s.objectID c.objectID = s.objectID
if s.options != nil {
c.options = s.options!
}
if s.thumb != nil { if s.thumb != nil {
c.thumbUrl = s.thumb c.thumbUrl = s.thumb
} }
@ -370,6 +377,32 @@ class DatabaseManager {
return sitem return sitem
} }
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<KSnapshot> {
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) { func addTag(_ name: String, snapshot: KSnapshot) {
let kFetch = KTag.fetchRequest() let kFetch = KTag.fetchRequest()
kFetch.predicate = NSPredicate(format: "name == %@", name) kFetch.predicate = NSPredicate(format: "name == %@", name)

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

9
kplayer/core/KSettings.swift

@ -7,7 +7,7 @@ import Foundation
class KSettings: ObservableObject { class KSettings: ObservableObject {
@Published @Published
var scale = Float(0.5)
var scale = Float(1.5)
@Published @Published
var autoloop = false var autoloop = false
@ -18,17 +18,24 @@ class KSettings: ObservableObject {
@Published @Published
var zoomed = false var zoomed = false
@Published
var jump = false
@Published @Published
var edit = false var edit = false
@Published @Published
var automaticallyWaitsToMinimizeStalling = true var automaticallyWaitsToMinimizeStalling = true
@Published
var embeddedVideoUrl : URL?
convenience init(model: KSettingsModel) { convenience init(model: KSettingsModel) {
self.init() self.init()
scale = model.scale scale = model.scale
autoloop = model.autoloop autoloop = model.autoloop
zoomed = model.zoomed zoomed = model.zoomed
jump = model.jump
slow = model.slow slow = model.slow
} }

1
kplayer/core/KSettingsModel.swift

@ -9,5 +9,6 @@ struct KSettingsModel: Codable {
var scale = Float(0.5) var scale = Float(0.5)
var autoloop = false var autoloop = false
var zoomed = false var zoomed = false
var jump = false
var slow = false var slow = false
} }

2
kplayer/core/KSnapshot+CoreDataProperties.swift

@ -31,7 +31,7 @@ extension KSnapshot {
@NSManaged public var index: Int32 @NSManaged public var index: Int32
@NSManaged public var item: KItem? @NSManaged public var item: KItem?
@NSManaged public var tags: NSSet? @NSManaged public var tags: NSSet?
@NSManaged public var options: String?
} }
// MARK: Generated accessors for tags // MARK: Generated accessors for tags

54
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 { func loadFavDirs(_ url: URL, completionHandler: @escaping Weiter) -> Void {
var res = [MediaItem]() var res = [MediaItem]()
@ -196,6 +248,8 @@ class LocalManager {
i.externalURL = fileURL.absoluteString i.externalURL = fileURL.absoluteString
i.local = true i.local = true
} }
let pathComponents = fileURL.pathComponents
mediaItem.path = pathComponents[pathComponents.count-2]
DatabaseManager.sharedInstance.saveItemMetaData(mediaItem) DatabaseManager.sharedInstance.saveItemMetaData(mediaItem)
res.append(mediaItem) res.append(mediaItem)
} catch { } catch {

4
kplayer/core/MediaItem.swift

@ -94,6 +94,8 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable {
var cookies = "" var cookies = ""
var options = ""
convenience init(model: MediaModel) { convenience init(model: MediaModel) {
self.init(name: model.name, path: model.path, root: model.root, type: model.type) 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.local = true
self.indexId = model.indexId self.indexId = model.indexId
self.tags = model.tags self.tags = model.tags
self.options = model.options
for m in model.children { for m in model.children {
let item = MediaItem(model: m) let item = MediaItem(model: m)
@ -141,6 +144,7 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable {
model.favorite = favorite model.favorite = favorite
model.indexId = indexId model.indexId = indexId
model.tags = tags model.tags = tags
model.options = options
return model return model
} }

2
kplayer/core/MediaModel.swift

@ -26,4 +26,6 @@ public struct MediaModel : Codable {
var children: [MediaModel] var children: [MediaModel]
let type: ItemType let type: ItemType
var tags = [String]() var tags = [String]()
var options = ""
} }

9
kplayer/detail/DetailViewController+Show.swift

@ -9,11 +9,11 @@ import SwiftUI
extension DetailViewController { extension DetailViewController {
func showDetails(sectionItem: MediaItem, selectedItem: MediaItem) {
func showDetails(sectionItem: MediaItem, selectedItem: MediaItem, section: Int) {
if sectionItem.isVideo() { if sectionItem.isVideo() {
showVideo(selectedItem: selectedItem) showVideo(selectedItem: selectedItem)
} else if sectionItem.isPic() { } else if sectionItem.isPic() {
showPhotos(sectionItem.children, selectedItem: selectedItem)
showPhotos(sectionItem.children, selectedItem: selectedItem, section: section)
} else if sectionItem.isWeb() { } else if sectionItem.isWeb() {
showWeb(selectedItem: selectedItem) showWeb(selectedItem: selectedItem)
} else if sectionItem.type == ItemType.DOWNLOAD { } 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) let base = MediaItem(name: "", path: "", root: "", type: ItemType.PICFOLDER)
base.children = im base.children = im
if im.count < 1 { if im.count < 1 {
@ -29,6 +29,8 @@ extension DetailViewController {
} }
let model = SPhotoModel(allItems: base.clone().children) let model = SPhotoModel(allItems: base.clone().children)
model.selectItem(selectedItem) model.selectItem(selectedItem)
model.folderItems = detailItem!.children
model.folderIndex = section
let view = SPhotoAlbumView(completionHandler: { saved in let view = SPhotoAlbumView(completionHandler: { saved in
self.collectionView.reloadData() self.collectionView.reloadData()
@ -128,6 +130,7 @@ extension DetailViewController {
model.edit = delegate!.settings().edit model.edit = delegate!.settings().edit
model.slow = delegate!.settings().slow model.slow = delegate!.settings().slow
model.zoomed = delegate!.settings().zoomed model.zoomed = delegate!.settings().zoomed
model.jump = delegate!.settings().jump
let player = SVideoPlayer(completionHandler: { saved in let player = SVideoPlayer(completionHandler: { saved in
if saved { if saved {

3
kplayer/detail/DetailViewController.swift

@ -363,9 +363,8 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout
print(selectedItem.name) 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)
} }
} }

2
kplayer/kplayer.xcdatamodeld/.xccurrentversion

@ -3,6 +3,6 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>_XCCurrentVersionName</key> <key>_XCCurrentVersionName</key>
<string>kplayer.xcdatamodel</string>
<string>kplayer 3.xcdatamodel</string>
</dict> </dict>
</plist> </plist>

38
kplayer/kplayer.xcdatamodeld/kplayer 3.xcdatamodel/contents

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="20G817" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="KItem" representedClassName=".KItem" syncable="YES">
<attribute name="favorite" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="local" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="path" optional="YES" attributeType="String"/>
<attribute name="root" optional="YES" attributeType="String"/>
<attribute name="type" optional="YES" attributeType="String"/>
<relationship name="snapshots" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="KSnapshot" inverseName="item" inverseEntity="KSnapshot"/>
</entity>
<entity name="KSnapshot" representedClassName=".KSnapshot" syncable="YES">
<attribute name="index" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="length" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="loop" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="offx" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="offy" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="options" optional="YES" attributeType="String"/>
<attribute name="rating" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="scale" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="sizex" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sizey" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="thumb" optional="YES" attributeType="String"/>
<attribute name="time" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="timeStamp" optional="YES" attributeType="Date"/>
<relationship name="item" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="KItem" inverseName="snapshots" inverseEntity="KItem"/>
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="KTag" inverseName="tagged" inverseEntity="KTag"/>
</entity>
<entity name="KTag" representedClassName=".KTag" syncable="YES">
<attribute name="name" optional="YES" attributeType="String"/>
<relationship name="tagged" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="KSnapshot" inverseName="tags" inverseEntity="KSnapshot"/>
</entity>
<elements>
<element name="KItem" positionX="-59.99090576171875" positionY="213.7807769775391" width="128" height="148"/>
<element name="KSnapshot" positionX="266.1280517578125" positionY="32.68440246582031" width="128" height="254"/>
<element name="KTag" positionX="592.757568359375" positionY="115.8440093994141" width="128" height="73"/>
</elements>
</model>

4
kplayer/kplayer.xcdatamodeld/kplayer.xcdatamodel/contents

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="20G165" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="20G817" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="KItem" representedClassName=".KItem" syncable="YES"> <entity name="KItem" representedClassName=".KItem" syncable="YES">
<attribute name="favorite" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="favorite" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="local" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="local" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
@ -31,7 +31,7 @@
</entity> </entity>
<elements> <elements>
<element name="KItem" positionX="-59.99090576171875" positionY="213.7807769775391" width="128" height="148"/> <element name="KItem" positionX="-59.99090576171875" positionY="213.7807769775391" width="128" height="148"/>
<element name="KSnapshot" positionX="266.1280517578125" positionY="32.68440246582031" width="128" height="253"/>
<element name="KSnapshot" positionX="266.1280517578125" positionY="32.68440246582031" width="128" height="239"/>
<element name="KTag" positionX="592.757568359375" positionY="115.8440093994141" width="128" height="73"/> <element name="KTag" positionX="592.757568359375" positionY="115.8440093994141" width="128" height="73"/>
</elements> </elements>
</model> </model>

3
kplayer/master/KSettingsView.swift

@ -24,6 +24,9 @@ struct KSettingsView: View {
Toggle(isOn: $kSettings.zoomed, label: { Toggle(isOn: $kSettings.zoomed, label: {
Text("Zoomed") Text("Zoomed")
}) })
Toggle(isOn: $kSettings.jump, label: {
Text("Jump")
})
Toggle(isOn: $kSettings.edit, label: { Toggle(isOn: $kSettings.edit, label: {
Text("Edit") Text("Edit")
}) })

33
kplayer/master/MasterViewController.swift

@ -169,8 +169,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
m.local = true m.local = true
item.children.append(m) item.children.append(m)
m.parent = item m.parent = item
}
else {
} else {
do { do {
try FileHelper.createDir(name: answer) try FileHelper.createDir(name: answer)
item.name = answer item.name = answer
@ -193,6 +192,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
} }
// MARK: - Segues // MARK: - Segues
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" { if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow { if let indexPath = self.tableView.indexPathForSelectedRow {
@ -227,6 +227,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
} }
// Show Cell // Show Cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let item = model.items[indexPath.row] let item = model.items[indexPath.row]
@ -253,6 +254,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
} }
// Document Picker // Document Picker
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
print("hello") print("hello")
@ -286,6 +288,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
} }
// Drag and drop // Drag and drop
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
if tableView.indexPathForSelectedRow == destinationIndexPath { if tableView.indexPathForSelectedRow == destinationIndexPath {
@ -305,8 +308,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
return UITableViewDropProposal(operation: .forbidden) return UITableViewDropProposal(operation: .forbidden)
} }
} }
}
else {
} else {
return UITableViewDropProposal(operation: .forbidden) return UITableViewDropProposal(operation: .forbidden)
} }
@ -329,7 +331,9 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
// attempt to load strings from the drop coordinator // attempt to load strings from the drop coordinator
let progress = coordinator.session.loadObjects(ofClass: NSString.self) { items in let progress = coordinator.session.loadObjects(ofClass: NSString.self) { items in
// convert the item provider array to a string array or bail out // 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 let r = destinationIndexPath.row
if r < self.model.items.count { 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 items = try! JSONDecoder().decode(MediaModel.self, from: string.data(using: .utf8)!)
let m = MediaItem(model: items) let m = MediaItem(model: items)
m.local = true m.local = true
m.externalURL = nil
let at = m.playerURL! let at = m.playerURL!
if destinationItem.root == "/tags" { if destinationItem.root == "/tags" {
@ -350,17 +355,31 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
} }
m.path = destinationItem.path m.path = destinationItem.path
m.externalURL = nil
var to = m.playerURL! var to = m.playerURL!
print(destinationItem.name) print(destinationItem.name)
do {
do { do {
try FileManager.default.moveItem(at: at, to: to) try FileManager.default.moveItem(at: at, to: to)
try FileHelper.setExcludeFromiCloudBackup(&to, isExcluded: true) try FileHelper.setExcludeFromiCloudBackup(&to, isExcluded: true)
} catch {
print(error)
}
do {
try FileManager.default.moveItem(at: at.appendingPathExtension("json"), to: to.appendingPathExtension("json")) 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 { do {
try FileManager.default.moveItem(at: at.appendingPathExtension("jpg"), to: to.appendingPathExtension("jpg")) try FileManager.default.moveItem(at: at.appendingPathExtension("jpg"), to: to.appendingPathExtension("jpg"))
} catch { } catch {
print(error)
} }
let weiter: Weiter = { let weiter: Weiter = {
@ -388,6 +407,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
} }
// Authentication // Authentication
func doAuthenticate() { func doAuthenticate() {
let authenticationContext = LAContext() let authenticationContext = LAContext()
var error: NSError? var error: NSError?
@ -404,5 +424,6 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
LocalManager.sharedInstance.authenticated = success LocalManager.sharedInstance.authenticated = success
}) })
} }
} }

7
kplayer/photo/SPhotoAlbumView.swift

@ -13,6 +13,8 @@ struct SPhotoAlbumView: View {
@State var more = false @State var more = false
@State var edit = false @State var edit = false
@State var embedded = false
@State var embDown = false
init(completionHandler: ((Bool) -> ())?, model: SPhotoModel) { init(completionHandler: ((Bool) -> ())?, model: SPhotoModel) {
self.completionHandler = completionHandler self.completionHandler = completionHandler
@ -38,6 +40,7 @@ struct SPhotoAlbumView: View {
Text("\(model.index)").frame(width: 70) Text("\(model.index)").frame(width: 70)
})//.foregroundColor(update ? Color.yellow : Color.blue) })//.foregroundColor(update ? Color.yellow : Color.blue)
.buttonStyle(BorderlessButtonStyle()) .buttonStyle(BorderlessButtonStyle())
KToggleButton(text: "embd", binding: $embedded)
Spacer() Spacer()
SPhotoScrubber(model: model) SPhotoScrubber(model: model)
} }
@ -45,7 +48,9 @@ struct SPhotoAlbumView: View {
.frame(height: 50) .frame(height: 50)
SPhotoView(model: model) 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 { v.overlay(VStack {
KToggleButton(text: "spring", binding: $model.spring).frame(height: 30) KToggleButton(text: "spring", binding: $model.spring).frame(height: 30)
Button(action: { Button(action: {

37
kplayer/photo/SPhotoModel.swift

@ -7,6 +7,9 @@ import Foundation
import SwiftUI import SwiftUI
class SPhotoModel : ObservableObject { class SPhotoModel : ObservableObject {
@Published var folderItems = [MediaItem]()
@Published var folderIndex = 0
@Published var allItems : [MediaItem] @Published var allItems : [MediaItem]
@Published var indexItems : [MediaItem] @Published var indexItems : [MediaItem]
@Published var selectedItem : MediaItem @Published var selectedItem : MediaItem
@ -21,6 +24,13 @@ class SPhotoModel : ObservableObject {
init(allItems: [MediaItem]) { init(allItems: [MediaItem]) {
self.allItems = allItems self.allItems = allItems
selectedItem = allItems[0] selectedItem = allItems[0]
indexItems = allItems
update(allItems: allItems)
}
private func update(allItems: [MediaItem]) {
self.allItems = allItems
selectedItem = allItems[0]
let max = 17 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)
})
}
} }

19
kplayer/photo/SPhotoView.swift

@ -36,6 +36,7 @@ struct SPhotoView: View {
@State var startindex = -1 @State var startindex = -1
@State var animateX = 0 @State var animateX = 0
@State var dampen = 0.05 @State var dampen = 0.05
@State var jump = LocalManager.sharedInstance.settings.jump
init(model: SPhotoModel) { init(model: SPhotoModel) {
self.model = model self.model = model
@ -45,6 +46,7 @@ struct SPhotoView: View {
GeometryReader { geo in GeometryReader { geo in
// AsyncImage(item: model.selectedItem, thumb: false, placeholder: { Text("Loading ...") }, image: { Image(uiImage: $0).resizable() }) // AsyncImage(item: model.selectedItem, thumb: false, placeholder: { Text("Loading ...") }, image: { Image(uiImage: $0).resizable() })
ZStack {
SwiftUI.AsyncImage(url: URL(string: model.allItems[model.index].imageUrlAbsolute)) { image in SwiftUI.AsyncImage(url: URL(string: model.allItems[model.index].imageUrlAbsolute)) { image in
image.resizable().scaledToFit() image.resizable().scaledToFit()
} }
@ -57,6 +59,7 @@ struct SPhotoView: View {
} }
.frame(height: geo.size.height).frame(width: geo.size.width, alignment: .leading) .frame(height: geo.size.height).frame(width: geo.size.width, alignment: .leading)
.scaleEffect(model.scale).offset(model.dragOffset).modifier(KAnimate(dragOffset: model.dragOffset, spring: model.spring)) .scaleEffect(model.scale).offset(model.dragOffset).modifier(KAnimate(dragOffset: model.dragOffset, spring: model.spring))
}
.onTapGesture(count: 2) { .onTapGesture(count: 2) {
print("3 tapped!") print("3 tapped!")
if model.scale == 1 { if model.scale == 1 {
@ -76,10 +79,20 @@ struct SPhotoView: View {
if startindex < 0 { if startindex < 0 {
startindex = model.index 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 { if model.index >= model.allItems.count {
model.index = model.allItems.count - 1 model.index = model.allItems.count - 1
} }

28
kplayer/util/VideoHelper.swift

@ -7,7 +7,7 @@ import Foundation
import AVFoundation import AVFoundation
class VideoHelper { 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 { guard item.asset.isExportable else {
completion(nil) completion(nil)
return return
@ -31,7 +31,7 @@ class VideoHelper {
} }
let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: composition) let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: composition)
var preset: String = AVAssetExportPresetPassthrough
var preset: String = zoomed ? AVAssetExportPresetHEVC1920x1080 : AVAssetExportPresetPassthrough
// if compatiblePresets.contains(AVAssetExportPreset3840x2160) { preset = AVAssetExportPreset3840x2160 } // if compatiblePresets.contains(AVAssetExportPreset3840x2160) { preset = AVAssetExportPreset3840x2160 }
guard guard
@ -41,6 +41,30 @@ class VideoHelper {
return 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.outputURL = file
exportSession.outputFileType = AVFileType.mp4 exportSession.outputFileType = AVFileType.mp4
let startTime = CMTime(seconds: 0.0, preferredTimescale: 100); let startTime = CMTime(seconds: 0.0, preferredTimescale: 100);

150
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<Bool>
private let down: Binding<Bool>
@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<Bool>, down: Binding<Bool>) {
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)
}
}

2
kplayer/video/SVideoModel.swift

@ -11,6 +11,7 @@ class SVideoModel : ObservableObject {
@Published var allItems : [MediaItem] @Published var allItems : [MediaItem]
@Published var currentSnapshot: MediaItem @Published var currentSnapshot: MediaItem
@Published var baseItem: MediaItem @Published var baseItem: MediaItem
@Published var frames = [KFrame]()
// The progress through the video, as a percentage (from 0 to 1) // The progress through the video, as a percentage (from 0 to 1)
@Published var videoPos: Double = 0 @Published var videoPos: Double = 0
@ -26,6 +27,7 @@ class SVideoModel : ObservableObject {
@Published var loop = false @Published var loop = false
@Published var slow = false @Published var slow = false
@Published var zoomed = false @Published var zoomed = false
@Published var jump = false
@Published var favorite = false @Published var favorite = false
@Published var speed: Float = 1.0 @Published var speed: Float = 1.0

249
kplayer/video/SVideoPlayer.swift

@ -27,8 +27,11 @@ struct SVideoPlayer: View, EditItemDelegate {
@State var seekSmoothly = false @State var seekSmoothly = false
@State var upsidedown = false @State var upsidedown = false
@State var more = 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 tilt = false
@State var jump = false
@State var smoothTime = -1.0 @State var smoothTime = -1.0
@State var smoothSeekTime = -1.0 @State var smoothSeekTime = -1.0
@State var timeCounter = 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: { Button(action: {
model.favorite.toggle() 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(model.currentSnapshot.name).foregroundColor(Color.blue)
Text(""" Text("""
@ -141,15 +145,26 @@ struct SVideoPlayer: View, EditItemDelegate {
Spacer() Spacer()
Group {
Button(action: { model.edit.toggle() }, label: { Button(action: { model.edit.toggle() }, label: {
Text("edit") Text("edit")
}) })
.buttonStyle(BorderlessButtonStyle()); .buttonStyle(BorderlessButtonStyle());
if !model.baseItem.compilation { if !model.baseItem.compilation {
if model.currentSnapshot != nil {
Button(action: addFrame, label: {
Text("frame")
})
.buttonStyle(BorderlessButtonStyle())
}
Button(action: doSnapshot, label: { Button(action: doSnapshot, label: {
Text("snap") Text("snap")
}) })
.buttonStyle(BorderlessButtonStyle()); .buttonStyle(BorderlessButtonStyle());
}
} }
} }
.frame(height: 50) .frame(height: 50)
@ -159,9 +174,9 @@ struct SVideoPlayer: View, EditItemDelegate {
VStack { VStack {
let v = VideoPlayerView(model: model, let v = VideoPlayerView(model: model,
player: player) player: player)
.scaleEffect(model.scale)
.scaleEffect(small ? 1 : model.scale)
.rotation3DEffect(.degrees(upsidedown ? 180 : 0), axis: (x: 1, y: 0, z: 0)) .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) { .onTapGesture(count: 2) {
print("3 tapped!") print("3 tapped!")
if model.scale == 1 { if model.scale == 1 {
@ -178,7 +193,8 @@ struct SVideoPlayer: View, EditItemDelegate {
let dragged = gesture.translation let dragged = gesture.translation
if move(dragged, start: gesture.startLocation) { 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 .onEnded { gesture in
@ -202,7 +218,14 @@ struct SVideoPlayer: View, EditItemDelegate {
v.overlay(EditItemView(item: model.currentSnapshot, len: model.videoDuration, delegate: self) v.overlay(EditItemView(item: model.currentSnapshot, len: model.videoDuration, delegate: self)
.frame(width: 420, alignment: .top).offset(x: 0, y: -50), alignment: .topTrailing) .frame(width: 420, alignment: .top).offset(x: 0, y: -50), alignment: .topTrailing)
} else { } 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 { v.overlay(VStack {
Button(action: { Button(action: {
@ -218,17 +241,15 @@ struct SVideoPlayer: View, EditItemDelegate {
.frame(height: 30) .frame(height: 30)
.foregroundColor(tilt ? Color.yellow : Color.blue).buttonStyle(BorderlessButtonStyle()) .foregroundColor(tilt ? Color.yellow : Color.blue).buttonStyle(BorderlessButtonStyle())
Group {
KToggleButton(text: "slow", binding: $model.slow).frame(height: 30) KToggleButton(text: "slow", binding: $model.slow).frame(height: 30)
KToggleButton(text: "zoom", binding: $model.zoomed).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)
// }
KToggleButton(text: "jump", binding: $model.jump).frame(height: 30)
KToggleButton(text: "small", binding: $small).frame(height: 30)
KToggleButton(text: "loop", binding: $model.loop).frame(height: 30)
KToggleButton(text: "flip", binding: $upsidedown).frame(height: 30) KToggleButton(text: "flip", binding: $upsidedown).frame(height: 30)
}
Button(action: { Button(action: {
if model.speed == 1.0 && timeSlomoCounter == 0 { 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) .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 v
} }
} }
@ -320,7 +369,50 @@ struct SVideoPlayer: View, EditItemDelegate {
} }
.onAppear() { .onAppear() {
model.observed = player model.observed = player
model.observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.02, preferredTimescale: 600), queue: nil) { time in
model.observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.02, preferredTimescale: 600), queue: nil, using: timeObserver())
player.automaticallyWaitsToMinimizeStalling = LocalManager.sharedInstance.settings.automaticallyWaitsToMinimizeStalling
let item = model.currentPlayerItem()
item.preferredForwardBufferDuration = 2.0
player.removeAllItems()
self.player.replaceCurrentItem(with: item)
model.currentURL = model.currentSnapshot.playerURL
print(model.currentURL!)
// player.removeAllItems()
// player.insert(model.currentPlayerItem(), after: nil)
// Start the player going, otherwise controls don't appear
player.play()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
gotoSnapshot(model.currentSnapshot)
}
}
.onDisappear {
// When this View isn't being shown anymore stop the player
player.pause()
cleanup()
self.player.replaceCurrentItem(with: nil)
}
.onReceive(orientationChanged) { _ in
self.orientation = UIDevice.current.orientation
}
.onReceive(bitrateChanged) { notification in
guard let playerItem = notification.object as? AVPlayerItem,
let lastEvent = playerItem.accessLog()?.events.last else {
return
}
model.bitRate = Int(lastEvent.indicatedBitrate / 1024 / 1024)
}
}
private func timeObserver() -> (CMTime) -> () {
{ time in
if timeCounter >= 1 { if timeCounter >= 1 {
timeCounter -= 1 timeCounter -= 1
} }
@ -332,6 +424,21 @@ struct SVideoPlayer: View, EditItemDelegate {
seekTime(model.currentSnapshot.time) 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 tilt {
if let data = motionManager.deviceMotion { if let data = motionManager.deviceMotion {
var rotation = atan2(data.gravity.x, var rotation = atan2(data.gravity.x,
@ -363,48 +470,12 @@ struct SVideoPlayer: View, EditItemDelegate {
rotazero = -1000 rotazero = -1000
} }
} }
player.automaticallyWaitsToMinimizeStalling = LocalManager.sharedInstance.settings.automaticallyWaitsToMinimizeStalling
let item = model.currentPlayerItem()
item.preferredForwardBufferDuration = 2.0
player.removeAllItems()
self.player.replaceCurrentItem(with: item)
model.currentURL = model.currentSnapshot.playerURL
print(model.currentURL!)
// player.removeAllItems()
// player.insert(model.currentPlayerItem(), after: nil)
// Start the player going, otherwise controls don't appear
player.play()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
gotoSnapshot(model.currentSnapshot)
}
}
.onDisappear {
// When this View isn't being shown anymore stop the player
player.pause()
cleanup()
self.player.replaceCurrentItem(with: nil)
}
.onReceive(orientationChanged) { _ in
self.orientation = UIDevice.current.orientation
}
.onReceive(bitrateChanged) { notification in
guard let playerItem = notification.object as? AVPlayerItem,
let lastEvent = playerItem.accessLog()?.events.last else {
return
}
model.bitRate = Int(lastEvent.indicatedBitrate / 1024 / 1024)
}
} }
private func closePlayer(withSave: Bool) { private func closePlayer(withSave: Bool) {
if model.currentSnapshot.local {
LocalManager.sharedInstance.settings.embeddedVideoUrl = model.currentURL
}
model.baseItem.favorite = model.favorite model.baseItem.favorite = model.favorite
player.pause() player.pause()
player.replaceCurrentItem(with: nil) player.replaceCurrentItem(with: nil)
@ -456,7 +527,7 @@ struct SVideoPlayer: View, EditItemDelegate {
return false return false
} else { } else {
if model.scale != 1.0 {
if model.scale != 1.0 || small {
return true return true
} }
else { else {
@ -471,7 +542,7 @@ struct SVideoPlayer: View, EditItemDelegate {
} }
} }
if dragged.height > 100 && jump {
if dragged.height > 100 && model.jump {
if timeCounter == 0 { if timeCounter == 0 {
if let i = model.allItems.index(where: { m in m === model.currentSnapshot }) { if let i = model.allItems.index(where: { m in m === model.currentSnapshot }) {
if i + 1 < model.allItems.count { if i + 1 < model.allItems.count {
@ -489,7 +560,7 @@ struct SVideoPlayer: View, EditItemDelegate {
if let time = getCurrentTime() { if let time = getCurrentTime() {
let dragWidth = 20.0 let dragWidth = 20.0
if !model.seeking { if !model.seeking {
if model.scale > 1.0 && start.y > 200 {
if (model.scale > 1.0 || small) && start.y > 200 {
return true return true
} }
@ -515,6 +586,15 @@ struct SVideoPlayer: View, EditItemDelegate {
return false return false
} }
private func relative() -> String {
if let time = getCurrentTime() {
return Utility.formatSecondsToHMS(time - model.currentSnapshot.time)
}
else {
return "more"
}
}
private func getCurrentTime() -> Double? { private func getCurrentTime() -> Double? {
player.currentItem?.currentTime().seconds 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(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 { if !model.paused {
player.play() player.play()
player.rate = model.speed player.rate = model.speed
@ -574,6 +651,26 @@ struct SVideoPlayer: View, EditItemDelegate {
func gotoSnapshot(_ currentSnapshot: MediaItem) { func gotoSnapshot(_ currentSnapshot: MediaItem) {
model.currentSnapshot = currentSnapshot 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 { if currentSnapshot.playerURL != model.currentURL {
model.currentURL = currentSnapshot.playerURL model.currentURL = currentSnapshot.playerURL
@ -617,6 +714,8 @@ struct SVideoPlayer: View, EditItemDelegate {
f = f * CGFloat(LocalManager.sharedInstance.settings.scale) f = f * CGFloat(LocalManager.sharedInstance.settings.scale)
} }
model.scale = f 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() { func setEnd() {
if let time = getCurrentTime() { if let time = getCurrentTime() {
let snapTime = model.currentSnapshot.time let snapTime = model.currentSnapshot.time
@ -665,7 +790,7 @@ struct SVideoPlayer: View, EditItemDelegate {
func okEdit() { func okEdit() {
DatabaseManager.sharedInstance.saveItemMetaData(model.currentSnapshot) DatabaseManager.sharedInstance.saveItemMetaData(model.currentSnapshot)
model.edit = false model.edit = false
model.dirty = false
model.dirty = true
} }
func seek(_ v: Double) { func seek(_ v: Double) {
@ -687,7 +812,7 @@ struct SVideoPlayer: View, EditItemDelegate {
return return
} }
player.pause() 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 progress: { p in
let percent = Int(p * 100) let percent = Int(p * 100)
savetext = "\(percent)" savetext = "\(percent)"

20
kplayer/video/VideoPlayerView.swift

@ -11,7 +11,7 @@ import AVFoundation
// This is the UIView that contains the AVPlayerLayer for rendering the video // This is the UIView that contains the AVPlayerLayer for rendering the video
class VideoPlayerUIView: UIView { class VideoPlayerUIView: UIView {
private let player: AVPlayer private let player: AVPlayer
private let playerLayer = AVPlayerLayer()
public let playerLayer = AVPlayerLayer()
private let videoPos: Binding<Double> private let videoPos: Binding<Double>
private let videoDuration: Binding<Double> private let videoDuration: Binding<Double>
private let seeking: Binding<Bool> private let seeking: Binding<Bool>
@ -26,7 +26,7 @@ class VideoPlayerUIView: UIView {
super.init(frame: .zero) super.init(frame: .zero)
backgroundColor = .black
// backgroundColor = .black
playerLayer.player = player playerLayer.player = player
layer.addSublayer(playerLayer) 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) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")

Loading…
Cancel
Save