Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions iOSClient/Albums/Presentation/Details/AlbumDetailsScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,10 @@ import SwiftUI

struct AlbumDetailsScreen: View {
private unowned let controller: NCMainTabBarController
private let album: Album
@StateObject private var viewModel: AlbumDetailsViewModel
@State private var showMedia = false

init(
controller: NCMainTabBarController,
album: Album,
navigator: AlbumsNavigator = AlbumsNavigator()
) {
init(controller: NCMainTabBarController, album: Album, navigator: AlbumsNavigator = AlbumsNavigator()) {
self.controller = controller
self.album = album
_viewModel = StateObject(
wrappedValue: AlbumDetailsViewModel(controller: controller, album: album, navigator: navigator)
)
Expand Down Expand Up @@ -142,8 +135,7 @@ struct AlbumDetailsScreen: View {
PhotosGridView(
controller: controller,
photos: viewModel.photos,
onAddPhotosIntent: handleAddPhotosIntent,
album: album,
albumTitle: viewModel.screenTitle,
onRemovePhoto: { photo in
Task { @MainActor in
await viewModel.removePhoto(photo)
Expand All @@ -158,6 +150,5 @@ struct AlbumDetailsScreen: View {

private func handleAddPhotosIntent() {
viewModel.onAddPhotosIntent()
showMedia = true
}
}
80 changes: 42 additions & 38 deletions iOSClient/Albums/Presentation/Details/PhotoGridItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import NextcloudKit
struct PhotoGridItemView: View {
@Environment(\.localAccount) var localAccount: String

let album: Album
let photo: AlbumPhoto
let aspectRatio: CGFloat
let showsMediaTypeIcon: Bool
Expand All @@ -28,12 +27,10 @@ struct PhotoGridItemView: View {
@State private var isLoading = false

init(
album: Album,
photo: AlbumPhoto,
aspectRatio: CGFloat = 1,
showsMediaTypeIcon: Bool = true
) {
self.album = album
self.photo = photo
self.aspectRatio = aspectRatio
self.showsMediaTypeIcon = showsMediaTypeIcon
Expand Down Expand Up @@ -76,57 +73,64 @@ struct PhotoGridItemView: View {
}
}

@MainActor
private func loadThumbnailFromPhoto() async {
// Clear the previous image before validating the new photo. Otherwise a
// reused cover view could keep showing a removed photo with no replacement.
await MainActor.run {
self.thumbnail = nil
self.isLoading = metadata.hasPreview && !photo.id.isEmpty
}
thumbnail = nil
isLoading = !photo.id.isEmpty
defer { isLoading = false }

// 1. Validate: Only load if it has a preview and a valid ID
guard metadata.hasPreview, !photo.id.isEmpty else {
guard !photo.id.isEmpty else {
return
}

// 2. Setup parameters from Photo object and Metadata fallback
let image = await Self.loadPreview(for: photo, account: localAccount)
guard !Task.isCancelled else { return }
thumbnail = image
}

@MainActor
static func loadPreview(for photo: AlbumPhoto, account: String) async -> UIImage? {
guard !Task.isCancelled, !photo.id.isEmpty else { return nil }

let metadata = photo.metadata
let fileId = photo.id
let ocId = metadata.ocId
let userId = metadata.userId
let urlBase = metadata.urlBase
let etag = metadata.etag
let previewExt = NCGlobal.shared.previewExt512
let utility = NCUtility()

// 3. Try Disk Cache First
if let cachedImage = NCUtility().getImage(
ocId: fileId,
etag: etag,
ext: NCGlobal.shared.previewExt512,
userId: userId,
urlBase: urlBase
) {
await MainActor.run {
self.thumbnail = cachedImage
self.isLoading = false
}
return
if let cachedImage = utility.getImage(ocId: ocId, etag: etag, ext: previewExt, userId: userId, urlBase: urlBase) {
return cachedImage
}

// 4. Download Preview
let results = await NextcloudKit.shared.downloadPreviewAsync(fileId: fileId, etag: etag, account: localAccount) { _ in }

await MainActor.run {
if results.error == .success,
let data = results.responseData?.data,
let image = UIImage(data: data) {
self.thumbnail = image
guard metadata.hasPreview else { return nil }

// 5. Save to cache (optional but recommended)
Task.detached(priority: .background) {
NCUtility().createImageFileFrom(
data: data, ocId: fileId, etag: etag, userId: userId, urlBase: urlBase
)
}
let results = await NextcloudKit.shared.downloadPreviewAsync(fileId: fileId, etag: etag, account: account) { task in
Task {
let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(
account: account,
path: fileId,
name: "DownloadPreview"
)
await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task)
}
self.isLoading = false
}
guard !Task.isCancelled else { return nil }

guard results.error == .success,
let data = results.responseData?.data,
let image = UIImage(data: data) else {
return nil
}

Task.detached(priority: .background) {
NCUtility().createImageFileFrom(data: data, ocId: ocId, etag: etag, ext: previewExt, userId: userId, urlBase: urlBase)
}

return image
}
}
33 changes: 10 additions & 23 deletions iOSClient/Albums/Presentation/Details/PhotosGridView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ struct PhotosGridView: View {
private unowned let controller: NCMainTabBarController

let photos: [AlbumPhoto]
let onAddPhotosIntent: () -> Void
let album: Album
let albumTitle: String
let onRemovePhoto: (AlbumPhoto) -> Void

@State private var photoToRemove: AlbumPhoto?
Expand All @@ -20,14 +19,12 @@ struct PhotosGridView: View {
init(
controller: NCMainTabBarController,
photos: [AlbumPhoto],
onAddPhotosIntent: @escaping () -> Void,
album: Album,
albumTitle: String,
onRemovePhoto: @escaping (AlbumPhoto) -> Void
) {
self.controller = controller
self.photos = photos
self.onAddPhotosIntent = onAddPhotosIntent
self.album = album
self.albumTitle = albumTitle
self.onRemovePhoto = onRemovePhoto
}

Expand All @@ -43,21 +40,12 @@ struct PhotosGridView: View {
}

private var coverPhoto: AlbumPhoto? {
let serverCover = photos.first { $0.id == album.lastPhotoId }
if serverCover?.metadata.hasPreview == true {
return serverCover
}

let mostRecentWithPreview = photos
.filter(\.metadata.hasPreview)
.reduce(nil as AlbumPhoto?) { newest, photo in
guard let newest else { return photo }
return photo.metadata.date.compare(newest.metadata.date as Date) == .orderedDescending
? photo
: newest
photos.filter(\.metadata.hasPreview).max {
if $0.metadata.date != $1.metadata.date {
return $0.metadata.date.compare($1.metadata.date as Date) == .orderedAscending
}

return mostRecentWithPreview ?? serverCover
return $0.id < $1.id
}
}

var body: some View {
Expand All @@ -70,7 +58,6 @@ struct PhotosGridView: View {
} label: {
ZStack(alignment: .bottomLeading) {
PhotoGridItemView(
album: album,
photo: coverPhoto,
aspectRatio: 16.0 / 7.0,
showsMediaTypeIcon: false
Expand All @@ -84,7 +71,7 @@ struct PhotosGridView: View {
)

VStack(alignment: .leading, spacing: 4) {
Text(album.name)
Text(albumTitle)
.font(.title2.bold())

Text(
Expand All @@ -108,7 +95,7 @@ struct PhotosGridView: View {
Button {
openingPhoto = photo
} label: {
PhotoGridItemView(album: album, photo: photo)
PhotoGridItemView(photo: photo)
}
.disabled(openingPhoto != nil)
.contextMenu {
Expand Down
74 changes: 3 additions & 71 deletions iOSClient/Albums/Presentation/List/AddToAlbumsListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,13 @@ struct AddToAlbumsListView: View {

struct AlbumRow: View {
let album: Album
private enum ImageState { case loading, empty, thumbnail(UIImage) }
@State private var imageState: ImageState = .loading
var localAccount: String

var body: some View {
HStack {
thumbnailView()
.frame(width: 80, height: 60)
AlbumGridItemView(album: album)
.environment(\.localAccount, localAccount)
.frame(width: 60, height: 60)
.cornerRadius(6)

VStack(alignment: .leading, spacing: 2) {
Expand All @@ -161,9 +160,6 @@ struct AlbumRow: View {
}
}
.padding(.horizontal, 8)
.task(id: album.lastPhotoId) {
await loadThumbnail()
}
}

private func makeSubtitle(for album: Album) -> String? {
Expand All @@ -184,68 +180,4 @@ struct AlbumRow: View {
}
return parts.joined(separator: " - ")
}

/// Renders the thumbnail image based on the current state
@ViewBuilder
private func thumbnailView() -> some View {
switch imageState {
case .loading:
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray.opacity(0.1))
case .empty:
Image(systemName: "photo.stack.fill")
.resizable()
.scaledToFit()
.foregroundStyle(
Color(NCBrandColor.shared.getElement(account: localAccount))
)
.frame(maxWidth: .infinity, maxHeight: 180)
case .thumbnail(let uiImage):
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.clipped()
}
}

private func loadThumbnail() async {
if album.lastPhotoId == "-1" || (album.itemCount ?? 0) == 0 {
imageState = .empty
return
}
guard let photoId = album.lastPhotoId else {
imageState = .empty
return
}

Task {
let resultsPreview = await NextcloudKit.shared.downloadPreviewAsync(fileId: photoId, etag: "", account: localAccount) { task in
Task {
let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(
account: localAccount,
path: photoId,
name: "DownloadPreview")
await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task)
}
}
if resultsPreview.error == .success, let data = resultsPreview.responseData?.data {
let session = NCSession.shared.getSession(account: localAccount)
if let image = NCUtility().createImageFileFrom(
data: data,
ocId: photoId,
etag: "",
ext: NCGlobal.shared.previewExt512,
userId: session.userId,
urlBase: session.urlBase
) {
Task { @MainActor in
await MainActor.run { imageState = .thumbnail(image) }
}
} else {
await MainActor.run { imageState = .empty }
}
}
}
}
}
Loading
Loading