From fcbd5fe6804754690fac126d695ca8a7797dd6c2 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 29 Oct 2025 21:44:33 +0800 Subject: [PATCH] :sparkles: watchOS showing video --- .../Views/AttachmentView.swift | 96 +++++++++++++++++++ .../Views/AudioPlayerView.swift | 47 +++++++++ ...hmentImageView.swift => ImageViewer.swift} | 24 ++--- .../Views/PostViews.swift | 2 +- .../Views/VideoPlayerView.swift | 12 +++ 5 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 ios/WatchRunner Watch App/Views/AttachmentView.swift create mode 100644 ios/WatchRunner Watch App/Views/AudioPlayerView.swift rename ios/WatchRunner Watch App/Views/{AttachmentImageView.swift => ImageViewer.swift} (54%) create mode 100644 ios/WatchRunner Watch App/Views/VideoPlayerView.swift diff --git a/ios/WatchRunner Watch App/Views/AttachmentView.swift b/ios/WatchRunner Watch App/Views/AttachmentView.swift new file mode 100644 index 00000000..2f7f49c3 --- /dev/null +++ b/ios/WatchRunner Watch App/Views/AttachmentView.swift @@ -0,0 +1,96 @@ +// +// AttachmentImageView.swift +// WatchRunner Watch App +// +// Created by LittleSheep on 2025/10/29. +// + +import SwiftUI +import AVKit +import AVFoundation + +struct AttachmentView: View { + let attachment: SnCloudFile + @EnvironmentObject var appState: AppState + @StateObject private var imageLoader = ImageLoader() + + var body: some View { + Group { + if let mimeType = attachment.mimeType { + if mimeType.starts(with: "image") { + if let serverUrl = appState.serverUrl, let imageUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl) { + NavigationLink( + destination: ImageViewer(imageUrl: imageUrl).environmentObject(appState) + ) { + if imageLoader.isLoading { + ProgressView() + } else if let image = imageLoader.image { + image + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity) + .cornerRadius(8) + } else if let errorMessage = imageLoader.errorMessage { + Text("Failed to load attachment: \(errorMessage)") + .font(.caption) + .foregroundColor(.red) + .cornerRadius(8) + } else { + Text("File: \(attachment.id)") + .cornerRadius(8) + } + } + .buttonStyle(PlainButtonStyle()) + } else { + Text("Image URL not available.") + } + } else if mimeType.starts(with: "video") { + if let serverUrl = appState.serverUrl, let videoUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl) { + let thumbnailUrl = videoUrl.appendingPathComponent("thumbnail") // Construct thumbnail URL + NavigationLink(destination: VideoPlayerView(videoUrl: videoUrl)) { + AsyncImage(url: thumbnailUrl) { phase in + if let image = phase.image { + image + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity) + .cornerRadius(8) + } else if phase.error != nil { + Image(systemName: "play.rectangle.fill") // Placeholder for video thumbnail + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity) + .foregroundColor(.gray) + .cornerRadius(8) + } else { + ProgressView() + .cornerRadius(8) + } + } + } + .buttonStyle(PlainButtonStyle()) + } else { + Text("Video URL not available.") + } + } else if mimeType.starts(with: "audio") { + if let serverUrl = appState.serverUrl, let audioUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl) { + AudioPlayerView(audioUrl: audioUrl) + } else { + Text("Cannot play audio: URL not available.") + } + } else { + Text("Unsupported media type: \(mimeType)") + } + } else { + Text("File: \(attachment.id) (No MIME type)") + } + } + .task(id: attachment.id) { + if let serverUrl = appState.serverUrl, let attachmentUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl), let token = appState.token { + if attachment.mimeType?.starts(with: "image") == true { + await imageLoader.loadImage(from: attachmentUrl, token: token) + } + } + } + } +} diff --git a/ios/WatchRunner Watch App/Views/AudioPlayerView.swift b/ios/WatchRunner Watch App/Views/AudioPlayerView.swift new file mode 100644 index 00000000..0fbb3ab9 --- /dev/null +++ b/ios/WatchRunner Watch App/Views/AudioPlayerView.swift @@ -0,0 +1,47 @@ + +// +// AudioPlayerView.swift +// WatchRunner Watch App +// +// Created by LittleSheep on 2025/10/29. +// + +import SwiftUI +import AVFoundation + +struct AudioPlayerView: View { + let audioUrl: URL + @State private var player: AVPlayer? + @State private var isPlaying: Bool = false + + var body: some View { + VStack { + if player != nil { + Button(action: togglePlayPause) { + Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill") + .font(.largeTitle) + } + .buttonStyle(.plain) + } else { + Text("Loading audio...") + } + } + .onAppear { + player = AVPlayer(url: audioUrl) + } + .onDisappear { + player?.pause() + player = nil + } + } + + private func togglePlayPause() { + guard let player = player else { return } + if isPlaying { + player.pause() + } else { + player.play() + } + isPlaying.toggle() + } +} diff --git a/ios/WatchRunner Watch App/Views/AttachmentImageView.swift b/ios/WatchRunner Watch App/Views/ImageViewer.swift similarity index 54% rename from ios/WatchRunner Watch App/Views/AttachmentImageView.swift rename to ios/WatchRunner Watch App/Views/ImageViewer.swift index a6b35ef5..717e8fe7 100644 --- a/ios/WatchRunner Watch App/Views/AttachmentImageView.swift +++ b/ios/WatchRunner Watch App/Views/ImageViewer.swift @@ -1,14 +1,7 @@ -// -// AttachmentImageView.swift -// WatchRunner Watch App -// -// Created by LittleSheep on 2025/10/29. -// - import SwiftUI -struct AttachmentImageView: View { - let attachment: SnCloudFile +struct ImageViewer: View { + let imageUrl: URL @EnvironmentObject var appState: AppState @StateObject private var imageLoader = ImageLoader() @@ -20,19 +13,22 @@ struct AttachmentImageView: View { image .resizable() .aspectRatio(contentMode: .fit) - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .scaledToFit() } else if let errorMessage = imageLoader.errorMessage { - Text("Failed to load attachment: \(errorMessage)") + Text("Failed to load image: \(errorMessage)") .font(.caption) .foregroundColor(.red) } else { - Text("File: \(attachment.id)") + Text("Failed to load image.") } } - .task(id: attachment.id) { - if let serverUrl = appState.serverUrl, let imageUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl), let token = appState.token, attachment.mimeType?.starts(with: "image") == true { + .task(id: imageUrl) { + if let token = appState.token { await imageLoader.loadImage(from: imageUrl, token: token) } } + .navigationTitle("Image") + .navigationBarTitleDisplayMode(.inline) } } diff --git a/ios/WatchRunner Watch App/Views/PostViews.swift b/ios/WatchRunner Watch App/Views/PostViews.swift index 8007eccc..275296ab 100644 --- a/ios/WatchRunner Watch App/Views/PostViews.swift +++ b/ios/WatchRunner Watch App/Views/PostViews.swift @@ -116,7 +116,7 @@ struct PostDetailView: View { Divider() Text("Attachments").font(.headline) ForEach(post.attachments) { attachment in - AttachmentImageView(attachment: attachment) + AttachmentView(attachment: attachment) } } diff --git a/ios/WatchRunner Watch App/Views/VideoPlayerView.swift b/ios/WatchRunner Watch App/Views/VideoPlayerView.swift new file mode 100644 index 00000000..f21eec33 --- /dev/null +++ b/ios/WatchRunner Watch App/Views/VideoPlayerView.swift @@ -0,0 +1,12 @@ +import SwiftUI +import AVKit +import AVFoundation + +struct VideoPlayerView: View { + let videoUrl: URL + + var body: some View { + VideoPlayer(player: AVPlayer(url: videoUrl)) + .edgesIgnoringSafeArea(.all) // Make it full screen + } +}