25 changed files with 714 additions and 181 deletions
-
12kplayer.xcodeproj/project.pbxproj
-
77kplayer/Images.xcassets/AppIcon.appiconset/Contents.json
-
33kplayer/core/DatabaseManager.swift
-
13kplayer/core/KFrame.swift
-
9kplayer/core/KSettings.swift
-
1kplayer/core/KSettingsModel.swift
-
2kplayer/core/KSnapshot+CoreDataProperties.swift
-
54kplayer/core/LocalManager.swift
-
4kplayer/core/MediaItem.swift
-
2kplayer/core/MediaModel.swift
-
9kplayer/detail/DetailViewController+Show.swift
-
3kplayer/detail/DetailViewController.swift
-
2kplayer/kplayer.xcdatamodeld/.xccurrentversion
-
38kplayer/kplayer.xcdatamodeld/kplayer 3.xcdatamodel/contents
-
4kplayer/kplayer.xcdatamodeld/kplayer.xcdatamodel/contents
-
3kplayer/master/KSettingsView.swift
-
33kplayer/master/MasterViewController.swift
-
7kplayer/photo/SPhotoAlbumView.swift
-
37kplayer/photo/SPhotoModel.swift
-
19kplayer/photo/SPhotoView.swift
-
28kplayer/util/VideoHelper.swift
-
150kplayer/video/SEmbeddedVideo.swift
-
2kplayer/video/SVideoModel.swift
-
249kplayer/video/SVideoPlayer.swift
-
20kplayer/video/VideoPlayerView.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 |
|||
} |
|||
@ -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> |
|||
@ -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) |
|||
} |
|||
|
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue