new video adding ui + selecting multiple videos
This commit is contained in:
1 parent
047c9e9504
commit
ef444f7c5c
7 files changed
+238
-85
No files matched your search
@@ -8,6 +8,39 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import ZIPFoundation
|
import ZIPFoundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import CoreTransferable
|
||||||
|
|
||||||
|
struct Movie: Transferable {
|
||||||
|
let url: URL
|
||||||
|
|
||||||
|
static var transferRepresentation: some TransferRepresentation {
|
||||||
|
FileRepresentation(contentType: .movie) { movie in
|
||||||
|
SentTransferredFile(movie.url)
|
||||||
|
} importing: { received in
|
||||||
|
let videoFolder = SymHandler.getDocumentsDirectory().appendingPathComponent("Videos", conformingTo: .directory)
|
||||||
|
if !FileManager.default.fileExists(atPath: videoFolder.path()) {
|
||||||
|
try? FileManager.default.createDirectory(at: videoFolder, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
let copy = videoFolder.appending(path: "\(UUID()).mp4")
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: copy.path()) {
|
||||||
|
try FileManager.default.removeItem(at: copy)
|
||||||
|
}
|
||||||
|
|
||||||
|
try FileManager.default.copyItem(at: received.file, to: copy)
|
||||||
|
return Self.init(url: copy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LoadState {
|
||||||
|
case unknown, loading, loaded(Movie), failed
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LoadInfo: Identifiable {
|
||||||
|
var id = UUID()
|
||||||
|
var loadState: LoadState
|
||||||
|
}
|
||||||
|
|
||||||
class PosterBoardManager: ObservableObject {
|
class PosterBoardManager: ObservableObject {
|
||||||
static let ShortcutURL = "https://www.icloud.com/shortcuts/a28d2c02ca11453cb5b8f91c12cfa692"
|
static let ShortcutURL = "https://www.icloud.com/shortcuts/a28d2c02ca11453cb5b8f91c12cfa692"
|
||||||
@@ -18,6 +51,7 @@ class PosterBoardManager: ObservableObject {
|
|||||||
static let shared = PosterBoardManager()
|
static let shared = PosterBoardManager()
|
||||||
|
|
||||||
@Published var selectedTendies: [URL] = []
|
@Published var selectedTendies: [URL] = []
|
||||||
|
@Published var videos: [LoadInfo] = []
|
||||||
|
|
||||||
func getTendiesStoreURL() -> URL {
|
func getTendiesStoreURL() -> URL {
|
||||||
let tendiesStoreURL = SymHandler.getDocumentsDirectory().appendingPathComponent("KFC Bucket", conformingTo: .directory)
|
let tendiesStoreURL = SymHandler.getDocumentsDirectory().appendingPathComponent("KFC Bucket", conformingTo: .directory)
|
||||||
@@ -178,17 +212,25 @@ class PosterBoardManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyTendies(appHash: String, videoURL: URL?) throws {
|
func applyTendies(appHash: String) throws {
|
||||||
// organize the descriptors into their respective extensions
|
// organize the descriptors into their respective extensions
|
||||||
var extList: [String: [URL]] = [:]
|
var extList: [String: [URL]] = [:]
|
||||||
// create the video first
|
// create the video first
|
||||||
if let videoURL = videoURL {
|
if videos.count > 0 {
|
||||||
|
extList["com.apple.WallpaperKit.CollectionsPoster"] = []
|
||||||
|
for video in videos {
|
||||||
|
switch video.loadState {
|
||||||
|
case .loaded(let movie):
|
||||||
do {
|
do {
|
||||||
let newVideo = try VideoHandler.createCaml(from: videoURL, autoReverses: false)
|
let newVideo = try VideoHandler.createCaml(from: movie.url, autoReverses: false)
|
||||||
extList["com.apple.WallpaperKit.CollectionsPoster"] = [newVideo]
|
extList["com.apple.WallpaperKit.CollectionsPoster"]?.append(newVideo)
|
||||||
} catch {
|
} catch {
|
||||||
print(error.localizedDescription)
|
print(error.localizedDescription)
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
print("Video not loaded!")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
UIApplication.shared.change(title: "Applying Wallpapers...", body: "Extracting tendies...")
|
UIApplication.shared.change(title: "Applying Wallpapers...", body: "Extracting tendies...")
|
||||||
for url in selectedTendies {
|
for url in selectedTendies {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// LoopingPlayerUIView.swift
|
||||||
|
// Pocket Poster
|
||||||
|
//
|
||||||
|
// Created by lemin on 6/11/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
class LoopingPlayerUIView: UIView {
|
||||||
|
|
||||||
|
private var playerLayer = AVPlayerLayer()
|
||||||
|
private var playerLooper: AVPlayerLooper?
|
||||||
|
private var player = AVQueuePlayer()
|
||||||
|
|
||||||
|
init(videoURL: URL){
|
||||||
|
let asset = AVAsset(url: videoURL)
|
||||||
|
let item = AVPlayerItem(asset: asset)
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
// Setup the player
|
||||||
|
playerLayer.player = player
|
||||||
|
playerLayer.videoGravity = .resizeAspectFill
|
||||||
|
layer.addSublayer(playerLayer)
|
||||||
|
|
||||||
|
// Create a new player looper with the queue player and template item
|
||||||
|
playerLooper = AVPlayerLooper(player: player, templateItem: item)
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
playerLayer.frame = bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// PlayerView.swift
|
||||||
|
// Pocket Poster
|
||||||
|
//
|
||||||
|
// Created by lemin on 6/11/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PlayerView: UIViewRepresentable {
|
||||||
|
var videoURL: URL
|
||||||
|
|
||||||
|
init(videoURL: URL) {
|
||||||
|
self.videoURL = videoURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UIView {
|
||||||
|
return LoopingPlayerUIView(videoURL: videoURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ struct Pocket_PosterApp: App {
|
|||||||
WindowGroup {
|
WindowGroup {
|
||||||
Group {
|
Group {
|
||||||
if finishedTutorial {
|
if finishedTutorial {
|
||||||
ContentView()
|
RootView()
|
||||||
} else {
|
} else {
|
||||||
OnBoardingView(cards: onBoardingCards, isFinished: $finishedTutorial)
|
OnBoardingView(cards: onBoardingCards, isFinished: $finishedTutorial)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,25 +15,6 @@ extension UIDocumentPickerViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Movie: Transferable {
|
|
||||||
let url: URL
|
|
||||||
|
|
||||||
static var transferRepresentation: some TransferRepresentation {
|
|
||||||
FileRepresentation(contentType: .movie) { movie in
|
|
||||||
SentTransferredFile(movie.url)
|
|
||||||
} importing: { received in
|
|
||||||
let copy = URL.documentsDirectory.appending(path: "movie.mp4")
|
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: copy.path()) {
|
|
||||||
try FileManager.default.removeItem(at: copy)
|
|
||||||
}
|
|
||||||
|
|
||||||
try FileManager.default.copyItem(at: received.file, to: copy)
|
|
||||||
return Self.init(url: copy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
// Prefs
|
// Prefs
|
||||||
@AppStorage("pbHash") var pbHash: String = ""
|
@AppStorage("pbHash") var pbHash: String = ""
|
||||||
@@ -44,12 +25,6 @@ struct ContentView: View {
|
|||||||
|
|
||||||
@State var showTendiesImporter: Bool = false
|
@State var showTendiesImporter: Bool = false
|
||||||
|
|
||||||
enum LoadState {
|
|
||||||
case unknown, loading, loaded(Movie), failed
|
|
||||||
}
|
|
||||||
@State var selectedVideo: PhotosPickerItem?
|
|
||||||
@State private var loadState = LoadState.unknown
|
|
||||||
|
|
||||||
@State var showErrorAlert = false
|
@State var showErrorAlert = false
|
||||||
@State var lastError: String?
|
@State var lastError: String?
|
||||||
@State var hideResetHelp: Bool = true
|
@State var hideResetHelp: Bool = true
|
||||||
@@ -63,49 +38,6 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
VStack {
|
|
||||||
switch loadState {
|
|
||||||
case .unknown:
|
|
||||||
EmptyView()
|
|
||||||
case .loading:
|
|
||||||
ProgressView()
|
|
||||||
case .loaded(let movie):
|
|
||||||
Text("Video imported")
|
|
||||||
case .failed:
|
|
||||||
Text("Import failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch loadState {
|
|
||||||
case .loaded(let movie):
|
|
||||||
Button(action: {
|
|
||||||
selectedVideo = nil
|
|
||||||
try? FileManager.default.removeItem(at: movie.url)
|
|
||||||
loadState = .unknown
|
|
||||||
}) {
|
|
||||||
Text("Remove Selected Video")
|
|
||||||
}
|
|
||||||
.buttonStyle(TintedButton(color: .red, fullwidth: true))
|
|
||||||
default:
|
|
||||||
PhotosPicker("Select Video", selection: $selectedVideo, matching: .videos)
|
|
||||||
.buttonStyle(TintedButton(color: .yellow, fullwidth: true))
|
|
||||||
.onChange(of: selectedVideo) { _ in
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
loadState = .loading
|
|
||||||
|
|
||||||
if let movie = try await selectedVideo?.loadTransferable(type: Movie.self) {
|
|
||||||
loadState = .loaded(movie)
|
|
||||||
} else {
|
|
||||||
loadState = .failed
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
loadState = .failed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||||
showTendiesImporter.toggle()
|
showTendiesImporter.toggle()
|
||||||
@@ -133,26 +65,16 @@ struct ContentView: View {
|
|||||||
Text("Enter your PosterBoard app hash in Settings.")
|
Text("Enter your PosterBoard app hash in Settings.")
|
||||||
} else {
|
} else {
|
||||||
VStack {
|
VStack {
|
||||||
if !pbManager.selectedTendies.isEmpty || selectedVideo != nil {
|
if !pbManager.selectedTendies.isEmpty || !pbManager.videos.isEmpty {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||||
UIApplication.shared.alert(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
|
UIApplication.shared.alert(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
do {
|
do {
|
||||||
var videoURL: URL? = nil
|
try pbManager.applyTendies(appHash: pbHash)
|
||||||
switch loadState {
|
|
||||||
case .loaded(let movie):
|
|
||||||
videoURL = movie.url
|
|
||||||
default:
|
|
||||||
videoURL = nil
|
|
||||||
}
|
|
||||||
try pbManager.applyTendies(appHash: pbHash, videoURL: videoURL)
|
|
||||||
SymHandler.cleanup() // just to be extra sure
|
SymHandler.cleanup() // just to be extra sure
|
||||||
try? FileManager.default.removeItem(at: pbManager.getTendiesStoreURL())
|
try? FileManager.default.removeItem(at: pbManager.getTendiesStoreURL())
|
||||||
if let videoURL = videoURL {
|
|
||||||
try? FileManager.default.removeItem(at: videoURL)
|
|
||||||
}
|
|
||||||
Haptic.shared.notify(.success)
|
Haptic.shared.notify(.success)
|
||||||
UIApplication.shared.dismissAlert(animated: false)
|
UIApplication.shared.dismissAlert(animated: false)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
//
|
||||||
|
// VideoView.swift
|
||||||
|
// Pocket Poster
|
||||||
|
//
|
||||||
|
// Created by lemin on 6/11/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import PhotosUI
|
||||||
|
import AVKit
|
||||||
|
|
||||||
|
struct VideoView: View {
|
||||||
|
@ObservedObject var pbManager = PosterBoardManager.shared
|
||||||
|
|
||||||
|
@State var selectedVideo: PhotosPickerItem?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
GeometryReader { geom in
|
||||||
|
TabView {
|
||||||
|
ForEach(pbManager.videos) { vid in
|
||||||
|
ZStack {
|
||||||
|
switch vid.loadState {
|
||||||
|
case .unknown:
|
||||||
|
EmptyView()
|
||||||
|
case .loading:
|
||||||
|
ProgressView()
|
||||||
|
case .loaded(let movie):
|
||||||
|
PlayerView(videoURL: movie.url)
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: geom.size.width * 0.7, height: geom.size.height * 0.8)
|
||||||
|
.clipped()
|
||||||
|
case .failed:
|
||||||
|
Text("Failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: geom.size.width * 0.7, height: geom.size.height * 0.8)
|
||||||
|
.shadow(radius: 5)
|
||||||
|
.background(Color.black.opacity(0.6))
|
||||||
|
.cornerRadius(24)
|
||||||
|
.overlay {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.stroke(.gray, lineWidth: 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Select Photo Option
|
||||||
|
if selectedVideo == nil {
|
||||||
|
ZStack {
|
||||||
|
PhotosPicker(selection: $selectedVideo, matching: .videos, label: {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.fill(.gray.opacity(0.4))
|
||||||
|
|
||||||
|
Circle()
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
|
||||||
|
Image(systemName: "plus")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.frame(width: geom.size.width * 0.7, height: geom.size.height * 0.8)
|
||||||
|
.shadow(radius: 5)
|
||||||
|
.background(Color.black.opacity(0.6))
|
||||||
|
.cornerRadius(24)
|
||||||
|
.overlay {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.stroke(.gray, lineWidth: 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: selectedVideo) { _ in
|
||||||
|
if selectedVideo == nil { return }
|
||||||
|
let id = pbManager.videos.count
|
||||||
|
pbManager.videos.append(.init(loadState: .loading))
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
if let movie = try await selectedVideo?.loadTransferable(type: Movie.self) {
|
||||||
|
await MainActor.run {
|
||||||
|
pbManager.videos[id].loadState = .loaded(movie)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await MainActor.run {
|
||||||
|
pbManager.videos[id].loadState = .failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
pbManager.videos[id].loadState = .failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedVideo = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabViewStyle(.page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// RootView.swift
|
||||||
|
// Pocket Poster
|
||||||
|
//
|
||||||
|
// Created by lemin on 6/11/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct RootView: View {
|
||||||
|
var body: some View {
|
||||||
|
TabView {
|
||||||
|
ContentView()
|
||||||
|
.tabItem {
|
||||||
|
Label("Home", systemImage: "house")
|
||||||
|
}
|
||||||
|
VideoView()
|
||||||
|
.tabItem {
|
||||||
|
Label("Videos", systemImage: "camera")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user