mirror of
https://github.com/immich-app/immich.git
synced 2025-06-23 15:30:51 -04:00
* feat: working widgets * chore/feat: cleaned up API, added album picker to random widget * album filtering for requests * check album and throw if not found * fix app IDs and project configuration * switch to repository/service model for updating widgets * fix: remove home widget import * revert info.plist formatting changes * ran swift-format on widget code * more formatting changes (this time run from xcode) * show memory on widget picker snapshot * fix: dart changes from code review * fix: swift code review changes (not including task groups) * fix: use task groups to run image retrievals concurrently, get rid of do catch in favor of if let * chore: cleanup widget service in dart app * chore: format swift * fix: remove comma why does xcode not freak out over this >:( * switch to preview size for thumbnail * chore: cropped large image * fix: properly resize widgets so we dont OOM * fix: set app group on logout happens on first install * fix: stupid app ids * fix: revert back to thumbnail we are hitting OOM exceptions due to resizing, once we have on-the-fly resizing on server this can be upgraded * fix: more memory efficient resizing method, remove extraneous resize commands from API call * fix: random widget use 12 entries instead of 24 to save memory * fix: modify duration of entries to 20 minutes and only generate 10 at a time to avoid OOM * feat: toggle to show album name on random widget * Podfile lock --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
167 lines
4.4 KiB
Swift
167 lines
4.4 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
|
|
) {
|
|
Task {
|
|
guard let api = try? await ImmichAPI() else {
|
|
completion(ImageEntry(date: Date(), image: nil, error: .noLogin))
|
|
return
|
|
}
|
|
|
|
guard let memories = try? await api.fetchMemory(for: Date.now)
|
|
else {
|
|
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
|
|
return
|
|
}
|
|
|
|
for memory in memories {
|
|
if let asset = memory.assets.first(where: { $0.type == .image }),
|
|
var entry = try? await buildEntry(
|
|
api: api,
|
|
asset: asset,
|
|
dateOffset: 0,
|
|
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
|
|
)
|
|
{
|
|
entry.resize()
|
|
completion(entry)
|
|
return
|
|
}
|
|
}
|
|
|
|
// fallback to random image
|
|
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(
|
|
api: api,
|
|
asset: randomImage,
|
|
dateOffset: 0
|
|
)
|
|
else {
|
|
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
|
|
return
|
|
}
|
|
|
|
imageEntry.resize()
|
|
completion(imageEntry)
|
|
}
|
|
}
|
|
|
|
func getTimeline(
|
|
in context: Context,
|
|
completion: @escaping @Sendable (Timeline<ImageEntry>) -> Void
|
|
) {
|
|
Task {
|
|
var entries: [ImageEntry] = []
|
|
let now = Date()
|
|
|
|
guard let api = try? await ImmichAPI() else {
|
|
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
|
|
completion(Timeline(entries: entries, policy: .atEnd))
|
|
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 buildEntry(
|
|
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 {
|
|
entries.append(
|
|
contentsOf: (try? await generateRandomEntries(
|
|
api: api,
|
|
now: now,
|
|
count: 12
|
|
)) ?? []
|
|
)
|
|
}
|
|
|
|
// 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))
|
|
}
|
|
|
|
// Resize all images to something that can be stored by iOS
|
|
for i in entries.indices {
|
|
entries[i].resize()
|
|
}
|
|
|
|
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.")
|
|
}
|
|
}
|