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
2 changes: 1 addition & 1 deletion Brand/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ import Foundation
//
let databaseName = "nextcloud.realm"
let tableAccountBackup = "tableAccountBackup.json"
let databaseSchemaVersion: UInt64 = 408
let databaseSchemaVersion: UInt64 = 409
2 changes: 2 additions & 0 deletions Brand/NCBrand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,14 @@ final class NCBrandColor: @unchecked Sendable {
return false
}

/*
public func getTheming(account: String?) -> UIColor {
if let account, let color = self.themingColor[account] {
return color
}
return customer
}
*/

public func getElement(account: String?) -> UIColor {
if let account, let color = self.themingColorElement[account] {
Expand Down
2 changes: 1 addition & 1 deletion Nextcloud.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -6129,7 +6129,7 @@
repositoryURL = "https://github.com/nextcloud/NextcloudKit";
requirement = {
kind = exactVersion;
version = 7.2.8;
version = 7.2.9;
};
};
F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */ = {
Expand Down
152 changes: 130 additions & 22 deletions iOSClient/Data/NCManageDatabase+Trash.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,41 @@ import UIKit
import RealmSwift
import NextcloudKit

class tableTrash: Object {
@objc dynamic var account = ""
@objc dynamic var classFile = ""
@objc dynamic var contentType = ""
@objc dynamic var date = NSDate()
@objc dynamic var directory: Bool = false
@objc dynamic var fileId = ""
@objc dynamic var fileName = ""
@objc dynamic var filePath = ""
@objc dynamic var hasPreview: Bool = false
@objc dynamic var iconName = ""
@objc dynamic var size: Int64 = 0
@objc dynamic var trashbinFileName = ""
@objc dynamic var trashbinOriginalLocation = ""
@objc dynamic var trashbinDeletionTime = NSDate()

override static func primaryKey() -> String {
return "fileId"
}
/// Represents a trash item stored in Realm.
///
/// Each object corresponds to a file or folder in the Nextcloud trashbin,
/// associated with a specific account.
///
/// The `identifier` is used as primary key and is built from:
/// `account + "|" + fileName`, where `fileName` includes the `.dXXXXX` suffix,
/// making each item unique.
///
/// - `fileName`: name of the file in trash (includes `.dXXXXX`)
/// - `trashbinFileName`: original file name before deletion
/// - `trashbinOriginalLocation`: original path before deletion
/// - `classFile`: type of file (e.g. "image", "video", "document")
///
/// This model replaces the legacy `tableTrash` schema.
typealias tableTrash = tableTrashV2
class tableTrashV2: Object {
// Primary key: unique per account + trash item
@Persisted(primaryKey: true) var identifier: String

@Persisted var account: String = ""
@Persisted var classFile: String = ""
@Persisted var contentType: String = ""
@Persisted var date: Date = Date()
@Persisted var directory: Bool = false
@Persisted var fileId: String = ""
@Persisted var fileName: String = ""
@Persisted var filePath: String = ""
@Persisted var hasPreview: Bool = false
@Persisted var iconName: String = ""
@Persisted var size: Int64 = 0
@Persisted var livePhoto: Bool = false
@Persisted var trashbinFileName: String = ""
@Persisted var trashbinOriginalLocation: String = ""
@Persisted var trashbinDeletionTime: Date = Date()
}

extension NCManageDatabase {
Expand All @@ -38,23 +54,35 @@ extension NCManageDatabase {
/// - account: The account string used to associate each trash item.
/// - items: An array of `NKTrash` items to be added to the database.
func addTrashAsync(items: [NKTrash], account: String) async {
let itemsFiltered = filterOutVideosMatchingImages(items)

await core.performRealmWriteAsync { realm in
items.forEach { trash in

// Delete all existing trash items for this account.
let existingItems = realm.objects(tableTrash.self)
.where { $0.account == account }
realm.delete(existingItems)

itemsFiltered.forEach { trash in
let object = tableTrash()

object.identifier = "\(account)|\(trash.fileName)"
object.account = account
object.contentType = trash.contentType
object.date = trash.date as NSDate
object.date = trash.date
object.directory = trash.directory
object.fileId = trash.fileId
object.fileName = trash.fileName
object.filePath = trash.filePath
object.hasPreview = trash.hasPreview
object.iconName = trash.iconName
object.size = trash.size
object.trashbinDeletionTime = trash.trashbinDeletionTime as NSDate
object.trashbinDeletionTime = trash.trashbinDeletionTime
object.trashbinFileName = trash.trashbinFileName
object.trashbinOriginalLocation = trash.trashbinOriginalLocation
object.classFile = trash.classFile
object.livePhoto = trash.livePhoto

realm.add(object, update: .all)
}
}
Expand Down Expand Up @@ -141,4 +169,84 @@ extension NCManageDatabase {
.map { tableTrash(value: $0) }
}
}

// MARK: - helpers

/// Filters out video items that have a matching image counterpart based on a shared trash suffix.
///
/// This function is designed to handle Live Photo pairs in the trash, where both the image
/// (e.g. `.jpg`) and the video (e.g. `.mov`) share the same suffix (e.g. `.d123456`).
///
/// The logic works as follows:
/// - Extract the suffix from each trash item file name.
/// - Detect which suffixes contain both an image and a video.
/// - Iterate through all items:
/// - If an item is a video and its suffix is shared with an image, the video is excluded.
/// - If an item is an image and its suffix is shared with a video, the image is kept and
/// marked with `isLivePhoto = true`.
/// - All other items are returned unchanged.
///
/// - Parameter items: An array of `NKTrash` items to process.
/// - Returns: A filtered array where Live Photo videos are removed and matching images are marked as Live Photos.
func filterOutVideosMatchingImages(_ items: [NKTrash]) -> [NKTrash] {
var suffixMap: [String: (hasImage: Bool, hasVideo: Bool)] = [:]

for item in items {
guard let suffix = trashSuffix(from: item.fileName) else {
continue
}
var entry = suffixMap[suffix] ?? (false, false)

if item.classFile == "image" {
entry.hasImage = true
} else if item.classFile == "video" {
entry.hasVideo = true
}

suffixMap[suffix] = entry
}

return items.compactMap { item -> NKTrash? in
guard let suffix = trashSuffix(from: item.fileName) else {
return item
}
let entry = suffixMap[suffix]
let isLive = (entry?.hasImage == true && entry?.hasVideo == true)

if item.classFile == "video" && isLive {
return nil
}

if item.classFile == "image" && isLive {
var copy = item
copy.livePhoto = true
return copy
}

return item
}
}

/// Extracts the suffix component from a trash file name.
///
/// The suffix is defined as the substring after the last dot (`.`) in the file name.
/// This is typically used to identify related files in the trash (e.g., Live Photo pairs),
/// where files share a common suffix such as `d123456`.
///
/// Examples:
/// - `file.jpg.d123456` → `d123456`
/// - `video.mov.d987654` → `d987654`
///
/// If the file name does not contain a dot or the suffix is empty, the function returns `nil`.
///
/// - Parameter fileName: The full file name string.
/// - Returns: The extracted suffix, or `nil` if not available.
func trashSuffix(from fileName: String) -> String? {
guard let lastDot = fileName.lastIndex(of: ".") else {
return nil
}

let suffix = String(fileName[fileName.index(after: lastDot)...])
return suffix.isEmpty ? nil : suffix
}
}
1 change: 1 addition & 0 deletions iOSClient/Data/NCManageDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ final class NCManageDatabase: @unchecked Sendable {
self.clearTable(tableMetadataTag.self)
self.clearTable(tableRecommendedFiles.self)
self.clearTable(tableShare.self)
self.clearTable(tableTrash.self)
}

func clearDatabase(account: String) {
Expand Down
2 changes: 1 addition & 1 deletion iOSClient/GUI/ComponentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ struct ButtonRounded: ButtonStyle {
.padding(.horizontal, 40)
.padding(.vertical, 10)
.background(disabled ? Color(UIColor.placeholderText) : Color(NCBrandColor.shared.getElement(account: account)))
.foregroundColor(disabled ? Color(UIColor.placeholderText) : Color(NCBrandColor.shared.getText(account: account)))
.foregroundColor(disabled ? Color(UIColor.lightGray) : .white)
.clipShape(Capsule())
.opacity(configuration.isPressed ? 0.5 : 1.0)
}
Expand Down
2 changes: 2 additions & 0 deletions iOSClient/Main/Collection Common/Cell/NCCellMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ protocol NCCellMainProtocol {
var localImg: UIImageView? { get set }
var statusImg: UIImageView? { get set }
var infoLbl: UILabel? { get set }

func selected(_ status: Bool, isEditMode: Bool, color: UIColor)
}

extension NCCellMainProtocol {
Expand Down
56 changes: 25 additions & 31 deletions iOSClient/Main/Collection Common/Cell/NCGridCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,23 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainP
imageItem.image = nil
imageItem.layer.cornerRadius = 6
imageItem.layer.masksToBounds = true
imageSelect.isHidden = true
imageSelect.image = NCImageCache.shared.getImageCheckedYes()

imageStatus.image = nil
imageFavorite.image = nil
imageLocal.image = nil

labelTitle.text = ""
labelExtension.text = ""
labelExtension.isHidden = true
labelInfo.text = ""
labelSubinfo.text = ""
iconsStackView.addBlurBackground(style: .systemMaterial)
iconsStackView.layer.cornerRadius = 8
iconsStackView.clipsToBounds = true

imageVisualEffect.isHidden = false
imageVisualEffect.effect = nil
imageVisualEffect.alpha = 0
imageVisualEffect.isUserInteractionEnabled = false
imageVisualEffect.backgroundColor = UIColor.white.withAlphaComponent(0.2)

buttonMore.menu = nil
buttonMore.showsMenuAsPrimaryAction = true

// Dynamic Type Font Configuration
//
Expand Down Expand Up @@ -118,30 +124,22 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainP
// adjustsFontForContentSizeCategory:
// Enables live updates when accessibility settings change.
//
labelTitle.text = ""
labelTitle.font = .callout()
labelTitle.adjustsFontForContentSizeCategory = true

labelExtension.text = ""
labelExtension.isHidden = true
labelExtension.font = .callout()
labelExtension.adjustsFontForContentSizeCategory = true

labelInfo.text = ""
labelInfo.font = .footnote()
labelInfo.adjustsFontForContentSizeCategory = true

labelSubinfo.text = ""
labelSubinfo.font = .footnote()
labelSubinfo.adjustsFontForContentSizeCategory = true

imageVisualEffect.layer.cornerRadius = 6
imageVisualEffect.clipsToBounds = true
imageVisualEffect.alpha = 0.5

iconsStackView.addBlurBackground(style: .systemMaterial)
iconsStackView.layer.cornerRadius = 8
iconsStackView.clipsToBounds = true

buttonMore.menu = nil
buttonMore.showsMenuAsPrimaryAction = true

contentView.bringSubviewToFront(buttonMore)
}

override func snapshotView(afterScreenUpdates afterUpdates: Bool) -> UIView? {
Expand All @@ -164,21 +162,17 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainP
buttonMore.isHidden = status
}

func selected(_ status: Bool, isEditMode: Bool) {
func selected(_ status: Bool, isEditMode: Bool, color: UIColor) {
if isEditMode {
buttonMore.isHidden = true
accessibilityCustomActions = nil
} else {
buttonMore.isHidden = false
}
if status {
imageSelect.isHidden = false
imageSelect.image = NCImageCache.shared.getImageCheckedYes()
imageVisualEffect.isHidden = false
} else {
imageSelect.isHidden = true
imageVisualEffect.isHidden = true
}

imageVisualEffect.alpha = status ? 1 : 0
imageSelect.alpha = status ? 1 : 0
imageSelect.image = NCImageCache.shared.getImageCheckedYes(color: color)
}

func writeInfoDateSize(date: NSDate, size: Int64) {
Expand Down Expand Up @@ -323,10 +317,10 @@ extension NCCollectionViewCommon {

// Edit mode
if fileSelect.contains(metadata.ocId) {
cell.selected(true, isEditMode: isEditMode)
cell.selected(true, isEditMode: isEditMode, color: NCBrandColor.shared.getElement(account: session.account))
a11yValues.append(NSLocalizedString("_selected_", comment: ""))
} else {
cell.selected(false, isEditMode: isEditMode)
cell.selected(false, isEditMode: isEditMode, color: NCBrandColor.shared.getElement(account: session.account))
}

// Accessibility
Expand Down
Loading
Loading