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
8 changes: 4 additions & 4 deletions Nextcloud.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -6618,7 +6618,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 7;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = NKUJUXUJ3B;
Expand Down Expand Up @@ -6686,7 +6686,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 7;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = NKUJUXUJ3B;
Expand Down Expand Up @@ -6955,8 +6955,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/nextcloud/NextcloudKit";
requirement = {
branch = "unified-sharing";
kind = branch;
kind = exactVersion;
version = 7.6.0;
};
};
F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 6 additions & 7 deletions iOSClient/Albums/API/Albums+WebDAV.swift
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,11 @@ public extension NextcloudKit {

// MARK: - Delete Photo from Album

/// Asynchronously deletes a Photo FromAlbum from the Nextcloud server.
/// Asynchronously removes a photo from an album without deleting the original file.
///
/// - Parameters:
/// - serverUrlFileName: The full URL string of the file or folder to delete.
/// - albumName: The name of the album containing the photo.
/// - fileName: The album entry name returned by WebDAV, including its file ID prefix.
/// - account: The Nextcloud account identifier.
/// - options: Optional request options including headers, timeout, and queue.
/// - taskHandler: Callback triggered with the underlying `URLSessionTask`.
Expand All @@ -481,7 +482,6 @@ public extension NextcloudKit {
/// - error: The `NKError` result indicating success or failure.
func deletePhotoFromAlbumAsync(albumName: String,
fileName: String,
serverUrlFileName: String,
account: String,
options: NKRequestOptions = NKRequestOptions(),
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }
Expand All @@ -493,7 +493,6 @@ public extension NextcloudKit {
await withCheckedContinuation { continuation in
deletePhotoFromAlbum(albumName: albumName,
fileName: fileName,
serverUrlFileName: serverUrlFileName,
account: account,
options: options,
taskHandler: taskHandler) { account, responseData, error in
Expand All @@ -506,10 +505,11 @@ public extension NextcloudKit {
}
}

/// Deletes a Photo From Album from the Nextcloud server at the specified URL.
/// Removes a photo from an album without deleting the original file.
///
/// - Parameters:
/// - serverUrlFileName: The full URL string of the file or folder to delete.
/// - albumName: The name of the album containing the photo.
/// - fileName: The album entry name returned by WebDAV, including its file ID prefix.
/// - account: The Nextcloud account identifier.
/// - options: Optional request options including headers, timeout, and queue.
/// - taskHandler: Callback triggered with the underlying `URLSessionTask`.
Expand All @@ -519,7 +519,6 @@ public extension NextcloudKit {
/// - error: The `NKError` result indicating success or failure.
func deletePhotoFromAlbum(albumName: String,
fileName: String,
serverUrlFileName: String,
account: String,
options: NKRequestOptions = NKRequestOptions(),
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,12 @@ struct AlbumDetailsScreen: View {
localAccount: viewModel.account,
photos: viewModel.photos,
onAddPhotosIntent: handleAddPhotosIntent,
album: album
album: album,
onRemovePhoto: { photo in
Task { @MainActor in
await viewModel.removePhoto(photo)
}
}
)
.refreshable {
viewModel.onPulledToRefresh()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,29 @@ class AlbumDetailsViewModel: ObservableObject {
}
}

@MainActor
func removePhoto(_ photo: AlbumPhoto) async {
guard !isLoadingPopupVisible, photos.keys.contains(photo) else { return }

isLoadingPopupVisible = true
defer { isLoadingPopupVisible = false }

// Remove the album entry, including when the original file has no local metadata.
let result = await NextcloudKit.shared.deletePhotoFromAlbumAsync(
albumName: album.name,
fileName: photo.fileName,
account: account
)

guard result.error == .success else {
await showErrorBanner(windowScene: windowScene, error: result.error)
return
}

photos.removeValue(forKey: photo)
AlbumsManager.shared.syncAlbums()
}

@MainActor func deletePhotos(with metadatas: [tableMetadata]) async {
for metadata in metadatas {
if let photo = photos.first(where: { $0.value?.ocId == metadata.ocId })?.key {
Expand Down Expand Up @@ -209,7 +232,7 @@ class AlbumDetailsViewModel: ObservableObject {
let fileName: String = photo.fileName
print("DEBUG: Attempting to remove: \(fileName)")

let results = await NextcloudKit.shared.deletePhotoFromAlbumAsync(albumName: album.name, fileName: fileName, serverUrlFileName: metadata.serverUrlFileName, account: metadata.account) { task in
let results = await NextcloudKit.shared.deletePhotoFromAlbumAsync(albumName: album.name, fileName: fileName, account: metadata.account) { task in
Task {
let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: metadata.account,
path: metadata.serverUrlFileName,
Expand Down
25 changes: 25 additions & 0 deletions iOSClient/Albums/Presentation/Details/PhotosGridView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ struct PhotosGridView: View {
let photos: [AlbumPhoto: tableMetadata?]
let onAddPhotosIntent: () -> Void
let album: Album
let onRemovePhoto: (AlbumPhoto) -> Void

@State private var photoToRemove: AlbumPhoto?

private var columns: [GridItem] {
if UIDevice.current.userInterfaceIdiom == .pad {
Expand Down Expand Up @@ -42,9 +45,31 @@ struct PhotosGridView: View {
iconSize: calculatedIconSize
)
}
.contextMenu {
Button(role: .destructive) {
photoToRemove = photo
} label: {
Label(NSLocalizedString("_remove_from_album_", comment: ""), systemImage: "minus.circle")
}
}
}
}
}
.alert(
NSLocalizedString("_remove_from_album_", comment: ""),
isPresented: Binding(
get: { photoToRemove != nil },
set: { if !$0 { photoToRemove = nil } }
),
presenting: photoToRemove
) { photo in
Button(NSLocalizedString("_remove_from_album_", comment: ""), role: .destructive) {
onRemovePhoto(photo)
}
Button(NSLocalizedString("_cancel_", comment: ""), role: .cancel) {}
} message: { _ in
Text(NSLocalizedString("_want_remove_from_album_", comment: ""))
}
}

@MainActor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,16 @@ struct NCMediaViewRepresentable: UIViewControllerRepresentable {
viewIfLoaded?.window?.windowScene
}

override var allowsSearchWhileSelecting: Bool { true }

override var isMediaPresentationActive: Bool { isViewActived }

override func viewDidLoad() {
super.viewDidLoad()
// Keep Media's selection bookkeeping without its share/move/delete toolbar.
tabBarSelect = NCMediaSelectTabBar(viewController: self)
collectionView.dragInteractionEnabled = false
collectionView.isUserInteractionEnabled = false
isEditMode = true
}

override func viewWillAppear(_ animated: Bool) {
Expand All @@ -80,22 +84,15 @@ struct NCMediaViewRepresentable: UIViewControllerRepresentable {
prepareTask = Task { @MainActor [weak self] in
guard let self else { return }
defer { self.prepareTask = nil }

// This controller is presented in a sheet, not as the selected Media tab.
await self.loadDataSource(forced: true)
guard !Task.isCancelled else { return }
// The normal search API requires an attached view outside edit mode.
await self.searchMediaTask?.value
guard !Task.isCancelled else { return }
await self.searchMediaUI(true)
guard !Task.isCancelled else { return }
// Search updates the database; its normal reload also requires the Media tab.
await self.loadDataSource(forced: true)
guard !Task.isCancelled else { return }
self.collectionViewReloadData()
self.isEditMode = true
self.selectionReady = true
self.collectionView.isUserInteractionEnabled = true
self.onReady?()

self.searchNewMedia()
}
}

Expand Down
7 changes: 7 additions & 0 deletions iOSClient/Media/NCMedia.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ class NCMedia: UIViewController {
return self.isViewLoaded && self.view.window != nil
}

// Album selection presents Media in a sheet and keeps searching while selecting.
var allowsSearchWhileSelecting: Bool { false }

var isMediaPresentationActive: Bool {
isViewActived && tabBarController?.selectedViewController === navigationController
}

var isPinchGestureActive: Bool {
return pinchGesture.state == .began || pinchGesture.state == .changed
}
Expand Down
6 changes: 3 additions & 3 deletions iOSClient/Media/NCMediaDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
self.isViewActived &&
self.session.account == account &&
self.view.window != nil &&
self.tabBarController?.selectedViewController === self.navigationController
self.isMediaPresentationActive
)
}

Expand All @@ -91,7 +91,7 @@
self.isViewActived &&
self.session.account == account &&
self.view.window != nil &&
self.tabBarController?.selectedViewController === self.navigationController
self.isMediaPresentationActive
) else {
return
}
Expand Down Expand Up @@ -195,12 +195,12 @@

func searchMediaUI(_ distant: Bool = false) async {
let shouldContinue = await MainActor.run { () -> Bool in
guard self.isViewActived,

Check warning on line 198 in iOSClient/Media/NCMediaDataSource.swift

View workflow job for this annotation

GitHub Actions / Lint

Control Statement Violation: `if`, `for`, `guard`, `switch`, `while`, and `catch` statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses (control_statement)
!self.searchMediaInProgress,
!self.isPinchGestureActive,
!self.showOnlyImages,
!self.showOnlyVideos,
!self.isEditMode else {
(!self.isEditMode || self.allowsSearchWhileSelecting) else {
return false
}

Expand Down
Loading