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. 35
      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. 5
      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. 63
      kplayer/master/MasterViewController.swift
  18. 7
      kplayer/photo/SPhotoAlbumView.swift
  19. 37
      kplayer/photo/SPhotoModel.swift
  20. 41
      kplayer/photo/SPhotoView.swift
  21. 28
      kplayer/util/VideoHelper.swift
  22. 150
      kplayer/video/SEmbeddedVideo.swift
  23. 2
      kplayer/video/SVideoModel.swift
  24. 275
      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 */; };
1C736D5A7C7CB9B14AF0ECA6 /* DetailViewController+Show.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736B17A8E9FB352B90A903 /* DetailViewController+Show.swift */; };
1C736D89CF86841F4C98A1F7 /* KPersistentContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7362DE1D6BE634D7C2ACBF /* KPersistentContainer.swift */; };
1C736DAE7F2A530AACDA0D18 /* KFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7363743CD8120637AC1EDE /* KFrame.swift */; };
1C736DFA8544C773E3C22F10 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73675F8DDFA82DEADB542E /* VideoPlayerView.swift */; };
1C736DFD076D9CC30F0B9D58 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736677D4EF2437358B2387 /* Utility.swift */; };
1C736E21B246C0BE7E123FD3 /* MediaModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736B41C6AC33F3FA592C63 /* MediaModel.swift */; };
1C736EB38B780CA47B50772F /* SEmbeddedVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7367B39F09CCD1760A345A /* SEmbeddedVideo.swift */; };
1C736EC45EE7DA5F7FCE63DA /* LocalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73659CC9B523B957E58DC6 /* LocalManager.swift */; };
1C736F6A223D4ADB2E1BA733 /* ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736069C214E9522BB1BD97 /* ItemCell.swift */; };
1C736F7D29B76C7037CEF778 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73647019E6C2E822127BA3 /* DatabaseManager.swift */; };
@ -118,6 +120,7 @@
1C736260E748CF136FF37EA7 /* UploadOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadOperation.swift; sourceTree = "<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>"; };
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>"; };
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>"; };
@ -133,6 +136,7 @@
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>"; };
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>"; };
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>"; };
@ -179,6 +183,7 @@
C98AF5E91B124D6A00D196CC /* kplayerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = kplayerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C98AF5EE1B124D6A00D196CC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<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>"; };
/* End PBXFileReference section */
@ -297,6 +302,7 @@
1C736C94157754DE1C808173 /* KSettingsModel.swift */,
1C73659CC9B523B957E58DC6 /* LocalManager.swift */,
1C73647019E6C2E822127BA3 /* DatabaseManager.swift */,
1C7363743CD8120637AC1EDE /* KFrame.swift */,
);
path = core;
sourceTree = "<group>";
@ -309,6 +315,7 @@
1C73675F8DDFA82DEADB542E /* VideoPlayerView.swift */,
1C73661C3F9F4E53645551AD /* KToggleButton.swift */,
1C7360F9835128FC0A198ED0 /* SVideoLoopPlayer.swift */,
1C7367B39F09CCD1760A345A /* SEmbeddedVideo.swift */,
);
path = video;
sourceTree = "<group>";
@ -642,6 +649,8 @@
1C7368DBC47F0CC152504141 /* SPhotoScrubber.swift in Sources */,
1C736C76C1D80B649474F0A5 /* SPhotoAlbumView.swift in Sources */,
1C736D0A14C365F3E874420C /* SelfSizingHostingController.swift in Sources */,
1C736DAE7F2A530AACDA0D18 /* KFrame.swift in Sources */,
1C736EB38B780CA47B50772F /* SEmbeddedVideo.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -904,9 +913,10 @@
C98AF5D61B124D6A00D196CC /* kplayer.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
C9B052432905C56F002F65C0 /* kplayer 3.xcdatamodel */,
C98AF5D71B124D6A00D196CC /* kplayer.xcdatamodel */,
);
currentVersion = C98AF5D71B124D6A00D196CC /* kplayer.xcdatamodel */;
currentVersion = C9B052432905C56F002F65C0 /* kplayer 3.xcdatamodel */;
path = kplayer.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

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

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

