From ef444f7c5c0388c6ac93a8032cbe9b73fec6bea5 Mon Sep 17 00:00:00 2001 From: leminlimez <59540996+leminlimez@users.noreply.github.com> Date: Wed, 11 Jun 2025 20:58:28 -0400 Subject: [PATCH] new video adding ui + selecting multiple videos --- .../Controllers/PosterBoardManager.swift | 56 ++++++++-- .../Extensions/LoopingPlayerUIView.swift | 41 +++++++ Pocket Poster/Extensions/PlayerView.swift | 23 ++++ Pocket Poster/Pocket_PosterApp.swift | 2 +- Pocket Poster/Views/ContentView.swift | 82 +------------- .../Views/NavigatorViews/VideoView.swift | 102 ++++++++++++++++++ Pocket Poster/Views/RootView.swift | 23 ++++ 7 files changed, 241 insertions(+), 88 deletions(-) create mode 100644 Pocket Poster/Extensions/LoopingPlayerUIView.swift create mode 100644 Pocket Poster/Extensions/PlayerView.swift create mode 100644 Pocket Poster/Views/NavigatorViews/VideoView.swift create mode 100644 Pocket Poster/Views/RootView.swift diff --git a/Pocket Poster/Controllers/PosterBoardManager.swift b/Pocket Poster/Controllers/PosterBoardManager.swift index a052ee7..b95fbf9 100644 --- a/Pocket Poster/Controllers/PosterBoardManager.swift +++ b/Pocket Poster/Controllers/PosterBoardManager.swift @@ -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,16 +212,24 @@ 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 { - do { - let newVideo = try VideoHandler.createCaml(from: videoURL, autoReverses: false) - extList["com.apple.WallpaperKit.CollectionsPoster"] = [newVideo] - } catch { - print(error.localizedDescription) + 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: 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...") diff --git a/Pocket Poster/Extensions/LoopingPlayerUIView.swift b/Pocket Poster/Extensions/LoopingPlayerUIView.swift new file mode 100644 index 0000000..b169828 --- /dev/null +++ b/Pocket Poster/Extensions/LoopingPlayerUIView.swift @@ -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 + } +} diff --git a/Pocket Poster/Extensions/PlayerView.swift b/Pocket Poster/Extensions/PlayerView.swift new file mode 100644 index 0000000..1f9d0c1 --- /dev/null +++ b/Pocket Poster/Extensions/PlayerView.swift @@ -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) { + } + + func makeUIView(context: Context) -> UIView { + return LoopingPlayerUIView(videoURL: videoURL) + } +} diff --git a/Pocket Poster/Pocket_PosterApp.swift b/Pocket Poster/Pocket_PosterApp.swift index 6a17223..22437f0 100644 --- a/Pocket Poster/Pocket_PosterApp.swift +++ b/Pocket Poster/Pocket_PosterApp.swift @@ -20,7 +20,7 @@ struct Pocket_PosterApp: App { WindowGroup { Group { if finishedTutorial { - ContentView() + RootView() } else { OnBoardingView(cards: onBoardingCards, isFinished: $finishedTutorial) } diff --git a/Pocket Poster/Views/ContentView.swift b/Pocket Poster/Views/ContentView.swift index 73c8853..adc9e83 100644 --- a/Pocket Poster/Views/ContentView.swift +++ b/Pocket Poster/Views/ContentView.swift @@ -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: { diff --git a/Pocket Poster/Views/NavigatorViews/VideoView.swift b/Pocket Poster/Views/NavigatorViews/VideoView.swift new file mode 100644 index 0000000..a5000d2 --- /dev/null +++ b/Pocket Poster/Views/NavigatorViews/VideoView.swift @@ -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) + } + } + } +} diff --git a/Pocket Poster/Views/RootView.swift b/Pocket Poster/Views/RootView.swift new file mode 100644 index 0000000..19bf41d --- /dev/null +++ b/Pocket Poster/Views/RootView.swift @@ -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") + } + } + } +}