feat: add screen recordings filter, exclude by default
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 <kutesir@provoc.ug> Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
@@ -11,6 +11,7 @@ struct PhotoAsset: Sendable {
|
|||||||
let duration: TimeInterval
|
let duration: TimeInterval
|
||||||
let isScreenshot: Bool
|
let isScreenshot: Bool
|
||||||
let isRAW: Bool
|
let isRAW: Bool
|
||||||
|
let isScreenRecording: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol PhotoLibraryProtocol: AnyObject {
|
protocol PhotoLibraryProtocol: AnyObject {
|
||||||
@@ -26,4 +27,5 @@ struct BackupFilter: Codable, Sendable, Equatable {
|
|||||||
var includeScreenshots: Bool = false
|
var includeScreenshots: Bool = false
|
||||||
var includeRAW: Bool = false
|
var includeRAW: Bool = false
|
||||||
var newFilesOnly: Bool = true
|
var newFilesOnly: Bool = true
|
||||||
|
var includeScreenRecordings: Bool = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ struct SettingsView: View {
|
|||||||
ToggleRow(icon: "camera.viewfinder", title: "Screenshots",
|
ToggleRow(icon: "camera.viewfinder", title: "Screenshots",
|
||||||
isOn: $store.backupFilter.includeScreenshots)
|
isOn: $store.backupFilter.includeScreenshots)
|
||||||
hairline
|
hairline
|
||||||
|
ToggleRow(icon: "record.circle", title: "Screen Recordings",
|
||||||
|
isOn: $store.backupFilter.includeScreenRecordings)
|
||||||
|
hairline
|
||||||
ToggleRow(icon: "rays", title: "RAW",
|
ToggleRow(icon: "rays", title: "RAW",
|
||||||
isOn: $store.backupFilter.includeRAW)
|
isOn: $store.backupFilter.includeRAW)
|
||||||
hairline
|
hairline
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ actor LocalPhotoIndex {
|
|||||||
var mediaType: Int // PHAssetMediaType.rawValue
|
var mediaType: Int // PHAssetMediaType.rawValue
|
||||||
var isScreenshot: Bool
|
var isScreenshot: Bool
|
||||||
var isRAW: Bool
|
var isRAW: Bool
|
||||||
|
var isScreenRecording: Bool = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private var records: [String: Record] = [:]
|
private var records: [String: Record] = [:]
|
||||||
@@ -118,6 +119,7 @@ actor LocalPhotoIndex {
|
|||||||
if !filter.includeVideos && record.mediaType == vid { return false }
|
if !filter.includeVideos && record.mediaType == vid { return false }
|
||||||
if !filter.includeScreenshots && record.isScreenshot { return false }
|
if !filter.includeScreenshots && record.isScreenshot { return false }
|
||||||
if !filter.includeRAW && record.isRAW { return false }
|
if !filter.includeRAW && record.isRAW { return false }
|
||||||
|
if !filter.includeScreenRecordings && record.isScreenRecording { return false }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,13 +163,15 @@ actor LocalPhotoIndex {
|
|||||||
?? "\(asset.localIdentifier.prefix(8)).jpg"
|
?? "\(asset.localIdentifier.prefix(8)).jpg"
|
||||||
let isRAW = resources.contains { $0.type == .alternatePhoto }
|
let isRAW = resources.contains { $0.type == .alternatePhoto }
|
||||||
let isScreenshot = asset.mediaSubtypes.contains(.photoScreenshot)
|
let isScreenshot = asset.mediaSubtypes.contains(.photoScreenshot)
|
||||||
|
let isScreenRecording = asset.mediaSubtypes.contains(.videoScreenRecording)
|
||||||
return Record(
|
return Record(
|
||||||
localIdentifier: asset.localIdentifier,
|
localIdentifier: asset.localIdentifier,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
creationDate: asset.creationDate,
|
creationDate: asset.creationDate,
|
||||||
mediaType: asset.mediaType.rawValue,
|
mediaType: asset.mediaType.rawValue,
|
||||||
isScreenshot: isScreenshot,
|
isScreenshot: isScreenshot,
|
||||||
isRAW: isRAW
|
isRAW: isRAW,
|
||||||
|
isScreenRecording: isScreenRecording
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ final class PhotoLibraryService: PhotoLibraryProtocol {
|
|||||||
PHAssetMediaSubtype.photoScreenshot.rawValue
|
PHAssetMediaSubtype.photoScreenshot.rawValue
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
if !filter.includeScreenRecordings {
|
||||||
|
subpredicates.append(NSPredicate(
|
||||||
|
format: "NOT ((mediaSubtype & %d) != 0)",
|
||||||
|
PHAssetMediaSubtype.videoScreenRecording.rawValue
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
if subpredicates.count > 1 {
|
if subpredicates.count > 1 {
|
||||||
options.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates)
|
options.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates)
|
||||||
@@ -46,6 +52,7 @@ final class PhotoLibraryService: PhotoLibraryProtocol {
|
|||||||
result.enumerateObjects { asset, _, _ in
|
result.enumerateObjects { asset, _, _ in
|
||||||
autoreleasepool {
|
autoreleasepool {
|
||||||
let isScreenshot = asset.mediaSubtypes.contains(.photoScreenshot)
|
let isScreenshot = asset.mediaSubtypes.contains(.photoScreenshot)
|
||||||
|
let isScreenRecording = asset.mediaSubtypes.contains(.videoScreenRecording)
|
||||||
let resources = PHAssetResource.assetResources(for: asset)
|
let resources = PHAssetResource.assetResources(for: asset)
|
||||||
let isRAW = resources.contains { $0.type == .alternatePhoto }
|
let isRAW = resources.contains { $0.type == .alternatePhoto }
|
||||||
if isRAW && !filter.includeRAW { return }
|
if isRAW && !filter.includeRAW { return }
|
||||||
@@ -61,7 +68,8 @@ final class PhotoLibraryService: PhotoLibraryProtocol {
|
|||||||
pixelHeight: asset.pixelHeight,
|
pixelHeight: asset.pixelHeight,
|
||||||
duration: asset.duration,
|
duration: asset.duration,
|
||||||
isScreenshot: isScreenshot,
|
isScreenshot: isScreenshot,
|
||||||
isRAW: isRAW
|
isRAW: isRAW,
|
||||||
|
isScreenRecording: isScreenRecording
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,6 +99,12 @@ final class PhotoLibraryService: PhotoLibraryProtocol {
|
|||||||
PHAssetMediaSubtype.photoScreenshot.rawValue
|
PHAssetMediaSubtype.photoScreenshot.rawValue
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
if !filter.includeScreenRecordings {
|
||||||
|
subpredicates.append(NSPredicate(
|
||||||
|
format: "NOT ((mediaSubtype & %d) != 0)",
|
||||||
|
PHAssetMediaSubtype.videoScreenRecording.rawValue
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
if subpredicates.count > 1 {
|
if subpredicates.count > 1 {
|
||||||
options.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates)
|
options.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates)
|
||||||
|
|||||||
Reference in New Issue
Block a user