mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
cache the last image an ios widget fetched and use if a fetch fails in a future timeline build
This commit is contained in:
parent
172388c455
commit
e211b47fad
@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@ -117,8 +117,6 @@
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = Sync;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@ -473,10 +471,14 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
@ -505,10 +507,14 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
|
@ -1,59 +0,0 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
func buildEntry(
|
||||
api: ImmichAPI,
|
||||
asset: Asset,
|
||||
dateOffset: Int,
|
||||
subtitle: String? = nil
|
||||
)
|
||||
async throws -> ImageEntry
|
||||
{
|
||||
let entryDate = Calendar.current.date(
|
||||
byAdding: .minute,
|
||||
value: dateOffset * 20,
|
||||
to: Date.now
|
||||
)!
|
||||
let image = try await api.fetchImage(asset: asset)
|
||||
|
||||
return ImageEntry(date: entryDate, image: image, subtitle: subtitle, deepLink: asset.deepLink)
|
||||
}
|
||||
|
||||
func generateRandomEntries(
|
||||
api: ImmichAPI,
|
||||
now: Date,
|
||||
count: Int,
|
||||
albumId: String? = nil,
|
||||
subtitle: String? = nil
|
||||
)
|
||||
async throws -> [ImageEntry]
|
||||
{
|
||||
|
||||
var entries: [ImageEntry] = []
|
||||
let albumIds = albumId != nil ? [albumId!] : []
|
||||
|
||||
let randomAssets = try await api.fetchSearchResults(
|
||||
with: SearchFilters(size: count, albumIds: albumIds)
|
||||
)
|
||||
|
||||
await withTaskGroup(of: ImageEntry?.self) { group in
|
||||
for (dateOffset, asset) in randomAssets.enumerated() {
|
||||
group.addTask {
|
||||
return try? await buildEntry(
|
||||
api: api,
|
||||
asset: asset,
|
||||
dateOffset: dateOffset,
|
||||
subtitle: subtitle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for await result in group {
|
||||
if let entry = result {
|
||||
entries.append(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
170
mobile/ios/WidgetExtension/ImageEntry.swift
Normal file
170
mobile/ios/WidgetExtension/ImageEntry.swift
Normal file
@ -0,0 +1,170 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
typealias EntryMetadata = ImageEntry.Metadata
|
||||
|
||||
struct ImageEntry: TimelineEntry {
|
||||
let date: Date
|
||||
var image: UIImage?
|
||||
var metadata: Metadata = Metadata()
|
||||
|
||||
struct Metadata: Codable {
|
||||
var subtitle: String? = nil
|
||||
var error: WidgetError? = nil
|
||||
var deepLink: URL? = nil
|
||||
}
|
||||
|
||||
// Resizes the stored image to a maximum width of 450 pixels
|
||||
mutating func resize() {
|
||||
if image == nil || image!.size.height < 450 || image!.size.width < 450 {
|
||||
return
|
||||
}
|
||||
|
||||
image = image?.resized(toWidth: 450)
|
||||
|
||||
if image == nil {
|
||||
metadata.error = .unableToResize
|
||||
}
|
||||
}
|
||||
|
||||
static func build(
|
||||
api: ImmichAPI,
|
||||
asset: Asset,
|
||||
dateOffset: Int,
|
||||
subtitle: String? = nil
|
||||
)
|
||||
async throws -> Self
|
||||
{
|
||||
let entryDate = Calendar.current.date(
|
||||
byAdding: .minute,
|
||||
value: dateOffset * 20,
|
||||
to: Date.now
|
||||
)!
|
||||
let image = try await api.fetchImage(asset: asset)
|
||||
|
||||
return Self(
|
||||
date: entryDate,
|
||||
image: image,
|
||||
metadata: ImageEntry.Metadata(
|
||||
subtitle: subtitle,
|
||||
deepLink: asset.deepLink
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func cache(for key: String) throws {
|
||||
if let containerURL = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: IMMICH_SHARE_GROUP
|
||||
) {
|
||||
let imageURL = containerURL.appendingPathComponent("\(key)_image.txt")
|
||||
let metadataURL = containerURL.appendingPathComponent(
|
||||
"\(key)_metadata.txt"
|
||||
)
|
||||
|
||||
// build metadata JSON
|
||||
let entryMetadata = try JSONEncoder().encode(self.metadata)
|
||||
|
||||
// write to disk
|
||||
try self.image?.pngData()?.write(to: imageURL, options: .atomic)
|
||||
try entryMetadata.write(to: metadataURL, options: .atomic)
|
||||
}
|
||||
}
|
||||
|
||||
static func loadCached(for key: String, at date: Date = Date.now)
|
||||
-> ImageEntry?
|
||||
{
|
||||
if let containerURL = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: IMMICH_SHARE_GROUP
|
||||
) {
|
||||
let imageURL = containerURL.appendingPathComponent("\(key)_image.txt")
|
||||
let metadataURL = containerURL.appendingPathComponent(
|
||||
"\(key)_metadata.txt"
|
||||
)
|
||||
|
||||
guard let imageData = try? Data(contentsOf: imageURL),
|
||||
let metadataJSON = try? Data(contentsOf: metadataURL)
|
||||
else {
|
||||
// cache miss
|
||||
return nil
|
||||
}
|
||||
|
||||
guard
|
||||
let decodedMetadata = try? JSONDecoder().decode(
|
||||
Metadata.self,
|
||||
from: metadataJSON
|
||||
)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ImageEntry(
|
||||
date: date,
|
||||
image: UIImage(data: imageData),
|
||||
metadata: decodedMetadata
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
static func handleCacheFallback(
|
||||
for key: String,
|
||||
error: WidgetError = .fetchFailed
|
||||
) -> Timeline<ImageEntry> {
|
||||
var timelineEntry = ImageEntry(
|
||||
date: Date.now,
|
||||
image: nil,
|
||||
metadata: EntryMetadata(error: error)
|
||||
)
|
||||
|
||||
// skip cache if album not found or no login
|
||||
// we want to show these errors to the user since without intervention,
|
||||
// it will never succeed
|
||||
if error != .noLogin && error != .albumNotFound {
|
||||
if let cachedEntry = ImageEntry.loadCached(for: key) {
|
||||
timelineEntry = cachedEntry
|
||||
}
|
||||
}
|
||||
|
||||
return Timeline(entries: [timelineEntry], policy: .atEnd)
|
||||
}
|
||||
}
|
||||
|
||||
func generateRandomEntries(
|
||||
api: ImmichAPI,
|
||||
now: Date,
|
||||
count: Int,
|
||||
albumId: String? = nil,
|
||||
subtitle: String? = nil
|
||||
)
|
||||
async throws -> [ImageEntry]
|
||||
{
|
||||
|
||||
var entries: [ImageEntry] = []
|
||||
let albumIds = albumId != nil ? [albumId!] : []
|
||||
|
||||
let randomAssets = try await api.fetchSearchResults(
|
||||
with: SearchFilters(size: count, albumIds: albumIds)
|
||||
)
|
||||
|
||||
await withTaskGroup(of: ImageEntry?.self) { group in
|
||||
for (dateOffset, asset) in randomAssets.enumerated() {
|
||||
group.addTask {
|
||||
return try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: asset,
|
||||
dateOffset: dateOffset,
|
||||
subtitle: subtitle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for await result in group {
|
||||
if let entry = result {
|
||||
entries.append(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
@ -1,27 +1,6 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct ImageEntry: TimelineEntry {
|
||||
let date: Date
|
||||
var image: UIImage?
|
||||
var subtitle: String? = nil
|
||||
var error: WidgetError? = nil
|
||||
var deepLink: URL? = nil
|
||||
|
||||
// Resizes the stored image to a maximum width of 450 pixels
|
||||
mutating func resize() {
|
||||
if (image == nil || image!.size.height < 450 || image!.size.width < 450 ) {
|
||||
return
|
||||
}
|
||||
|
||||
image = image?.resized(toWidth: 450)
|
||||
|
||||
if image == nil {
|
||||
error = .unableToResize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImmichWidgetView: View {
|
||||
var entry: ImageEntry
|
||||
|
||||
@ -29,7 +8,7 @@ struct ImmichWidgetView: View {
|
||||
if entry.image == nil {
|
||||
VStack {
|
||||
Image("LaunchImage")
|
||||
Text(entry.error?.errorDescription ?? "")
|
||||
Text(entry.metadata.error?.errorDescription ?? "")
|
||||
.minimumScaleFactor(0.25)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
@ -44,7 +23,7 @@ struct ImmichWidgetView: View {
|
||||
)
|
||||
VStack {
|
||||
Spacer()
|
||||
if let subtitle = entry.subtitle {
|
||||
if let subtitle = entry.metadata.subtitle {
|
||||
Text(subtitle)
|
||||
.foregroundColor(.white)
|
||||
.padding(8)
|
||||
@ -55,7 +34,7 @@ struct ImmichWidgetView: View {
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.widgetURL(entry.deepLink)
|
||||
.widgetURL(entry.metadata.deepLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -70,7 +49,9 @@ struct ImmichWidgetView: View {
|
||||
ImageEntry(
|
||||
date: date,
|
||||
image: UIImage(named: "ImmichLogo"),
|
||||
subtitle: "1 year ago"
|
||||
metadata: ImageEntry.Metadata(
|
||||
subtitle: "1 year ago"
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -2,7 +2,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
enum WidgetError: Error {
|
||||
enum WidgetError: Error, Codable {
|
||||
case noLogin
|
||||
case fetchFailed
|
||||
case unknown
|
||||
@ -23,10 +23,10 @@ extension WidgetError: LocalizedError {
|
||||
|
||||
case .albumNotFound:
|
||||
return "Album not found"
|
||||
|
||||
|
||||
case .invalidURL:
|
||||
return "An invalid URL was used"
|
||||
|
||||
|
||||
case .invalidImage:
|
||||
return "An invalid image was received"
|
||||
|
||||
@ -46,7 +46,7 @@ enum AssetType: String, Codable {
|
||||
struct Asset: Codable {
|
||||
let id: String
|
||||
let type: AssetType
|
||||
|
||||
|
||||
var deepLink: URL? {
|
||||
return URL(string: "immich://asset?id=\(id)")
|
||||
}
|
||||
@ -75,6 +75,8 @@ struct Album: Codable {
|
||||
let albumName: String
|
||||
}
|
||||
|
||||
let IMMICH_SHARE_GROUP = "group.app.immich.share"
|
||||
|
||||
// MARK: API
|
||||
|
||||
class ImmichAPI {
|
||||
@ -86,7 +88,7 @@ class ImmichAPI {
|
||||
|
||||
init() async throws {
|
||||
// fetch the credentials from the UserDefaults store that dart placed here
|
||||
guard let defaults = UserDefaults(suiteName: "group.app.immich.share"),
|
||||
guard let defaults = UserDefaults(suiteName: IMMICH_SHARE_GROUP),
|
||||
let serverURL = defaults.string(forKey: "widget_server_url"),
|
||||
let sessionKey = defaults.string(forKey: "widget_auth_token")
|
||||
else {
|
||||
@ -189,18 +191,25 @@ class ImmichAPI {
|
||||
else {
|
||||
throw .invalidURL
|
||||
}
|
||||
|
||||
guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil) else {
|
||||
|
||||
guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil)
|
||||
else {
|
||||
throw .invalidURL
|
||||
}
|
||||
|
||||
let decodeOptions: [NSString: Any] = [
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: 400,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: 400,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
]
|
||||
|
||||
guard let thumbnail = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions as CFDictionary) else {
|
||||
|
||||
guard
|
||||
let thumbnail = CGImageSourceCreateThumbnailAtIndex(
|
||||
imageSource,
|
||||
0,
|
||||
decodeOptions as CFDictionary
|
||||
)
|
||||
else {
|
||||
throw .fetchFailed
|
||||
}
|
||||
|
||||
|
@ -7,14 +7,17 @@
|
||||
import UIKit
|
||||
|
||||
extension UIImage {
|
||||
/// Crops the image to ensure width and height do not exceed maxSize.
|
||||
/// Keeps original aspect ratio and crops excess equally from edges (center crop).
|
||||
func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? {
|
||||
let canvas = CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height)))
|
||||
let format = imageRendererFormat
|
||||
format.opaque = isOpaque
|
||||
return UIGraphicsImageRenderer(size: canvas, format: format).image {
|
||||
_ in draw(in: CGRect(origin: .zero, size: canvas))
|
||||
}
|
||||
/// Crops the image to ensure width and height do not exceed maxSize.
|
||||
/// Keeps original aspect ratio and crops excess equally from edges (center crop).
|
||||
func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? {
|
||||
let canvas = CGSize(
|
||||
width: width,
|
||||
height: CGFloat(ceil(width / size.width * size.height))
|
||||
)
|
||||
let format = imageRendererFormat
|
||||
format.opaque = isOpaque
|
||||
return UIGraphicsImageRenderer(size: canvas, format: format).image {
|
||||
_ in draw(in: CGRect(origin: .zero, size: canvas))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,19 +21,31 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
) {
|
||||
Task {
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
completion(ImageEntry(date: Date(), image: nil, error: .noLogin))
|
||||
completion(
|
||||
ImageEntry(
|
||||
date: Date(),
|
||||
image: nil,
|
||||
metadata: EntryMetadata(error: .noLogin)
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
guard let memories = try? await api.fetchMemory(for: Date.now)
|
||||
else {
|
||||
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
|
||||
completion(
|
||||
ImageEntry(
|
||||
date: Date(),
|
||||
image: nil,
|
||||
metadata: EntryMetadata(error: .fetchFailed)
|
||||
)
|
||||
)
|
||||
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,
|
||||
@ -52,18 +64,30 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
with: SearchFilters(size: 1)
|
||||
).first
|
||||
else {
|
||||
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
|
||||
completion(
|
||||
ImageEntry(
|
||||
date: Date(),
|
||||
image: nil,
|
||||
metadata: EntryMetadata(error: .fetchFailed)
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
var imageEntry = try? await buildEntry(
|
||||
var imageEntry = try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: randomImage,
|
||||
dateOffset: 0
|
||||
)
|
||||
else {
|
||||
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
|
||||
completion(
|
||||
ImageEntry(
|
||||
date: Date(),
|
||||
image: nil,
|
||||
metadata: EntryMetadata(error: .fetchFailed)
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@ -80,9 +104,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 +122,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 +159,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 +168,9 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
entries[i].resize()
|
||||
}
|
||||
|
||||
// cache the last image
|
||||
try? entries.last!.cache(for: cacheKey)
|
||||
|
||||
completion(Timeline(entries: entries, policy: .atEnd))
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
@ -64,7 +64,11 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
||||
-> ImageEntry
|
||||
{
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
return ImageEntry(date: Date(), image: nil, error: .noLogin)
|
||||
return ImageEntry(
|
||||
date: Date(),
|
||||
image: nil,
|
||||
metadata: EntryMetadata(error: .noLogin)
|
||||
)
|
||||
}
|
||||
|
||||
guard
|
||||
@ -72,17 +76,25 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
||||
with: SearchFilters(size: 1)
|
||||
).first
|
||||
else {
|
||||
return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
|
||||
return ImageEntry(
|
||||
date: Date(),
|
||||
image: nil,
|
||||
metadata: EntryMetadata(error: .fetchFailed)
|
||||
)
|
||||
}
|
||||
|
||||
guard
|
||||
var entry = try? await buildEntry(
|
||||
var entry = try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: randomImage,
|
||||
dateOffset: 0
|
||||
)
|
||||
else {
|
||||
return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
|
||||
return ImageEntry(
|
||||
date: Date(),
|
||||
image: nil,
|
||||
metadata: EntryMetadata(error: .fetchFailed)
|
||||
)
|
||||
}
|
||||
|
||||
entry.resize()
|
||||
@ -99,30 +111,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 +150,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 +160,9 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
||||
entries[i].resize()
|
||||
}
|
||||
|
||||
// cache the last image
|
||||
try? entries.last!.cache(for: cacheKey)
|
||||
|
||||
return Timeline(entries: entries, policy: .atEnd)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user