Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array/>
</plist>
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "CA988DE0-7135-4640-B30E-7EABBA4F8651"
type = "1"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "C0E5C4EA-FE4A-4136-ABA4-AB5F50DBD9D1"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Immich Gallery/Services/AssetService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "18"
endingLineNumber = "18"
landmarkName = "fetchAssets(page:limit:albumId:personId:tagId:city:isAllPhotos:isFavorite:folderPath:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>Immich Gallery.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>TopShelfExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
</dict>
</dict>
</plist>
51 changes: 50 additions & 1 deletion Immich Gallery/Models/ImmichModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct ImmichAsset: Codable, Identifiable, Equatable {
let resized: Bool?
let thumbhash: String?
let fileModifiedAt: String
let fileCreatedAt: String
let fileCreatedAt: String // Time media was created
let localDateTime: String
let updatedAt: String
let isFavorite: Bool
Expand Down Expand Up @@ -51,6 +51,55 @@ struct ImmichAsset: Codable, Identifiable, Equatable {
}
}

extension Array where Element == ImmichAsset {
func filtered(years: Set<Int>, devices: Set<String>, locations: Set<String>) -> [ImmichAsset] {
return self.filter { asset in
// 1. Filter by Year (extracted from localDateTime: "YYYY-MM-DD...")
let assetYear = Int(asset.localDateTime.prefix(4)) ?? 0
let yearMatch = years.isEmpty || years.contains(assetYear)

// 2. Filter by Device Model (from ExifInfo)
let deviceModel = asset.exifInfo?.model ?? "Unknown"
let deviceMatch = devices.isEmpty || devices.contains(deviceModel)

// 3. Filter by Location (matches City, State, or Country)
let city = asset.exifInfo?.city ?? ""

let locationMatch = locations.isEmpty ||
locations.contains(city)

// Asset must pass all active filters
return yearMatch && deviceMatch && locationMatch
}
}

func sorted(by sortField: String, sortOrder: String = "desc") -> [ImmichAsset] {
let ascending = (sortOrder.lowercased() == "asc")

return self.sorted { lhs, rhs in
var lhsValue = ""
var rhsValue = ""

if sortField == "localDateTime" {
lhsValue = lhs.localDateTime
rhsValue = rhs.localDateTime
} else if sortField == "originalFileName" {
lhsValue = lhs.originalFileName
rhsValue = rhs.originalFileName
} else {
lhsValue = lhs.localDateTime
rhsValue = rhs.localDateTime
}

if ascending {
return lhsValue < rhsValue
} else {
return lhsValue > rhsValue
}
}
}
}

enum AssetType: String, Codable {
case image = "IMAGE"
case video = "VIDEO"
Expand Down
72 changes: 52 additions & 20 deletions Immich Gallery/Services/AssetProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ struct AssetProviderFactory {
protocol AssetProvider {
func fetchAssets(page: Int, limit: Int) async throws -> SearchResult
func fetchRandomAssets(limit: Int) async throws -> SearchResult
func fetchAllCities() async throws -> [String]
func fetchAllYears() async throws -> [Int]
func fetchAllDevices() async throws -> [String]
}

class AlbumAssetProvider: AssetProvider {
Expand Down Expand Up @@ -75,7 +78,6 @@ class AlbumAssetProvider: AssetProvider {
return cachedAssets
}

// Fetch the album with full asset list; Immich includes assets unless withoutAssets is true
let album = try await albumService.getAlbumInfo(albumId: albumId, withoutAssets: false)
cachedAssets = album.assets
return album.assets
Expand Down Expand Up @@ -124,6 +126,37 @@ class AlbumAssetProvider: AssetProvider {
)
}

func fetchAllCities() async throws -> [String] {
let assets = try await loadAlbumAssets()
let cities = assets.compactMap { asset in
if let city = asset.exifInfo?.city, !city.isEmpty {
return city
}
return nil
}
return Array(Set(cities)).sorted()
}

func fetchAllYears() async throws -> [Int] {
let assets = try await loadAlbumAssets()
let years = assets.compactMap { asset -> Int? in
let yearString = asset.localDateTime.prefix(4)
return Int(yearString)
}
return Array(Set(years)).sorted(by: >)
}

func fetchAllDevices() async throws -> [String] {
let assets = try await loadAlbumAssets()
let devices = assets.compactMap { asset in
if let model = asset.exifInfo?.model, !model.isEmpty {
return model
}
return nil
}
return Array(Set(devices)).sorted()
}

private func sortAssets(_ assets: [ImmichAsset]) -> [ImmichAsset] {
let sortOrder = currentSortOrder()
return assets.sorted { lhs, rhs in
Expand All @@ -144,24 +177,15 @@ class AlbumAssetProvider: AssetProvider {
}

private func currentSortOrder() -> SortOrder {
let storedValue = UserDefaults.standard.string(forKey: UserDefaultsKeys.assetSortOrder) ?? "desc"
let storedValue = UserDefaults.standard.string(forKey: "assetSortOrder") ?? "desc"
return storedValue.lowercased() == "asc" ? .oldestFirst : .newestFirst
}

private func captureDate(for asset: ImmichAsset) -> Date {
if let date = parseDate(asset.exifInfo?.dateTimeOriginal) {
return date
}
if let date = parseDate(asset.fileCreatedAt) {
return date
}
if let date = parseDate(asset.fileModifiedAt) {
return date
}
if let date = parseDate(asset.updatedAt) {
return date
}

if let date = parseDate(asset.exifInfo?.dateTimeOriginal) { return date }
if let date = parseDate(asset.fileCreatedAt) { return date }
if let date = parseDate(asset.fileModifiedAt) { return date }
if let date = parseDate(asset.updatedAt) { return date }
return .distantPast
}

Expand Down Expand Up @@ -196,13 +220,10 @@ class GeneralAssetProvider: AssetProvider {
}

func fetchAssets(page: Int, limit: Int) async throws -> SearchResult {
// If config is provided, use it; otherwise fall back to individual parameters
if let config = config {
return try await assetService.fetchAssets(config: config, page: page, limit: limit, isAllPhotos: isAllPhotos)
} else {
return try await assetService.fetchAssets(
page: page,
limit: limit,
return try await assetService.fetchAllFilteredAndSortedAssets(
albumId: nil,
personId: personId,
tagId: tagId,
Expand All @@ -215,7 +236,6 @@ class GeneralAssetProvider: AssetProvider {
}

func fetchRandomAssets(limit: Int) async throws -> SearchResult {
// If config is provided, use it; otherwise fall back to individual parameters
if let config = config {
return try await assetService.fetchRandomAssets(config: config, limit: limit)
} else {
Expand All @@ -228,4 +248,16 @@ class GeneralAssetProvider: AssetProvider {
)
}
}

func fetchAllCities() async throws -> [String] {
return try await assetService.fetchAllCities()
}

func fetchAllYears() async throws -> [Int] {
return try await assetService.fetchAllYears()
}

func fetchAllDevices() async throws -> [String] {
return try await assetService.fetchAllDevices()
}
}
Loading