watchOS showing video

This commit is contained in:
2025-10-29 21:44:33 +08:00
parent ad91b17af7
commit fcbd5fe680
5 changed files with 166 additions and 15 deletions

View File

@@ -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)
}
}
}
}
}

View File

@@ -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()
}
}

View File

@@ -1,14 +1,7 @@
//
// AttachmentImageView.swift
// WatchRunner Watch App
//
// Created by LittleSheep on 2025/10/29.
//
import SwiftUI import SwiftUI
struct AttachmentImageView: View { struct ImageViewer: View {
let attachment: SnCloudFile let imageUrl: URL
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@StateObject private var imageLoader = ImageLoader() @StateObject private var imageLoader = ImageLoader()
@@ -20,19 +13,22 @@ struct AttachmentImageView: View {
image image
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.scaledToFit()
} else if let errorMessage = imageLoader.errorMessage { } else if let errorMessage = imageLoader.errorMessage {
Text("Failed to load attachment: \(errorMessage)") Text("Failed to load image: \(errorMessage)")
.font(.caption) .font(.caption)
.foregroundColor(.red) .foregroundColor(.red)
} else { } else {
Text("File: \(attachment.id)") Text("Failed to load image.")
} }
} }
.task(id: attachment.id) { .task(id: imageUrl) {
if let serverUrl = appState.serverUrl, let imageUrl = getAttachmentUrl(for: attachment.id, serverUrl: serverUrl), let token = appState.token, attachment.mimeType?.starts(with: "image") == true { if let token = appState.token {
await imageLoader.loadImage(from: imageUrl, token: token) await imageLoader.loadImage(from: imageUrl, token: token)
} }
} }
.navigationTitle("Image")
.navigationBarTitleDisplayMode(.inline)
} }
} }

View File

@@ -116,7 +116,7 @@ struct PostDetailView: View {
Divider() Divider()
Text("Attachments").font(.headline) Text("Attachments").font(.headline)
ForEach(post.attachments) { attachment in ForEach(post.attachments) { attachment in
AttachmentImageView(attachment: attachment) AttachmentView(attachment: attachment)
} }
} }

View File

@@ -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
}
}