35
kplayer/core/DatabaseManager.swift

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

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

1
kplayer/core/KSettingsModel.swift

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

2
kplayer/core/KSnapshot+CoreDataProperties.swift

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

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

4
kplayer/core/MediaItem.swift

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

2
kplayer/core/MediaModel.swift

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

9
kplayer/detail/DetailViewController+Show.swift

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

5
kplayer/detail/DetailViewController.swift

@ -363,10 +363,9 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout
print(selectedItem.name)
}
// }
self.showDetails(sectionItem: self.currentItem!, selectedItem: selectedItem)
self.showDetails(sectionItem: self.currentItem!, selectedItem: selectedItem, section: indexPath.section)
}
delegate!.loadDetails(selectedItem: currentItem!, completionHandler: weiter)
delegate!.loadDetails(selectedItem: currentItem!, completionHandler: weiter)
}
}

2
kplayer/kplayer.xcdatamodeld/.xccurrentversion

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>kplayer.xcdatamodel</string>
<string>kplayer 3.xcdatamodel</string>
</dict>
</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"?>
<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">
<attribute name="favorite" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="local" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
@ -31,7 +31,7 @@
</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="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"/>
</elements>
</model>

3
kplayer/master/KSettingsView.swift

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

63
kplayer/master/MasterViewController.swift

