Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
25d8e85
Refactor media viewer paging and playback architecture
marinofaggiana May 22, 2026
af48bc0
Refactor media viewer comments
marinofaggiana May 22, 2026
2a0175e
Clean up media viewer helpers
marinofaggiana May 22, 2026
7ca1519
Refactor the media viewer paging and playback architecture.
marinofaggiana May 23, 2026
bd8fe9b
Remove video playback fallback timeout
marinofaggiana May 24, 2026
7587bfb
Update NCVideoPlaybackController.swift
marinofaggiana May 24, 2026
ecd5053
cleaning
marinofaggiana May 24, 2026
c566818
Isolate video progress control area
marinofaggiana May 25, 2026
6166ec7
Guard paging index updates during layout changes
marinofaggiana May 25, 2026
e5a3951
Reduce media viewer loader logs
marinofaggiana May 25, 2026
c36a120
Remove media viewer failed download overlay
marinofaggiana May 25, 2026
c7539d0
cleaning messagge internal log
marinofaggiana May 25, 2026
9fc1b8a
lint
marinofaggiana May 25, 2026
62d886f
Enable VisionKit only for full images
marinofaggiana May 25, 2026
ece7a76
Pass audio preview to audio viewer
marinofaggiana May 25, 2026
8e49e08
Avoid standalone preview for audio pages
marinofaggiana May 25, 2026
6242678
Audio GUI improvements
marinofaggiana May 25, 2026
d311864
cleaning code
marinofaggiana May 26, 2026
e90892d
Add fullscreen video transition overlay
marinofaggiana May 26, 2026
a2332e8
Reduce media loader optional-path logs
marinofaggiana May 26, 2026
c617bcc
Remove unused media viewer loader error
marinofaggiana May 26, 2026
1b0875e
cleaning
marinofaggiana May 26, 2026
7f2e30b
Lighten media detail value styling
marinofaggiana May 27, 2026
4ebe158
Merge remote-tracking branch 'origin/master' into MediaViewer
marinofaggiana May 27, 2026
b505b24
Fix cached video routing and clean media viewer details
marinofaggiana May 27, 2026
4ad6341
Preserve local URL during video prefetch
marinofaggiana May 27, 2026
055dcdc
Hide video resolver errors from logs and UI
marinofaggiana May 27, 2026
86db0eb
Clean video playback VLC fallback
marinofaggiana May 27, 2026
fc8d5db
source improvements
marinofaggiana May 27, 2026
b9cbaf7
cleaning
marinofaggiana May 27, 2026
0a26743
rename class
marinofaggiana May 27, 2026
a0abfc5
cleaning source
marinofaggiana May 27, 2026
8c60add
Fix AVPlayer controls after PiP return
marinofaggiana May 27, 2026
e52c2c4
Protect slider from page swipe gestures
marinofaggiana May 27, 2026
175f0ab
Remove swipe inhibition logic
marinofaggiana May 27, 2026
88822a4
fix
marinofaggiana May 27, 2026
e6da6cd
Improve video controls scrub handling
marinofaggiana May 27, 2026
8282e9c
Clean up unused video controls
marinofaggiana May 27, 2026
02dd9ad
Constrain - close pan gesture filtering
marinofaggiana May 27, 2026
487c1e5
Media viewer: refine video playback cover and chrome-aware background…
marinofaggiana May 29, 2026
8f3fc45
Media viewer: refine video playback cover and chrome-aware background…
marinofaggiana May 29, 2026
d4d3135
Documentation
marinofaggiana May 29, 2026
c4118f3
clean
marinofaggiana May 29, 2026
c1f5c9a
Remove artificial video selection debounce before preparing playback
marinofaggiana May 29, 2026
91ebb8e
fix autolpay button state
marinofaggiana May 29, 2026
497b220
Align VLC play pause button state with requested playback state
marinofaggiana May 29, 2026
a5ac7fb
fix color
marinofaggiana May 29, 2026
4f878a9
fix
marinofaggiana May 29, 2026
10609bc
close fix
marinofaggiana May 29, 2026
3088c67
Video Playback Engine
marinofaggiana May 29, 2026
352643f
Merge remote-tracking branch 'origin/master' into MediaViewer
marinofaggiana May 29, 2026
a00f01e
fix
marinofaggiana May 29, 2026
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
264 changes: 211 additions & 53 deletions Nextcloud.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions iOSClient/Data/NCManageDatabase+Metadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,26 @@ extension NCManageDatabase {
} ?? 0
}

