mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
* improvements to error handling, ability to select "Favorites" as a virtual album, fix widgets not showing image when tinting homescreen * dont include isFavorite all the time * remove check for if the album exists this will never run because we default to Album.NONE and its impossible to distinguish between no album selected and album DNE (we dont know what the store ID is, only what iOS gives)
171 lines
4.5 KiB
Swift
171 lines
4.5 KiB
Swift
import AppIntents
|
|
import SwiftUI
|
|
import WidgetKit
|
|
|
|
struct ImmichMemoryProvider: TimelineProvider {
|
|
func getYearDifferenceSubtitle(assetYear: Int) -> String {
|
|
let currentYear = Calendar.current.component(.year, from: Date.now)
|
|
// construct a "X years ago" subtitle
|
|
let yearDifference = currentYear - assetYear
|
|
|
|
return "\(yearDifference) year\(yearDifference == 1 ? "" : "s") ago"
|
|
}
|
|
|
|
func placeholder(in context: Context) -> ImageEntry {
|
|
ImageEntry(date: Date(), image: nil)
|
|
}
|
|
|
|
func getSnapshot(
|
|
in context: Context,
|
|
completion: @escaping @Sendable (ImageEntry) -> Void
|
|
) {
|
|
let cacheKey = "memory_\(context.family.rawValue)"
|
|
|
|
Task {
|
|
guard let api = try? await ImmichAPI() else {
|
|
completion(
|
|
ImageEntry.handleError(for: cacheKey, error: .noLogin).entries.first!
|
|
)
|
|
return
|
|
}
|
|
|
|
guard let memories = try? await api.fetchMemory(for: Date.now)
|
|
else {
|
|
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
|
return
|
|
}
|
|
|
|
for memory in memories {
|
|
if let asset = memory.assets.first(where: { $0.type == .image }),
|
|
let entry = try? await ImageEntry.build(
|
|
api: api,
|
|
asset: asset,
|
|
dateOffset: 0,
|
|
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
|
|
)
|
|
{
|
|
completion(entry)
|
|
return
|
|
}
|
|
}
|
|
|
|
// fallback to random image
|
|
guard
|
|
let randomImage = try? await api.fetchSearchResults().first,
|
|
let imageEntry = try? await ImageEntry.build(
|
|
api: api,
|
|
asset: randomImage,
|
|
dateOffset: 0
|
|
)
|
|
else {
|
|
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
|
return
|
|
}
|
|
|
|
completion(imageEntry)
|
|
}
|
|
}
|
|
|
|
func getTimeline(
|
|
in context: Context,
|
|
completion: @escaping @Sendable (Timeline<ImageEntry>) -> Void
|
|
) {
|
|
Task {
|
|
var entries: [ImageEntry] = []
|
|
let now = Date()
|
|
|
|
let cacheKey = "memory_\(context.family.rawValue)"
|
|
|
|
guard let api = try? await ImmichAPI() else {
|
|
completion(
|
|
ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
|
)
|
|
return
|
|
}
|
|
|
|
let memories = try await api.fetchMemory(for: Date.now)
|
|
|
|
await withTaskGroup(of: ImageEntry?.self) { group in
|
|
var totalAssets = 0
|
|
|
|
for memory in memories {
|
|
for asset in memory.assets {
|
|
if asset.type == .image && totalAssets < 12 {
|
|
group.addTask {
|
|
try? await ImageEntry.build(
|
|
api: api,
|
|
asset: asset,
|
|
dateOffset: totalAssets,
|
|
subtitle: getYearDifferenceSubtitle(
|
|
assetYear: memory.data.year
|
|
)
|
|
)
|
|
}
|
|
|
|
totalAssets += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
for await result in group {
|
|
if let entry = result {
|
|
entries.append(entry)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we didnt add any memory images (some failure occured or no images in memory),
|
|
// default to 12 hours of random photos
|
|
if entries.count == 0 {
|
|
// this must be a do/catch since we need to
|
|
// distinguish between a network fail and an empty search
|
|
do {
|
|
let search = try await generateRandomEntries(
|
|
api: api,
|
|
now: now,
|
|
count: 12
|
|
)
|
|
|
|
// Load or save a cached asset for when network conditions are bad
|
|
if search.count == 0 {
|
|
completion(
|
|
ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
|
)
|
|
return
|
|
}
|
|
|
|
entries.append(contentsOf: search)
|
|
} catch {
|
|
completion(ImageEntry.handleError(for: cacheKey))
|
|
return
|
|
}
|
|
}
|
|
|
|
// cache the last image
|
|
try? entries.last!.cache(for: cacheKey)
|
|
|
|
completion(Timeline(entries: entries, policy: .atEnd))
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ImmichMemoryWidget: Widget {
|
|
let kind: String = "com.immich.widget.memory"
|
|
|
|
var body: some WidgetConfiguration {
|
|
StaticConfiguration(
|
|
kind: kind,
|
|
provider: ImmichMemoryProvider()
|
|
) { entry in
|
|
ImmichWidgetView(entry: entry)
|
|
.containerBackground(.regularMaterial, for: .widget)
|
|
}
|
|
// allow image to take up entire widget
|
|
.contentMarginsDisabled()
|
|
|
|
// widget picker info
|
|
.configurationDisplayName("Memories")
|
|
.description("See memories from Immich.")
|
|
}
|
|
}
|