@ -13,7 +13,7 @@ import SwiftUI
typealias Weiter = ([MediaItem]) -> Void
protocol MasterDelegate : DetailDelegate {
protocol MasterDelegate: DetailDelegate {
func loadFolder(selectedItem: MediaItem, completionHandler: @escaping (MediaItem) -> Void)
func loadItemDetails(selectedItem: MediaItem, completionHandler: @escaping (MediaItem) -> Void)
@ -22,13 +22,13 @@ protocol MasterDelegate : DetailDelegate {
class MasterViewController: UITableViewController, UISearchResultsUpdating, UITableViewDropDelegate, UIDocumentPickerDelegate {
let searchController = UISearchController(searchResultsController: nil)
let model = ItemModel()
var delegate : MasterDelegate?
var detailDelegate : DetailDelegate?
var detailController : DetailViewController?
var delegate: MasterDelegate?
var detailDelegate: DetailDelegate?
var detailController: DetailViewController?
var authenticated = false
var currentSelection : MediaItem?
var currentSelection: MediaItem?
override func awakeFromNib() {
super.awakeFromNib()
@ -57,7 +57,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
let str = search.searchBar.text!
if str.hasSuffix("*") {
let txt = str[str.startIndex ..< str.index(str.endIndex, offsetBy: -1)]
let txt = str[str.startIndex..<str.index(str.endIndex, offsetBy: -1)]
if let p = model.items[0].parent {
let neu = MediaItem(name: String(txt), path: p.path + "*" + txt, root: p.root, type: ItemType.PICFOLDER)
model.items.append(neu)
@ -83,7 +83,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
if (selectedItem.local && selectedItem.name == "combine") {
searchView(item: selectedItem)
// gotoDetails(selectedItem)
// gotoDetails(selectedItem)
}
if (selectedItem.local && selectedItem.name == "new") {
@ -129,7 +129,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
self.dismiss(animated: true, completion: nil);
DatabaseManager.sharedInstance.searchSnapshots(item: item)
self.gotoDetails(item)
} )
})
let pc = UIHostingController(rootView: kv)
let navController = UINavigationController(rootViewController: pc) // Creating a navigation controller with pc at the root of the navigation stack.
navController.modalPresentationStyle = .automatic
@ -169,8 +169,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
m.local = true
item.children.append(m)
m.parent = item
}
else {
} else {
do {
try FileHelper.createDir(name: answer)
item.name = answer
@ -193,6 +192,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
}
// MARK: - Segues
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow {
@ -227,6 +227,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
}
// Show Cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let item = model.items[indexPath.row]
@ -253,6 +254,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
}
// Document Picker
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
print("hello")
@ -286,6 +288,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
}
// Drag and drop
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
if tableView.indexPathForSelectedRow == destinationIndexPath {
@ -305,8 +308,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
return UITableViewDropProposal(operation: .forbidden)
}
}
}
else {
} else {
return UITableViewDropProposal(operation: .forbidden)
}
@ -329,7 +331,9 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
// attempt to load strings from the drop coordinator
let progress = coordinator.session.loadObjects(ofClass: NSString.self) { items in
// convert the item provider array to a string array or bail out
guard let strings = items as? [String] else { return }
guard let strings = items as? [String] else {
return
}
let r = destinationIndexPath.row
if r < self.model.items.count {
@ -342,6 +346,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
let items = try! JSONDecoder().decode(MediaModel.self, from: string.data(using: .utf8)!)
let m = MediaItem(model: items)
m.local = true
m.externalURL = nil
let at = m.playerURL!
if destinationItem.root == "/tags" {
@ -350,20 +355,34 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
}
m.path = destinationItem.path
m.externalURL = nil
var to = m.playerURL!
print(destinationItem.name)
do {
try FileManager.default.moveItem(at: at, to: to)
try FileHelper.setExcludeFromiCloudBackup(&to, isExcluded: true)
try FileManager.default.moveItem(at: at.appendingPathExtension("json"), to: to.appendingPathExtension("json"))
do {
try FileManager.default.moveItem(at: at, to: to)
try FileHelper.setExcludeFromiCloudBackup(&to, isExcluded: true)
} catch {
print(error)
}
do {
try FileManager.default.moveItem(at: at.appendingPathExtension("json"), to: to.appendingPathExtension("json"))
} catch {
print(error)
}
do {
try FileManager.default.moveItem(at: at.appendingPathExtension("orig"), to: to.appendingPathExtension("orig"))
} catch {
print(error)
}
do {
try FileManager.default.moveItem(at: at.appendingPathExtension("jpg"), to: to.appendingPathExtension("jpg"))
} catch {
print(error)
}
let weiter:Weiter = {
let weiter: Weiter = {
(g) in
let fav = LocalManager.sharedInstance.favorites
ItemModel().sortItems(selectedItem: fav, children: g)
@ -374,7 +393,7 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
let refreshed = self.model.items[s.row]
self.detailController!.detailItem = refreshed
}
}
}
LocalManager.sharedInstance.loadFavDirs(FileHelper.getDocumentsDirectory(), completionHandler: weiter)
} catch {
@ -387,10 +406,11 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
}
// Authentication
// Authentication
func doAuthenticate() {
let authenticationContext = LAContext()
var error : NSError?
var error: NSError?
guard authenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
showAlert(title: "Error", message: "This device does not have a TouchID sensor.")
@ -404,5 +424,6 @@ class MasterViewController: UITableViewController, UISearchResultsUpdating, UITa
LocalManager.sharedInstance.authenticated = success
})
}
}

7
kplayer/photo/SPhotoAlbumView.swift

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

37
kplayer/photo/SPhotoModel.swift

@ -7,6 +7,9 @@ import Foundation
import SwiftUI
class SPhotoModel : ObservableObject {
@Published var folderItems = [MediaItem]()
@Published var folderIndex = 0
@Published var allItems : [MediaItem]
@Published var indexItems : [MediaItem]
@Published var selectedItem : MediaItem
@ -21,6 +24,13 @@ class SPhotoModel : ObservableObject {
init(allItems: [MediaItem]) {
self.allItems = allItems
selectedItem = allItems[0]
indexItems = allItems
update(allItems: allItems)
}
private func update(allItems: [MediaItem]) {
self.allItems = allItems
selectedItem = allItems[0]
let max = 17
@ -61,4 +71,31 @@ class SPhotoModel : ObservableObject {
}
}
}
func back() {
if (folderIndex > 0) {
folderIndex -= 1
} else {
folderIndex = folderItems.count - 1
}
show()
}
func next() {
if (folderIndex < folderItems.count - 1) {
folderIndex += 1
} else {
folderIndex = 0
}
show()
}
func show() {
var selectedItem = folderItems[folderIndex]
NetworkManager.sharedInstance.loadPicDetails(items: selectedItem, result: { (im: [MediaItem]) in
self.index = 0
self.update(allItems: im)
})
}
}