/// Returns only the ocIds that still have a matching metadata row in Realm.
///
/// - Parameter ocIds: Candidate media ocIds used by the media viewer.
/// - Returns: Valid ocIds preserving the original input order.
func getValidMetadataOcIdsAsync(_ ocIds: [String]) async -> [String] {
guard !ocIds.isEmpty else {
return []
}

return await core.performRealmReadAsync { realm in
let existingOcIds = Set(
realm.objects(tableMetadata.self)
.filter("ocId IN %@", ocIds)
.map(\.ocId)
)

return ocIds.filter { existingOcIds.contains($0) }
} ?? []
}

func metadataExistsAsync(predicate: NSPredicate) async -> Bool {
await core.performRealmReadAsync { realm in
realm.objects(tableMetadata.self)
Expand Down
34 changes: 1 addition & 33 deletions iOSClient/Files/NCFiles.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import RealmSwift
import SwiftUI

class NCFiles: NCCollectionViewCommon {
internal var fileNameBlink: String?
internal var lastOffsetY: CGFloat = 0
internal var lastScrollTime: TimeInterval = 0
internal var accumulatedScrollDown: CGFloat = 0
Expand Down Expand Up @@ -107,11 +106,6 @@ class NCFiles: NCCollectionViewCommon {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

if !self.dataSource.isEmpty() {
blinkCell(fileName: self.fileNameBlink)
fileNameBlink = nil
}

Task {
// Plus Menu reload
await self.mainNavigationController?.menuPlus?.create(session: session)
Expand All @@ -132,12 +126,6 @@ class NCFiles: NCCollectionViewCommon {
}
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)

fileNameBlink = nil
}

// MARK: - DataSource

override func reloadDataSource() async {
Expand Down Expand Up @@ -355,31 +343,11 @@ class NCFiles: NCCollectionViewCommon {
return (metadatas, error, reloadRequired)
}

func blinkCell(fileName: String?) {
if let fileName = fileName, let metadata = database.getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", session.account, self.serverUrl, fileName)) {
let indexPath = self.dataSource.getIndexPathMetadata(ocId: metadata.ocId)
if let indexPath = indexPath {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
UIView.animate(withDuration: 0.3) {
self.collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: false)
} completion: { _ in
if let cell = self.collectionView.cellForItem(at: indexPath) {
cell.backgroundColor = .darkGray
UIView.animate(withDuration: 2) {
cell.backgroundColor = .clear
}
}
}
}
}
}
}

func open(metadata: tableMetadata?) async {
guard let metadata else {
return
}
await didSelectMetadata(metadata, withOcIds: false)
await didSelectMetadata(metadata, withOcIds: false, viewerTransitionSource: nil)
}

// MARK: - NCAccountSettingsModelDelegate
Expand Down
12 changes: 12 additions & 0 deletions iOSClient/Main/Collection Common/Cell/NCCellMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ protocol NCCellMainProtocol {
var infoLbl: UILabel? { get set }

func selected(_ status: Bool, isEditMode: Bool, color: UIColor)
func viewerTransitionSource() -> NCMediaViewerTransitionSource?
}

extension NCCellMainProtocol {
Expand All @@ -38,6 +39,17 @@ extension NCCellMainProtocol {
get { return nil }
set {}
}

func viewerTransitionSource() -> NCMediaViewerTransitionSource? {
guard let imageView = previewImg,
let image = imageView.image,
let window = imageView.window else {
return nil
}
let sourceFrame = imageView.convert(imageView.bounds, to: window)

return NCMediaViewerTransitionSource(image: image, sourceFrame: sourceFrame, cornerRadius: imageView.layer.cornerRadius)
}
}

#if !EXTENSION
Expand Down
11 changes: 11 additions & 0 deletions iOSClient/Main/Collection Common/Cell/NCRecommendationsCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ class NCRecommendationsCell: UICollectionViewCell, UIGestureRecognizerDelegate {
}
}

func viewerTransitionSource() -> NCMediaViewerTransitionSource? {
guard let imageView = image,
let image = imageView.image,
let window = imageView.window else {
return nil
}
let sourceFrame = imageView.convert(imageView.bounds, to: window)

return NCMediaViewerTransitionSource(image: image, sourceFrame: sourceFrame, cornerRadius: imageView.layer.cornerRadius)
}

