1
0
mirror of https://github.com/immich-app/immich.git synced 2025-08-08 23:07:06 +02:00

feat(mobile): cache latest ios widget entry for fallback (#19824)

* cache the last image an ios widget fetched and use if a fetch fails in a future timeline build

* code review fixes

* downgrade pbx for flutter

* use cache in snapshots
This commit is contained in:
Brandon Wees
2025-07-09 13:59:54 -05:00
committed by GitHub
parent a201665b7e
commit a918481c0b
7 changed files with 248 additions and 146 deletions

View File

@ -19,21 +19,23 @@ struct ImmichMemoryProvider: TimelineProvider {
in context: Context,
completion: @escaping @Sendable (ImageEntry) -> Void
) {
let cacheKey = "memory_\(context.family.rawValue)"
Task {
guard let api = try? await ImmichAPI() else {
completion(ImageEntry(date: Date(), image: nil, error: .noLogin))
completion(ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin).entries.first!)
return
}
guard let memories = try? await api.fetchMemory(for: Date.now)
else {
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
completion(ImageEntry.handleCacheFallback(for: cacheKey).entries.first!)
return
}
for memory in memories {
if let asset = memory.assets.first(where: { $0.type == .image }),
var entry = try? await buildEntry(
var entry = try? await ImageEntry.build(
api: api,
asset: asset,
dateOffset: 0,
@ -50,20 +52,14 @@ struct ImmichMemoryProvider: TimelineProvider {
guard
let randomImage = try? await api.fetchSearchResults(
with: SearchFilters(size: 1)
).first
else {
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
return
}
guard
var imageEntry = try? await buildEntry(
).first,
var imageEntry = try? await ImageEntry.build(
api: api,
asset: randomImage,
dateOffset: 0
)
else {
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
completion(ImageEntry.handleCacheFallback(for: cacheKey).entries.first!)
return
}
@ -80,9 +76,12 @@ struct ImmichMemoryProvider: TimelineProvider {
var entries: [ImageEntry] = []
let now = Date()
let cacheKey = "memory_\(context.family.rawValue)"
guard let api = try? await ImmichAPI() else {
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
completion(Timeline(entries: entries, policy: .atEnd))
completion(
ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin)
)
return
}
@ -95,7 +94,7 @@ struct ImmichMemoryProvider: TimelineProvider {
for asset in memory.assets {
if asset.type == .image && totalAssets < 12 {
group.addTask {
try? await buildEntry(
try? await ImageEntry.build(
api: api,
asset: asset,
dateOffset: totalAssets,
@ -132,7 +131,8 @@ struct ImmichMemoryProvider: TimelineProvider {
// If we fail to fetch images, we still want to add an entry
// with a nil image and an error
if entries.count == 0 {
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
completion(ImageEntry.handleCacheFallback(for: cacheKey))
return
}
// Resize all images to something that can be stored by iOS
@ -140,6 +140,9 @@ struct ImmichMemoryProvider: TimelineProvider {
entries[i].resize()
}
// cache the last image
try? entries.last!.cache(for: cacheKey)
completion(Timeline(entries: entries, policy: .atEnd))
}
}

View File

@ -45,7 +45,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
@Parameter(title: "Album")
var album: Album?
@Parameter(title: "Show Album Name", default: false)
var showAlbumName: Bool
}
@ -54,7 +54,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
struct ImmichRandomProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> ImageEntry {
ImageEntry(date: Date(), image: nil)
ImageEntry(date: Date())
}
func snapshot(
@ -63,26 +63,23 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
) async
-> ImageEntry
{
let cacheKey = "random_none_\(context.family.rawValue)"
guard let api = try? await ImmichAPI() else {
return ImageEntry(date: Date(), image: nil, error: .noLogin)
return ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin).entries.first!
}
guard
let randomImage = try? await api.fetchSearchResults(
with: SearchFilters(size: 1)
).first
else {
return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
}
guard
var entry = try? await buildEntry(
).first,
var entry = try? await ImageEntry.build(
api: api,
asset: randomImage,
dateOffset: 0
)
else {
return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
return ImageEntry.handleCacheFallback(for: cacheKey).entries.first!
}
entry.resize()
@ -99,30 +96,34 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
var entries: [ImageEntry] = []
let now = Date()
// If we don't have a server config, return an entry with an error
guard let api = try? await ImmichAPI() else {
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
return Timeline(entries: entries, policy: .atEnd)
}
// nil if album is NONE or nil
let albumId =
configuration.album?.id != "NONE" ? configuration.album?.id : nil
var albumName: String? = albumId != nil ? configuration.album?.albumName : nil
let albumName: String? =
albumId != nil ? configuration.album?.albumName : nil
let cacheKey = "random_\(albumId ?? "none")_\(context.family.rawValue)"
// If we don't have a server config, return an entry with an error
guard let api = try? await ImmichAPI() else {
return ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin)
}
if albumId != nil {
// make sure the album exists on server, otherwise show error
guard let albums = try? await api.fetchAlbums() else {
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
return Timeline(entries: entries, policy: .atEnd)
return ImageEntry.handleCacheFallback(for: cacheKey)
}
if !albums.contains(where: { $0.id == albumId }) {
entries.append(ImageEntry(date: now, image: nil, error: .albumNotFound))
return Timeline(entries: entries, policy: .atEnd)
return ImageEntry.handleCacheFallback(
for: cacheKey,
error: .albumNotFound
)
}
}
// build entries
entries.append(
contentsOf: (try? await generateRandomEntries(
api: api,
@ -134,9 +135,9 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
?? []
)
// If we fail to fetch images, we still want to add an entry with a nil image and an error
// Load or save a cached asset for when network conditions are bad
if entries.count == 0 {
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
return ImageEntry.handleCacheFallback(for: cacheKey)
}
// Resize all images to something that can be stored by iOS
@ -144,6 +145,9 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
entries[i].resize()
}
// cache the last image
try? entries.last!.cache(for: cacheKey)
return Timeline(entries: entries, policy: .atEnd)
}
}