41
kplayer/photo/SPhotoView.swift

@ -36,6 +36,7 @@ struct SPhotoView: View {
@State var startindex = -1
@State var animateX = 0
@State var dampen = 0.05
@State var jump = LocalManager.sharedInstance.settings.jump
init(model: SPhotoModel) {
self.model = model
@ -45,18 +46,20 @@ struct SPhotoView: View {
GeometryReader { geo in
// AsyncImage(item: model.selectedItem, thumb: false, placeholder: { Text("Loading ...") }, image: { Image(uiImage: $0).resizable() })
SwiftUI.AsyncImage(url: URL(string: model.allItems[model.index].imageUrlAbsolute)) { image in
image.resizable().scaledToFit()
}
placeholder: {
if let i = model.selectedItem.thumbImage {
Image(uiImage: i).resizable().scaledToFit()
} else {
Text("...")
ZStack {
SwiftUI.AsyncImage(url: URL(string: model.allItems[model.index].imageUrlAbsolute)) { image in
image.resizable().scaledToFit()
}
}
.frame(height: geo.size.height).frame(width: geo.size.width, alignment: .leading)
.scaleEffect(model.scale).offset(model.dragOffset).modifier(KAnimate(dragOffset: model.dragOffset, spring: model.spring))
placeholder: {
if let i = model.selectedItem.thumbImage {
Image(uiImage: i).resizable().scaledToFit()
} else {
Text("...")
}
}
.frame(height: geo.size.height).frame(width: geo.size.width, alignment: .leading)
.scaleEffect(model.scale).offset(model.dragOffset).modifier(KAnimate(dragOffset: model.dragOffset, spring: model.spring))
}
.onTapGesture(count: 2) {
print("3 tapped!")
if model.scale == 1 {
@ -76,10 +79,20 @@ struct SPhotoView: View {
if startindex < 0 {
startindex = model.index
}
print("\(startindex) \(dragged.width)")
if model.scale == 1 { //&& gesture.startLocation.x < 400 {
if (jump && dragged.height > 120) {
model.next()
return
}
if (jump && dragged.height < -120) {
model.back()
return
}
model.index = startindex + Int(dragged.width / 10.0)
if gesture.startLocation.y < 200 { //&& gesture.startLocation.x < 400 {
model.index = startindex + Int(dragged.width / 30.0)
print(dragged.width)
if model.index >= model.allItems.count {
model.index = model.allItems.count - 1
}

28
kplayer/util/VideoHelper.swift

@ -7,7 +7,7 @@ import Foundation
import AVFoundation
class VideoHelper {
public static func export(item: AVPlayerItem, clipStart: Double, clipDuration: Double, file: URL, progress: @escaping (Float) -> (), completion: @escaping (URL?) -> ()) {
public static func export(item: AVPlayerItem, clipStart: Double, clipDuration: Double, file: URL, snapshot: MediaItem, zoomed: Bool, progress: @escaping (Float) -> (), completion: @escaping (URL?) -> ()) {
guard item.asset.isExportable else {
completion(nil)
return
@ -31,7 +31,7 @@ class VideoHelper {
}
let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: composition)
var preset: String = AVAssetExportPresetPassthrough
var preset: String = zoomed ? AVAssetExportPresetHEVC1920x1080 : AVAssetExportPresetPassthrough
// if compatiblePresets.contains(AVAssetExportPreset3840x2160) { preset = AVAssetExportPreset3840x2160 }
guard
@ -41,6 +41,30 @@ class VideoHelper {
return
}
if zoomed {
let vc = AVMutableVideoComposition()
vc.renderSize = CGSize(width: 1920, height: 1080)
vc.renderScale = 1.0
vc.frameDuration = CMTimeMake(value: 1, timescale: 29)
let instruction = AVMutableVideoCompositionInstruction()
instruction.timeRange = CMTimeRangeMake(start: CMTime.zero, duration: item.duration)
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: sourceVideoTrack)
let h = 0 //sourceVideoTrack.naturalSize.height / 2;
let w = 0 // sourceVideoTrack.naturalSize.height / 2;
let y = snapshot.offset.y - 70
let f = -1.36 * 2.0
let rotation: CGAffineTransform = CGAffineTransform(scaleX: 1.0, y: 1.0).translatedBy(x: f * snapshot.offset.x , y: f * y )
//CGAffineTransformMakeScale(-1, 1)
layerInstruction.setTransform(rotation, at: CMTime.zero)
instruction.layerInstructions.append(layerInstruction)
vc.instructions = [instruction]
exportSession.videoComposition = vc
}
exportSession.outputURL = file
exportSession.outputFileType = AVFileType.mp4
let startTime = CMTime(seconds: 0.0, preferredTimescale: 100);

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

275
kplayer/video/SVideoPlayer.swift

@ -27,8 +27,11 @@ struct SVideoPlayer: View, EditItemDelegate {
@State var seekSmoothly = false
@State var upsidedown = false
@State var more = false
@State var frames = false
@State var small = false
@State var embedded = false
@State var embDown = false
@State var tilt = false
@State var jump = false
@State var smoothTime = -1.0
@State var smoothSeekTime = -1.0
@State var timeCounter = 0
@ -89,7 +92,7 @@ struct SVideoPlayer: View, EditItemDelegate {
}
}
KToggleButton(text: "more", binding: $more)
KToggleButton(text: "\(relative())", binding: $more)
Button(action: {
model.favorite.toggle()
@ -119,6 +122,7 @@ struct SVideoPlayer: View, EditItemDelegate {
}
}
KToggleButton(text: "embd", binding: $embedded).frame(height: 30)
Text(model.currentSnapshot.name).foregroundColor(Color.blue)
Text("""
@ -141,15 +145,26 @@ struct SVideoPlayer: View, EditItemDelegate {
Spacer()
Button(action: { model.edit.toggle() }, label: {
Text("edit")
})
.buttonStyle(BorderlessButtonStyle());
if !model.baseItem.compilation {
Button(action: doSnapshot, label: {
Text("snap")
Group {
Button(action: { model.edit.toggle() }, label: {
Text("edit")
})
.buttonStyle(BorderlessButtonStyle());
if !model.baseItem.compilation {
if model.currentSnapshot != nil {
Button(action: addFrame, label: {
Text("frame")
})
.buttonStyle(BorderlessButtonStyle())
}
Button(action: doSnapshot, label: {
Text("snap")
})
.buttonStyle(BorderlessButtonStyle());
}
}
}
.frame(height: 50)
@ -159,9 +174,9 @@ struct SVideoPlayer: View, EditItemDelegate {
VStack {
let v = VideoPlayerView(model: model,
player: player)
.scaleEffect(model.scale)
.scaleEffect(small ? 1 : model.scale)
.rotation3DEffect(.degrees(upsidedown ? 180 : 0), axis: (x: 1, y: 0, z: 0))
.offset(model.dragOffset).offset(x: xoffs, y: 0)
.offset(small ? CGSize.zero : model.dragOffset).modifier(KAnimate(dragOffset: model.dragOffset, spring: false)).offset(x: xoffs, y: 0)
.onTapGesture(count: 2) {
print("3 tapped!")
if model.scale == 1 {
@ -178,7 +193,8 @@ struct SVideoPlayer: View, EditItemDelegate {
let dragged = gesture.translation
if move(dragged, start: gesture.startLocation) {
model.dragOffset = CGSize(width: dragged.width + lastDragOffset.width, height: dragged.height + lastDragOffset.height)
let f = 1.5
model.dragOffset = CGSize(width: f*dragged.width + lastDragOffset.width, height: f*dragged.height + lastDragOffset.height)
}
}
.onEnded { gesture in
@ -202,7 +218,14 @@ struct SVideoPlayer: View, EditItemDelegate {
v.overlay(EditItemView(item: model.currentSnapshot, len: model.videoDuration, delegate: self)
.frame(width: 420, alignment: .top).offset(x: 0, y: -50), alignment: .topTrailing)
} else {
if more {
if embedded && !more {
v.overlay(SEmbeddedVideo(embedded: $embedded, down: $embDown), alignment: embDown ? .bottomLeading : .topLeading)
} else
if small && !more {
let normal = 385.0
v.overlay(Rectangle().stroke(Color.black, lineWidth: 2.0).opacity(0.3).frame(width: normal * 16.0 / 9.0, height: normal).offset(model.dragOffset), alignment: .topLeading)
}
else if more {
v.overlay(VStack {
Button(action: {
@ -218,17 +241,15 @@ struct SVideoPlayer: View, EditItemDelegate {
.frame(height: 30)
.foregroundColor(tilt ? Color.yellow : Color.blue).buttonStyle(BorderlessButtonStyle())
KToggleButton(text: "slow", binding: $model.slow).frame(height: 30)
KToggleButton(text: "zoom", binding: $model.zoomed).frame(height: 30)
KToggleButton(text: "jump", binding: $jump).frame(height: 30)
KToggleButton(text: "loop", binding: $model.loop).frame(height: 30)
// .fullScreenCover(isPresented: $model.loop) {
// SVideoLoopPlayer(completionHandler: {
// model.loop = false
// }, baseModel: model)
// }
Group {
KToggleButton(text: "slow", binding: $model.slow).frame(height: 30)
KToggleButton(text: "zoom", binding: $model.zoomed).frame(height: 30)
KToggleButton(text: "jump", binding: $model.jump).frame(height: 30)
KToggleButton(text: "small", binding: $small).frame(height: 30)
KToggleButton(text: "flip", binding: $upsidedown).frame(height: 30)
KToggleButton(text: "loop", binding: $model.loop).frame(height: 30)
KToggleButton(text: "flip", binding: $upsidedown).frame(height: 30)
}
Button(action: {
if model.speed == 1.0 && timeSlomoCounter == 0 {
@ -297,7 +318,35 @@ struct SVideoPlayer: View, EditItemDelegate {
})
}
.frame(width: 50, alignment: .top).offset(x: 0, y: 0), alignment: .topLeading)
} else {
}
else if frames && model.zoomed {
v.overlay(VStack {
ForEach(model.frames) { f in
Button(action: {
seekTime(model.currentSnapshot.time + f.time)
model.dragOffset.width = CGFloat(f.x)
model.dragOffset.height = CGFloat(f.y)
model.scale = f.scale
}, label: {
Text("\(f.time, specifier: "%.2f")")
}).simultaneousGesture(
LongPressGesture()
.onEnded { _ in
print("Loooong")
for i in 0...model.frames.count - 1 {
if model.frames[i].time == f.time {
model.frames.remove(at: i)
break
}
}
updateOptions()
}
)
}
}.frame(width: 70, alignment: .top).offset(x: 0, y: 0), alignment: .topTrailing)
}
else {
v
}
}
@ -320,49 +369,7 @@ struct SVideoPlayer: View, EditItemDelegate {
}
.onAppear() {
model.observed = player
model.observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.02, preferredTimescale: 600), queue: nil) { time in
if timeCounter >= 1 {
timeCounter -= 1
}
if timeSlomoCounter >= 1 {
timeSlomoCounter -= 1
}
if model.loop && model.currentSnapshot.length > 0 {
if time.seconds > model.currentSnapshot.time + model.currentSnapshot.length {
seekTime(model.currentSnapshot.time)
}
}
if tilt {
if let data = motionManager.deviceMotion {
var rotation = atan2(data.gravity.x,
data.gravity.y) - .pi
if rotation < (-1 * .pi) {
rotation += .pi + .pi
}
rotation *= 100.0
if rotazero == -1000 {
rotazero = rotation
}
rotation -= rotazero
// let roll = accelerometerData.attitude.yaw
if rotation > 30 {
xoffs += 3;
// print(xoffs)
}
if rotation < -30 {
xoffs -= 3;
// print(xoffs)
}
// print(Int(rotation * 100.0) )
}
} else {
xoffs = 0.0
rotazero = -1000
}
}
model.observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.02, preferredTimescale: 600), queue: nil, using: timeObserver())
player.automaticallyWaitsToMinimizeStalling = LocalManager.sharedInstance.settings.automaticallyWaitsToMinimizeStalling
@ -404,7 +411,71 @@ struct SVideoPlayer: View, EditItemDelegate {
}
private func timeObserver() -> (CMTime) -> () {
{ time in
if timeCounter >= 1 {
timeCounter -= 1
}
if timeSlomoCounter >= 1 {
timeSlomoCounter -= 1
}
if model.loop && model.currentSnapshot.length > 0 {
if time.seconds > model.currentSnapshot.time + model.currentSnapshot.length {
seekTime(model.currentSnapshot.time)
}
}
if model.zoomed {
if time.seconds > model.currentSnapshot.time {
let relativeTime = time.seconds - model.currentSnapshot.time;
for f in model.frames {
// print("\(f.time) \(relativeTime)")
if f.time > relativeTime && f.time < relativeTime + 1 {
model.scale = CGFloat(f.scale)
model.dragOffset.width = CGFloat(f.x)
model.dragOffset.height = CGFloat(f.y)
break
}
}
}
}
if tilt {
if let data = motionManager.deviceMotion {
var rotation = atan2(data.gravity.x,
data.gravity.y) - .pi
if rotation < (-1 * .pi) {
rotation += .pi + .pi
}
rotation *= 100.0
if rotazero == -1000 {
rotazero = rotation
}
rotation -= rotazero
// let roll = accelerometerData.attitude.yaw
if rotation > 30 {
xoffs += 3;
// print(xoffs)
}
if rotation < -30 {
xoffs -= 3;
// print(xoffs)
}
// print(Int(rotation * 100.0) )
}
} else {
xoffs = 0.0
rotazero = -1000
}
}
}
private func closePlayer(withSave: Bool) {
if model.currentSnapshot.local {
LocalManager.sharedInstance.settings.embeddedVideoUrl = model.currentURL
}
model.baseItem.favorite = model.favorite
player.pause()
player.replaceCurrentItem(with: nil)
@ -456,7 +527,7 @@ struct SVideoPlayer: View, EditItemDelegate {
return false
} else {
if model.scale != 1.0 {
if model.scale != 1.0 || small {
return true
}
else {
@ -471,7 +542,7 @@ struct SVideoPlayer: View, EditItemDelegate {
}
}
if dragged.height > 100 && jump {
if dragged.height > 100 && model.jump {
if timeCounter == 0 {
if let i = model.allItems.index(where: { m in m === model.currentSnapshot }) {
if i + 1 < model.allItems.count {
@ -489,7 +560,7 @@ struct SVideoPlayer: View, EditItemDelegate {
if let time = getCurrentTime() {
let dragWidth = 20.0
if !model.seeking {
if model.scale > 1.0 && start.y > 200 {
if (model.scale > 1.0 || small) && start.y > 200 {
return true
}
@ -515,6 +586,15 @@ struct SVideoPlayer: View, EditItemDelegate {
return false
}
private func relative() -> String {
if let time = getCurrentTime() {
return Utility.formatSecondsToHMS(time - model.currentSnapshot.time)
}
else {
return "more"
}
}
private func getCurrentTime() -> Double? {
player.currentItem?.currentTime().seconds
}
@ -534,9 +614,6 @@ struct SVideoPlayer: View, EditItemDelegate {
}
player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(600))) { _ in
// player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(10000)),
// toleranceBefore: CMTime(seconds: 0.3, preferredTimescale: CMTimeScale(10000)),
// toleranceAfter: CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(10000))){ _ in
if !model.paused {
player.play()
player.rate = model.speed
@ -574,6 +651,26 @@ struct SVideoPlayer: View, EditItemDelegate {
func gotoSnapshot(_ currentSnapshot: MediaItem) {
model.currentSnapshot = currentSnapshot
let lines = currentSnapshot.options.split(whereSeparator: \.isNewline)
model.frames.removeAll()
frames = false
for l in lines {
let values = l.components(separatedBy: CharacterSet.whitespacesAndNewlines) ?? []
if (values.count == 4) {
var f = KFrame()
f.time = Double(values[0]) ?? 0.0
f.scale = Double(values[1]) ?? 1.0
f.x = Int(values[2]) ?? 0
f.y = Int(values[3]) ?? 0
model.frames.append(f)
frames = true
}
print(l)
}
if currentSnapshot.playerURL != model.currentURL {
model.currentURL = currentSnapshot.playerURL
@ -617,6 +714,8 @@ struct SVideoPlayer: View, EditItemDelegate {
f = f * CGFloat(LocalManager.sharedInstance.settings.scale)
}
model.scale = f
model.dragOffset.width = 0
model.dragOffset.height = 0
}
}
@ -648,6 +747,32 @@ struct SVideoPlayer: View, EditItemDelegate {
}
}
func addFrame() {
if let time = getCurrentTime() {
var f = KFrame()
f.time = time - model.currentSnapshot.time
f.scale = model.scale
f.x = Int(model.dragOffset.width)
f.y = Int(model.dragOffset.height)
model.frames.append(f)
updateOptions()
frames = true
}
}
private func updateOptions() {
var options = ""
for f in model.frames {
options.append("\(String(format: "%.2f", f.time))\t\(String(format: "%.2f", f.scale))\t\(f.x)\t\(f.y)\n")
}
model.currentSnapshot.options = options
model.dirty = true
}
func setEnd() {
if let time = getCurrentTime() {
let snapTime = model.currentSnapshot.time
@ -665,7 +790,7 @@ struct SVideoPlayer: View, EditItemDelegate {
func okEdit() {
DatabaseManager.sharedInstance.saveItemMetaData(model.currentSnapshot)
model.edit = false
model.dirty = false
model.dirty = true
}
func seek(_ v: Double) {
@ -687,7 +812,7 @@ struct SVideoPlayer: View, EditItemDelegate {
return
}
player.pause()
VideoHelper.export(item: player.currentItem!, clipStart: c.time, clipDuration: dur, file: file,
VideoHelper.export(item: player.currentItem!, clipStart: c.time, clipDuration: dur, file: file, snapshot:c, zoomed: model.zoomed,
progress: { p in
let percent = Int(p * 100)
savetext = "\(percent)"

20
kplayer/video/VideoPlayerView.swift

@ -11,7 +11,7 @@ import AVFoundation
// This is the UIView that contains the AVPlayerLayer for rendering the video
class VideoPlayerUIView: UIView {
private let player: AVPlayer
private let playerLayer = AVPlayerLayer()
public let playerLayer = AVPlayerLayer()
private let videoPos: Binding<Double>
private let videoDuration: Binding<Double>
private let seeking: Binding<Bool>
@ -26,7 +26,7 @@ class VideoPlayerUIView: UIView {
super.init(frame: .zero)
backgroundColor = .black
// backgroundColor = .black
playerLayer.player = player
layer.addSublayer(playerLayer)
@ -54,22 +54,6 @@ class VideoPlayerUIView: UIView {
}
}
}
//
// override func viewDidLayoutSubviews() {
// playerLayer.frame = view.bounds
// let deviceOrientation = UIDevice.current.orientation
// switch deviceOrientation {
// case .landscapeLeft:
// playerLayer.connection!.videoOrientation = .landscapeRight
// case .landscapeRight:
// playerLayer.connection!.videoOrientation = .landscapeLeft
// case .portraitUpsideDown:
// playerLayer.connection!.videoOrientation = .portraitUpsideDown
// case .portrait:
// playerLayer.connection!.videoOrientation = .portrait
// default:
// player.connection!.videoOrientation = .portrait
// }
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")

Loading…
Cancel
Save