override func awakeFromNib() {
super.awakeFromNib()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ extension NCCollectionViewCommon: NCListCellDelegate, NCGridCellDelegate {
func tapShareListItem(with metadata: tableMetadata?, button: UIButton, sender: Any) {
Task {
guard let metadata else { return }
NCCreate().createShare(controller: self.controller, metadata: metadata, page: .sharing)
NCCreate().createShare(controller: self.controller, viewController: self.controller, metadata: metadata, page: .sharing)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import LucidBanner

extension NCCollectionViewCommon: UICollectionViewDelegate {
@MainActor
func didSelectMetadata(_ metadata: tableMetadata, withOcIds: Bool) async {
func didSelectMetadata(_ metadata: tableMetadata, withOcIds: Bool, viewerTransitionSource: NCMediaViewerTransitionSource?) async {
let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account)

if metadata.e2eEncrypted {
Expand Down Expand Up @@ -94,7 +94,7 @@ extension NCCollectionViewCommon: UICollectionViewDelegate {
// --- E2EE -------
if metadata.isDirectoryE2EE {
if fileExists {
if let vc = await NCViewer().getViewerController(metadata: metadata, delegate: self) {
if let vc = await NCViewer().getViewerController(metadata: metadata, delegate: self, viewerTransitionSource: viewerTransitionSource) {
self.navigationController?.pushViewController(vc, animated: true)
}
} else {
Expand All @@ -110,11 +110,11 @@ extension NCCollectionViewCommon: UICollectionViewDelegate {
$0.classFile == NKTypeClassFile.video.rawValue ||
$0.classFile == NKTypeClassFile.audio.rawValue }.map(\.ocId)

if let vc = await NCViewer().getViewerController(metadata: metadata, ocIds: withOcIds ? ocIds : nil, image: image, delegate: self) {
if let vc = await NCViewer().getViewerController(metadata: metadata, ocIds: withOcIds ? ocIds : nil, image: image, delegate: self, viewerTransitionSource: viewerTransitionSource) {
self.navigationController?.pushViewController(vc, animated: true)
}
} else if !metadata.isDirectoryE2EE, metadata.isAvailableEditorView || utilityFileSystem.fileProviderStorageExists(metadata) || metadata.name == self.global.talkName {
if let vc = await NCViewer().getViewerController(metadata: metadata, image: image, delegate: self) {
if let vc = await NCViewer().getViewerController(metadata: metadata, image: image, delegate: self, viewerTransitionSource: viewerTransitionSource) {
self.navigationController?.pushViewController(vc, animated: true)
}
} else if NextcloudKit.shared.isNetworkReachable() {
Expand All @@ -128,7 +128,7 @@ extension NCCollectionViewCommon: UICollectionViewDelegate {
if metadata.name == "files" {
await downloadFile()
} else if !metadata.url.isEmpty,
let vc = await NCViewer().getViewerController(metadata: metadata, delegate: self) {
let vc = await NCViewer().getViewerController(metadata: metadata, delegate: self, viewerTransitionSource: viewerTransitionSource) {
self.navigationController?.pushViewController(vc, animated: true)
}
} else {
Expand All @@ -141,6 +141,7 @@ extension NCCollectionViewCommon: UICollectionViewDelegate {
guard let metadata = self.dataSource.getMetadata(indexPath: indexPath) else {
return
}
var viewerTransitionSource: NCMediaViewerTransitionSource?

if self.isEditMode {
if let index = self.fileSelect.firstIndex(of: metadata.ocId) {
Expand All @@ -154,8 +155,12 @@ extension NCCollectionViewCommon: UICollectionViewDelegate {
return
}

if let cell = collectionView.cellForItem(at: indexPath) as? NCCellMainProtocol {
viewerTransitionSource = cell.viewerTransitionSource()
}

Task {
await didSelectMetadata(metadata, withOcIds: true)
await didSelectMetadata(metadata, withOcIds: true, viewerTransitionSource: viewerTransitionSource)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Marino Faggiana
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation
import UIKit

extension NCCollectionViewCommon {
/// Returns the transition source for a media item in the collection view.
///
/// If the target cell is visible, the transition uses the real preview image view frame.
/// If the target cell is not materialized yet, the transition falls back to the
/// collection view layout attributes so the closing animation can still target
/// the correct item position.
///
/// - Parameter ocId: Nextcloud file identifier of the media item.
/// - Returns: Transition source if the item can be resolved.
func viewerTransitionSource(for ocId: String) -> NCMediaViewerTransitionSource? {
guard let indexPath = dataSource.getIndexPathMetadata(ocId: ocId),
let window = collectionView.window else {
return nil
}

collectionView.layoutIfNeeded()

if collectionView.cellForItem(at: indexPath) == nil {
collectionView.scrollToItem(
at: indexPath,
at: .centeredVertically,
animated: false
)

collectionView.layoutIfNeeded()
}

if let cell = collectionView.cellForItem(at: indexPath) as? NCCellMainProtocol,
let imageView = cell.previewImg,
let image = imageView.image {
let sourceFrame = imageView.convert(
imageView.bounds,
to: window
)

return NCMediaViewerTransitionSource(
image: image,
sourceFrame: sourceFrame,
cornerRadius: imageView.layer.cornerRadius
)
}

guard let attributes = collectionView.layoutAttributesForItem(at: indexPath) else {
return nil
}

let sourceFrame = collectionView.convert(
attributes.frame,
to: window
)

return NCMediaViewerTransitionSource(
image: UIImage(),
sourceFrame: sourceFrame,
cornerRadius: 6
)
}

/// Briefly highlights the collection view cell associated with the given ocId.
///
/// If the target item is not currently visible, the collection view scrolls to it first.
/// The highlight is intentionally lightweight and temporary.
@MainActor
func blinkItem(ocId: String) {
guard let indexPath = dataSource.getIndexPathMetadata(ocId: ocId) else {
return
}

collectionView.layoutIfNeeded()

if collectionView.cellForItem(at: indexPath) == nil {
collectionView.scrollToItem(
at: indexPath,
at: .centeredVertically,
animated: false
)

view.layoutIfNeeded()
collectionView.layoutIfNeeded()
}

guard let cell = collectionView.cellForItem(at: indexPath) else {
return
}

blink(view: cell.contentView)
}

/// Applies a short blink animation to the provided view.
///
/// - Parameter view: View that should be visually highlighted.
private func blink(view: UIView) {
let overlay = UIView(frame: view.bounds)
overlay.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.22)
overlay.layer.cornerRadius = view.layer.cornerRadius
overlay.isUserInteractionEnabled = false
overlay.autoresizingMask = [
.flexibleWidth,
.flexibleHeight
]

view.addSubview(overlay)

UIView.animate(
withDuration: 0.4,
delay: 0,
options: [.curveEaseInOut]
) {
overlay.alpha = 0.0
} completion: { _ in
overlay.removeFromSuperview()
}
}

}
4 changes: 2 additions & 2 deletions iOSClient/Main/Collection Common/NCCollectionViewCommon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -787,9 +787,9 @@ extension NCCollectionViewCommon: NCSectionFirstHeaderDelegate {
}
}

func tapRecommendations(with metadata: tableMetadata) {
func tapRecommendations(with metadata: tableMetadata, viewerTransitionSource: NCMediaViewerTransitionSource?) {
Task {
await didSelectMetadata(metadata, withOcIds: false)
await didSelectMetadata(metadata, withOcIds: false, viewerTransitionSource: viewerTransitionSource)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import NextcloudKit

protocol NCSectionFirstHeaderDelegate: AnyObject {
func tapRichWorkspace(_ sender: Any)
func tapRecommendations(with metadata: tableMetadata)
func tapRecommendations(with metadata: tableMetadata, viewerTransitionSource: NCMediaViewerTransitionSource?)
}

class NCSectionFirstHeader: UICollectionReusableView, UIGestureRecognizerDelegate {
Expand Down Expand Up @@ -232,11 +232,13 @@ extension NCSectionFirstHeader: UICollectionViewDataSource {
extension NCSectionFirstHeader: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let recommendedFiles = self.recommendations[indexPath.row]
guard let metadata = NCManageDatabase.shared.getMetadataFromFileId(recommendedFiles.id) else {
guard let metadata = NCManageDatabase.shared.getMetadataFromFileId(recommendedFiles.id),
let cell = collectionView.cellForItem(at: indexPath) as? NCRecommendationsCell else {
return
}
let viewerTransitionSource = cell.viewerTransitionSource()

self.delegate?.tapRecommendations(with: metadata)
self.delegate?.tapRecommendations(with: metadata, viewerTransitionSource: viewerTransitionSource)
}

func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
Expand Down
Loading
Loading