From 9cb1bc29cac635abe1e9d05c432b2a31c7e2a792 Mon Sep 17 00:00:00 2001 From: Robin Kutesa Date: Mon, 18 May 2026 23:33:47 +0300 Subject: [PATCH] feat: add screen recordings filter, exclude by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screen recordings are videos captured by iOS screen recording and are almost never wanted in a photo backup. They are now excluded from all counts and backup queues unless explicitly enabled. Changes: - BackupFilter.includeScreenRecordings (default: false) added - PHAssetMediaSubtype.videoScreenRecording predicate applied in both fetchAssets() and fetchResultForReconciliation() — excluded at the PHFetchRequest level before enumeration - LocalPhotoIndex.Record.isScreenRecording populated via makeRecord() so the fast-path countSafe/count also respects the toggle - LocalPhotoIndex.passes() checks includeScreenRecordings - Settings "Screen Recordings" toggle added below Screenshots This also resolves the transient count flash: screen recordings were being included in the "need backup" tally on the initial fast-path reconcile, then dropping when the filter was fully applied. Co-Authored-By: Kutesir Co-Authored-By: Sentry --- Core/Protocols/PhotoLibraryProtocol.swift | 2 ++ Features/Settings/SettingsView.swift | 3 +++ Services/LocalPhotoIndex.swift | 6 +++++- Services/PhotoLibraryService.swift | 16 +++++++++++++++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Core/Protocols/PhotoLibraryProtocol.swift b/Core/Protocols/PhotoLibraryProtocol.swift index 0ce1c9a..f029865 100644 --- a/Core/Protocols/PhotoLibraryProtocol.swift +++ b/Core/Protocols/PhotoLibraryProtocol.swift @@ -11,6 +11,7 @@ struct PhotoAsset: Sendable { let duration: TimeInterval let isScreenshot: Bool let isRAW: Bool + let isScreenRecording: Bool } protocol PhotoLibraryProtocol: AnyObject { @@ -26,4 +27,5 @@ struct BackupFilter: Codable, Sendable, Equatable { var includeScreenshots: Bool = false var includeRAW: Bool = false var newFilesOnly: Bool = true + var includeScreenRecordings: Bool = false } diff --git a/Features/Settings/SettingsView.swift b/Features/Settings/SettingsView.swift index 8370fcb..7d0990b 100644 --- a/Features/Settings/SettingsView.swift +++ b/Features/Settings/SettingsView.swift @@ -124,6 +124,9 @@ struct SettingsView: View { ToggleRow(icon: "camera.viewfinder", title: "Screenshots", isOn: $store.backupFilter.includeScreenshots) hairline + ToggleRow(icon: "record.circle", title: "Screen Recordings", + isOn: $store.backupFilter.includeScreenRecordings) + hairline ToggleRow(icon: "rays", title: "RAW", isOn: $store.backupFilter.includeRAW) hairline diff --git a/Services/LocalPhotoIndex.swift b/Services/LocalPhotoIndex.swift index e6d20ac..375f957 100644 --- a/Services/LocalPhotoIndex.swift +++ b/Services/LocalPhotoIndex.swift @@ -20,6 +20,7 @@ actor LocalPhotoIndex { var mediaType: Int // PHAssetMediaType.rawValue var isScreenshot: Bool var isRAW: Bool + var isScreenRecording: Bool = false } private var records: [String: Record] = [:] @@ -118,6 +119,7 @@ actor LocalPhotoIndex { if !filter.includeVideos && record.mediaType == vid { return false } if !filter.includeScreenshots && record.isScreenshot { return false } if !filter.includeRAW && record.isRAW { return false } + if !filter.includeScreenRecordings && record.isScreenRecording { return false } return true } @@ -161,13 +163,15 @@ actor LocalPhotoIndex { ?? "\(asset.localIdentifier.prefix(8)).jpg" let isRAW = resources.contains { $0.type == .alternatePhoto } let isScreenshot = asset.mediaSubtypes.contains(.photoScreenshot) + let isScreenRecording = asset.mediaSubtypes.contains(.videoScreenRecording) return Record( localIdentifier: asset.localIdentifier, filename: filename, creationDate: asset.creationDate, mediaType: asset.mediaType.rawValue, isScreenshot: isScreenshot, - isRAW: isRAW + isRAW: isRAW, + isScreenRecording: isScreenRecording ) } } diff --git a/Services/PhotoLibraryService.swift b/Services/PhotoLibraryService.swift index 8408e4d..902bcb8 100644 --- a/Services/PhotoLibraryService.swift +++ b/Services/PhotoLibraryService.swift @@ -34,6 +34,12 @@ final class PhotoLibraryService: PhotoLibraryProtocol { PHAssetMediaSubtype.photoScreenshot.rawValue )) } + if !filter.includeScreenRecordings { + subpredicates.append(NSPredicate( + format: "NOT ((mediaSubtype & %d) != 0)", + PHAssetMediaSubtype.videoScreenRecording.rawValue + )) + } if subpredicates.count > 1 { options.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates) @@ -46,6 +52,7 @@ final class PhotoLibraryService: PhotoLibraryProtocol { result.enumerateObjects { asset, _, _ in autoreleasepool { let isScreenshot = asset.mediaSubtypes.contains(.photoScreenshot) + let isScreenRecording = asset.mediaSubtypes.contains(.videoScreenRecording) let resources = PHAssetResource.assetResources(for: asset) let isRAW = resources.contains { $0.type == .alternatePhoto } if isRAW && !filter.includeRAW { return } @@ -61,7 +68,8 @@ final class PhotoLibraryService: PhotoLibraryProtocol { pixelHeight: asset.pixelHeight, duration: asset.duration, isScreenshot: isScreenshot, - isRAW: isRAW + isRAW: isRAW, + isScreenRecording: isScreenRecording )) } } @@ -91,6 +99,12 @@ final class PhotoLibraryService: PhotoLibraryProtocol { PHAssetMediaSubtype.photoScreenshot.rawValue )) } + if !filter.includeScreenRecordings { + subpredicates.append(NSPredicate( + format: "NOT ((mediaSubtype & %d) != 0)", + PHAssetMediaSubtype.videoScreenRecording.rawValue + )) + } if subpredicates.count > 1 { options.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates)