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 ZIPFoundation
|
||||
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 {
|
||||
static let ShortcutURL = "https://www.icloud.com/shortcuts/a28d2c02ca11453cb5b8f91c12cfa692"
|
||||
@@ -18,6 +51,7 @@ class PosterBoardManager: ObservableObject {
|
||||
static let shared = PosterBoardManager()
|
||||
|
||||
@Published var selectedTendies: [URL] = []
|
||||
@Published var videos: [LoadInfo] = []
|
||||
|
||||
func getTendiesStoreURL() -> URL {
|
||||
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
|
||||
var extList: [String: [URL]] = [:]
|
||||
// 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 {
|
||||
let newVideo = try VideoHandler.createCaml(from: videoURL, autoReverses: false)
|
||||
extList["com.apple.WallpaperKit.CollectionsPoster"] = [newVideo]
|
||||
let newVideo = try VideoHandler.createCaml(from: movie.url, autoReverses: false)
|
||||
extList["com.apple.WallpaperKit.CollectionsPoster"]?.append(newVideo)
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
default:
|
||||
print("Video not loaded!")
|
||||
}
|
||||
}
|
||||
}
|
||||
UIApplication.shared.change(title: "Applying Wallpapers...", body: "Extracting tendies...")
|
||||
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 {
|
||||
Group {
|
||||
if finishedTutorial {
|
||||
ContentView()
|
||||
RootView()
|
||||
} else {
|
||||
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 {
|
||||
// Prefs
|
||||
@AppStorage("pbHash") var pbHash: String = ""
|
||||
@@ -44,12 +25,6 @@ struct ContentView: View {
|
||||
|
||||
@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 lastError: String?
|
||||
@State var hideResetHelp: Bool = true
|
||||
@@ -63,49 +38,6 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
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: {
|
||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||
showTendiesImporter.toggle()
|
||||
@@ -133,26 +65,16 @@ struct ContentView: View {
|
||||
Text("Enter your PosterBoard app hash in Settings.")
|
||||
} else {
|
||||
VStack {
|
||||
if !pbManager.selectedTendies.isEmpty || selectedVideo != nil {
|
||||
if !pbManager.selectedTendies.isEmpty || !pbManager.videos.isEmpty {
|
||||
Button(action: {
|
||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||
UIApplication.shared.alert(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
var videoURL: URL? = nil
|
||||
switch loadState {
|
||||
case .loaded(let movie):
|
||||
videoURL = movie.url
|
||||
default:
|
||||
videoURL = nil
|
||||
}
|
||||
try pbManager.applyTendies(appHash: pbHash, videoURL: videoURL)
|
||||
try pbManager.applyTendies(appHash: pbHash)
|
||||
SymHandler.cleanup() // just to be extra sure
|
||||
try? FileManager.default.removeItem(at: pbManager.getTendiesStoreURL())
|
||||
if let videoURL = videoURL {
|
||||
try? FileManager.default.removeItem(at: videoURL)
|
||||
}
|
||||
Haptic.shared.notify(.success)
|
||||
UIApplication.shared.dismissAlert(animated: false)
|
||||
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