|
|
|
@ -25,6 +25,7 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
@State var confirmationShown = false |
|
|
|
@State var dirtyShown = false |
|
|
|
@State var blackShown = false |
|
|
|
@State var showSettings = false |
|
|
|
@State var truncateShown = false |
|
|
|
@State var seekSmoothly = false |
|
|
|
@State var upsidedown = false |
|
|
|
@ -42,13 +43,18 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
@State var xoffs = 0.0 |
|
|
|
@State var rotazero = -1000.0 |
|
|
|
|
|
|
|
@State var loop5 = false |
|
|
|
@State var loop5start = 0.0 |
|
|
|
|
|
|
|
@State var cutFlag = false |
|
|
|
@State var lores = false |
|
|
|
@State var second = false |
|
|
|
@State var tap3 = false |
|
|
|
@State var secondScale = 0.75 |
|
|
|
@State var secondOffset = CGSize(width: -70, height: 50) |
|
|
|
|
|
|
|
@State var orientation = UIDevice.current.orientation |
|
|
|
@State private var scrollTarget: Int? |
|
|
|
|
|
|
|
let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification) |
|
|
|
.makeConnectable() |
|
|
|
@ -65,6 +71,10 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
self.playerLooper = AVPlayerLooper(player: player, templateItem: model.currentPlayerItem()) |
|
|
|
} |
|
|
|
|
|
|
|
func isEnd() -> Bool { |
|
|
|
model.paused && model.currentSnapshot.time > 0 |
|
|
|
} |
|
|
|
|
|
|
|
func cleanup() { |
|
|
|
if model.observer != nil && model.observed != nil && model.observed! === player { |
|
|
|
do { |
|
|
|
@ -76,101 +86,133 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
var multiTapGesture: some Gesture { |
|
|
|
SimultaneousGesture(TapGesture(count: 2), TapGesture(count: 3)) |
|
|
|
.onEnded { gestureValue in |
|
|
|
if gestureValue.second != nil { |
|
|
|
if !model.baseItem.compilation { |
|
|
|
doSnapshot() |
|
|
|
} |
|
|
|
print("triple tap!") |
|
|
|
} else if gestureValue.first != nil { |
|
|
|
doubleTapZoom() |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
var body: some View { |
|
|
|
VStack { |
|
|
|
HStack { |
|
|
|
Group { |
|
|
|
Button(action: { |
|
|
|
if model.dirty { |
|
|
|
dirtyShown = true |
|
|
|
} else { |
|
|
|
let withSave = model.edit |
|
|
|
closePlayer(withSave: withSave) |
|
|
|
} |
|
|
|
}, label: { |
|
|
|
Text("cancel") |
|
|
|
}) |
|
|
|
.buttonStyle(BorderlessButtonStyle()).confirmationDialog("Save?", isPresented: $dirtyShown) { |
|
|
|
Button("save") { |
|
|
|
VStack { |
|
|
|
HStack { |
|
|
|
Button(action: { |
|
|
|
if model.dirty { |
|
|
|
if LocalManager.sharedInstance.settings.confirm { |
|
|
|
dirtyShown = true |
|
|
|
} else { |
|
|
|
cleanup() |
|
|
|
closePlayer(withSave: true) |
|
|
|
cleanup() |
|
|
|
} |
|
|
|
Button("cancel", role: .cancel) { |
|
|
|
closePlayer(withSave: false) |
|
|
|
} |
|
|
|
} else { |
|
|
|
let withSave = model.edit |
|
|
|
cleanup() |
|
|
|
closePlayer(withSave: withSave) |
|
|
|
} |
|
|
|
if !model.baseItem.compilation { |
|
|
|
Button(action: { blackShown = true }) { |
|
|
|
Text("black") |
|
|
|
} |
|
|
|
.foregroundColor(NetworkManager.sharedInstance.isBlack(model.baseItem) ? Color.yellow : Color.blue) |
|
|
|
.frame(height: 30).buttonStyle(BorderlessButtonStyle()).confirmationDialog("Delete?", isPresented: $blackShown) { |
|
|
|
Button("delete") { |
|
|
|
black(); |
|
|
|
closePlayer(withSave: false) |
|
|
|
blackShown = false |
|
|
|
}, label: { |
|
|
|
Text(LocalManager.sharedInstance.settings.confirm ? "cancel" : "next") |
|
|
|
}) |
|
|
|
.buttonStyle(BorderlessButtonStyle()).confirmationDialog("Save?", isPresented: $dirtyShown) { |
|
|
|
Button("save") { |
|
|
|
closePlayer(withSave: true) |
|
|
|
} |
|
|
|
Button("cancel", role: .cancel) { |
|
|
|
blackShown = false |
|
|
|
closePlayer(withSave: false) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
KToggleButton(text: "\(relative())", binding: $more) |
|
|
|
|
|
|
|
Button(action: { |
|
|
|
let tag = DatabaseManager.sharedInstance.currentTag |
|
|
|
if tag != "" { |
|
|
|
let item = model.currentSnapshot |
|
|
|
if item.tags.contains(tag) { |
|
|
|
item.tags.removeAll(where: { toRemove in toRemove == tag }) |
|
|
|
} else { |
|
|
|
item.tags.append(tag) |
|
|
|
if !model.baseItem.compilation { |
|
|
|
Button(action: { |
|
|
|
if isEnd() { |
|
|
|
setEnd() |
|
|
|
} else { |
|
|
|
blackShown = true |
|
|
|
} |
|
|
|
}) { |
|
|
|
Text(isEnd() ? "end" : "black") |
|
|
|
} |
|
|
|
model.dirty = true |
|
|
|
.foregroundColor(NetworkManager.sharedInstance.isBlack(model.baseItem) ? Color.yellow : Color.blue) |
|
|
|
.frame(height: 30).buttonStyle(BorderlessButtonStyle()).confirmationDialog("Delete?", isPresented: $blackShown) { |
|
|
|
Button("delete") { |
|
|
|
black(); |
|
|
|
closePlayer(withSave: false) |
|
|
|
blackShown = false |
|
|
|
} |
|
|
|
Button("cancel", role: .cancel) { |
|
|
|
blackShown = false |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
}, label: { |
|
|
|
Text(DatabaseManager.sharedInstance.currentTag) |
|
|
|
}).foregroundColor(model.currentSnapshot.tags.contains(DatabaseManager.sharedInstance.currentTag) ? Color.yellow : Color.blue) |
|
|
|
|
|
|
|
Button(action: { |
|
|
|
model.favorite.toggle() |
|
|
|
model.dirty = true |
|
|
|
}, label: { |
|
|
|
Image(systemName: "heart.fill") |
|
|
|
}) |
|
|
|
.foregroundColor(model.favorite ? Color.yellow : Color.blue).buttonStyle(BorderlessButtonStyle()) |
|
|
|
KToggleButton(text: "\(relative())", binding: $more) |
|
|
|
|
|
|
|
Button(action: { |
|
|
|
let tag = DatabaseManager.sharedInstance.currentTag |
|
|
|
if tag != "" { |
|
|
|
let item = model.currentSnapshot |
|
|
|
if item.tags.contains(tag) { |
|
|
|
item.tags.removeAll(where: { toRemove in toRemove == tag }) |
|
|
|
} else { |
|
|
|
item.tags.append(tag) |
|
|
|
} |
|
|
|
model.dirty = true |
|
|
|
} |
|
|
|
}, label: { |
|
|
|
Text(DatabaseManager.sharedInstance.currentTag) |
|
|
|
}) |
|
|
|
.foregroundColor(model.currentSnapshot.tags.contains(DatabaseManager.sharedInstance.currentTag) ? Color.yellow : Color.blue) |
|
|
|
|
|
|
|
Button(action: { |
|
|
|
model.favorite.toggle() |
|
|
|
model.dirty = true |
|
|
|
}, label: { |
|
|
|
Image(systemName: "heart.fill") |
|
|
|
}) |
|
|
|
.foregroundColor(model.favorite ? Color.yellow : Color.blue).buttonStyle(BorderlessButtonStyle()) |
|
|
|
|
|
|
|
Button(action: { confirmationShown = true }, label: { |
|
|
|
Text(savetext) |
|
|
|
}) |
|
|
|
.buttonStyle(BorderlessButtonStyle()) |
|
|
|
.confirmationDialog("Save to folder", isPresented: $confirmationShown) { |
|
|
|
Button("1") { |
|
|
|
save(currentSnapshot: model.currentSnapshot, name: "1") |
|
|
|
} |
|
|
|
Button("2") { |
|
|
|
save(currentSnapshot: model.currentSnapshot, name: "2") |
|
|
|
} |
|
|
|
Button("3") { |
|
|
|
save(currentSnapshot: model.currentSnapshot, name: "3") |
|
|
|
} |
|
|
|
KToggleButton(text: "loop5", binding: $loop5) |
|
|
|
KToggleButton(text: "cfg", binding: $showSettings).frame(height: 30) |
|
|
|
Button(action: { confirmationShown = true }, label: { |
|
|
|
Text(savetext) |
|
|
|
}) |
|
|
|
.buttonStyle(BorderlessButtonStyle()) |
|
|
|
.confirmationDialog("Save to folder", isPresented: $confirmationShown) { |
|
|
|
Button("1") { |
|
|
|
save(currentSnapshot: model.currentSnapshot, name: "1") |
|
|
|
} |
|
|
|
Button("2") { |
|
|
|
save(currentSnapshot: model.currentSnapshot, name: "2") |
|
|
|
} |
|
|
|
Button("3") { |
|
|
|
save(currentSnapshot: model.currentSnapshot, name: "3") |
|
|
|
} |
|
|
|
|
|
|
|
Button("cancel", role: .cancel) { |
|
|
|
Button("cancel", role: .cancel) { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
KToggleButton(text: "embd", binding: $embedded).frame(height: 30) |
|
|
|
Group { |
|
|
|
Text(model.currentSnapshot.name).foregroundColor(Color.blue) |
|
|
|
Text(""" |
|
|
|
(\(model.codec) \(model.height), \(model.nominalFrameRate), \(model.bitRate)m)\n\(model.scale, specifier: "%.2f")x (\(model.dragOffset.width, specifier: "%.0f"),\(model.dragOffset.height, specifier: "%.0f")) |
|
|
|
""").foregroundColor(Color.blue) |
|
|
|
|
|
|
|
KToggleButton(text: "embd", binding: $embedded).frame(height: 30) |
|
|
|
} |
|
|
|
HStack { |
|
|
|
Text(model.currentSnapshot.name).foregroundColor(Color.blue).fontWeight(Font.Weight.light) |
|
|
|
} |
|
|
|
} |
|
|
|
HStack { |
|
|
|
ScrollViewReader { scrollvalue in |
|
|
|
ScrollView(.horizontal, showsIndicators: false) { |
|
|
|
HStack { |
|
|
|
ForEach(model.allItems) { item in |
|
|
|
Button(action: { |
|
|
|
gotoSnapshot(item) |
|
|
|
scrollvalue.scrollTo(item.id) |
|
|
|
}) { |
|
|
|
AsyncImage(item: item, placeholder: { Text("Loading ...") }, |
|
|
|
image: { Image(uiImage: $0).resizable() }).border(.yellow, width: (item === model.currentSnapshot) ? 1 : 0) |
|
|
|
@ -179,33 +221,46 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
// .onChange(of: scrollTarget { target in |
|
|
|
// if let tg = target { |
|
|
|
// scrollTarget = nil |
|
|
|
// withAnimation { |
|
|
|
// scrollvalue.scrollTo(tg) |
|
|
|
// } |
|
|
|
// } |
|
|
|
// }) |
|
|
|
} |
|
|
|
|
|
|
|
Spacer() |
|
|
|
|
|
|
|
Spacer() |
|
|
|
VStack { |
|
|
|
HStack { |
|
|
|
Button(action: { model.edit.toggle() }, label: { |
|
|
|
Text("edit") |
|
|
|
}) |
|
|
|
.buttonStyle(BorderlessButtonStyle()); |
|
|
|
|
|
|
|
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()) |
|
|
|
} |
|
|
|
|
|
|
|
if !model.baseItem.compilation { |
|
|
|
if model.currentSnapshot != nil { |
|
|
|
Button(action: addFrame, label: { |
|
|
|
Text("frame") |
|
|
|
Button(action: doSnapshot, label: { |
|
|
|
Text("snap") |
|
|
|
}) |
|
|
|
.buttonStyle(BorderlessButtonStyle()) |
|
|
|
} |
|
|
|
|
|
|
|
Button(action: doSnapshot, label: { |
|
|
|
Text("snap") |
|
|
|
}) |
|
|
|
.buttonStyle(BorderlessButtonStyle()); |
|
|
|
.buttonStyle(BorderlessButtonStyle()); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
Text(""" |
|
|
|
(\(model.codec) \(model.height), \(model.nominalFrameRate), \(model.bitRate)m)\n\(model.scale, specifier: "%.2f")x (\(model.dragOffset.width, specifier: "%.0f"),\(model.dragOffset.height, specifier: "%.0f")) |
|
|
|
""").foregroundColor(Color.blue).fontWeight(Font.Weight.light) |
|
|
|
} |
|
|
|
} |
|
|
|
.frame(height: 50) |
|
|
|
} |
|
|
|
.frame(height: 50) |
|
|
|
|
|
|
|
GeometryReader { geometry in |
|
|
|
VStack { |
|
|
|
@ -214,25 +269,14 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
.scaleEffect(small ? 1 : model.scale) |
|
|
|
.rotation3DEffect(.degrees(upsidedown ? 180 : 0), axis: (x: 1, y: 0, z: 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!") |
|
|
|
second = !second |
|
|
|
if second { |
|
|
|
model.scale = lastScale |
|
|
|
} else { |
|
|
|
lastScale = model.scale |
|
|
|
|
|
|
|
if (lores) { |
|
|
|
model.scale = secondScale |
|
|
|
model.dragOffset = secondOffset |
|
|
|
} |
|
|
|
else { |
|
|
|
model.scale = 1 |
|
|
|
model.dragOffset = CGSize.zero |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
.gesture(multiTapGesture) |
|
|
|
.simultaneousGesture( |
|
|
|
LongPressGesture() |
|
|
|
.onEnded { _ in |
|
|
|
print("Loooong") |
|
|
|
togglePause() |
|
|
|
} |
|
|
|
) |
|
|
|
.gesture( |
|
|
|
DragGesture() |
|
|
|
.onChanged { gesture in |
|
|
|
@ -323,12 +367,7 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
.frame(height: 30) |
|
|
|
.buttonStyle(BorderlessButtonStyle()) |
|
|
|
Button(action: { |
|
|
|
if model.paused { |
|
|
|
player.play() |
|
|
|
} else { |
|
|
|
player.pause() |
|
|
|
} |
|
|
|
model.paused = !model.paused |
|
|
|
togglePause() |
|
|
|
}, label: { |
|
|
|
Text("pause") |
|
|
|
}) |
|
|
|
@ -366,7 +405,8 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
} |
|
|
|
Button(action: { cut() }) { |
|
|
|
Text("cut") |
|
|
|
}.foregroundColor(cutFlag ? Color.yellow : Color.blue) |
|
|
|
} |
|
|
|
.foregroundColor(cutFlag ? Color.yellow : Color.blue) |
|
|
|
.frame(height: 30).buttonStyle(BorderlessButtonStyle()) |
|
|
|
} |
|
|
|
|
|
|
|
@ -401,6 +441,12 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
}) |
|
|
|
} |
|
|
|
.frame(width: 50, alignment: .top).offset(x: 0, y: 0), alignment: .topLeading) |
|
|
|
} else if model.tagging { |
|
|
|
v.overlay(ScrollView() { |
|
|
|
TagEditor(item: model.currentSnapshot, completionHandler: NetworkManager.sharedInstance.saveItem) |
|
|
|
} |
|
|
|
.frame(width: 170, alignment: .top).offset(x: 0, y: 0), |
|
|
|
alignment: .topLeading) |
|
|
|
} else if frames && model.zoomed { |
|
|
|
v.overlay(VStack { |
|
|
|
ForEach(model.frames) { f in |
|
|
|
@ -450,6 +496,11 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
.sheet(isPresented: $showSettings, onDismiss: { showSettings = false }) { |
|
|
|
KSettingsView(kSettings: LocalManager.sharedInstance.settings, completionHandler: { |
|
|
|
showSettings = false |
|
|
|
}) |
|
|
|
} |
|
|
|
.onAppear() { |
|
|
|
model.observed = player |
|
|
|
model.observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.02, preferredTimescale: 600), queue: nil, using: timeObserver()) |
|
|
|
@ -491,10 +542,46 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
} |
|
|
|
|
|
|
|
model.bitRate = Int(lastEvent.indicatedBitrate / 1024 / 1024) |
|
|
|
}.onChange(of: loop5) { new in |
|
|
|
if new { |
|
|
|
if model.paused { |
|
|
|
model.paused = false |
|
|
|
player.play() |
|
|
|
player.rate = 0.8 |
|
|
|
} |
|
|
|
loop5start = player.currentTime().seconds |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
private func togglePause() { |
|
|
|
if model.paused { |
|
|
|
player.play() |
|
|
|
} else { |
|
|
|
player.pause() |
|
|
|
} |
|
|
|
model.paused = !model.paused |
|
|
|
} |
|
|
|
|
|
|
|
private func doubleTapZoom() { |
|
|
|
second = !second |
|
|
|
if second { |
|
|
|
model.scale = lastScale |
|
|
|
} else { |
|
|
|
lastScale = model.scale |
|
|
|
|
|
|
|
if (lores) { |
|
|
|
model.scale = secondScale |
|
|
|
model.dragOffset = secondOffset |
|
|
|
} else { |
|
|
|
model.scale = 1 |
|
|
|
model.dragOffset = CGSize.zero |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private func timeObserver() -> (CMTime) -> () { |
|
|
|
{ time in |
|
|
|
if timeCounter >= 1 { |
|
|
|
@ -503,6 +590,11 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
if timeSlomoCounter >= 1 { |
|
|
|
timeSlomoCounter -= 1 |
|
|
|
} |
|
|
|
if loop5 { |
|
|
|
if time.seconds > loop5start + 5.0 { |
|
|
|
seekTime(loop5start) |
|
|
|
} |
|
|
|
} |
|
|
|
if model.loop && !model.paused && model.currentSnapshot.length > 0 { |
|
|
|
if time.seconds > model.currentSnapshot.time + model.currentSnapshot.length { |
|
|
|
seekTime(model.currentSnapshot.time) |
|
|
|
@ -596,6 +688,7 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
newItem.external = currentItem.external |
|
|
|
model.allItems.append(newItem) |
|
|
|
model.currentSnapshot = newItem |
|
|
|
scrollTarget = model.allItems.count |
|
|
|
} |
|
|
|
|
|
|
|
private func move(_ dragged: CGSize, start: CGPoint) -> Bool { |
|
|
|
@ -606,7 +699,7 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
} |
|
|
|
|
|
|
|
if (start.y < 170) { |
|
|
|
let delta = (dragged.width + dragged.height) / 200.0 |
|
|
|
let delta = (dragged.width + dragged.height) / 100.0 |
|
|
|
seekTimeSmoothly(smoothTime + delta) |
|
|
|
|
|
|
|
return false |
|
|
|
@ -614,7 +707,7 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
if second || small { |
|
|
|
return true |
|
|
|
} else { |
|
|
|
let delta = (dragged.width + dragged.height) / 200.0 |
|
|
|
let delta = (dragged.width + dragged.height) / 30.0 |
|
|
|
seekTimeSmoothly(smoothTime + delta) |
|
|
|
|
|
|
|
return false |
|
|
|
@ -646,7 +739,7 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
if i - 1 >= 0 { |
|
|
|
gotoSnapshot(model.allItems[i - 1]) |
|
|
|
} else { |
|
|
|
gotoSnapshot(model.allItems[model.allItems.count-1]) |
|
|
|
gotoSnapshot(model.allItems[model.allItems.count - 1]) |
|
|
|
} |
|
|
|
timeCounter = 50 |
|
|
|
print("jump") |
|
|
|
@ -797,7 +890,7 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
} |
|
|
|
model.height = Int(height) |
|
|
|
|
|
|
|
lores = (height <= 1080) && (height*1.5 < heightSpace) |
|
|
|
lores = (height <= 1080) && (height * 1.5 < heightSpace) |
|
|
|
|
|
|
|
var f = height / heightSpace |
|
|
|
print("h \(height) \(heightSpace) \(f)") |
|
|
|
@ -815,8 +908,7 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
model.scale = f |
|
|
|
model.dragOffset.width = 0 |
|
|
|
model.dragOffset.height = 0 |
|
|
|
} |
|
|
|
else { |
|
|
|
} else { |
|
|
|
if (model.scale != 1) { |
|
|
|
lastScale = model.scale |
|
|
|
} |
|
|
|
@ -824,8 +916,7 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
if (lores) { |
|
|
|
model.scale = secondScale |
|
|
|
model.dragOffset = secondOffset |
|
|
|
} |
|
|
|
else { |
|
|
|
} else { |
|
|
|
model.scale = 1 |
|
|
|
model.dragOffset = CGSize.zero |
|
|
|
} |
|
|
|
|