2025-06-17 09:43:09 -05:00
|
|
|
import AppIntents
|
|
|
|
import SwiftUI
|
|
|
|
import WidgetKit
|
|
|
|
|
|
|
|
// MARK: Widget Configuration
|
|
|
|
|
|
|
|
extension Album: @unchecked Sendable, AppEntity, Identifiable {
|
|
|
|
|
|
|
|
struct AlbumQuery: EntityQuery {
|
|
|
|
func entities(for identifiers: [Album.ID]) async throws -> [Album] {
|
2025-07-15 21:17:24 -05:00
|
|
|
return await suggestedEntities().filter {
|
2025-06-17 09:43:09 -05:00
|
|
|
identifiers.contains($0.id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-15 21:17:24 -05:00
|
|
|
func suggestedEntities() async -> [Album] {
|
|
|
|
let albums = (try? await AlbumCache.shared.getAlbums()) ?? []
|
|
|
|
|
|
|
|
let options =
|
|
|
|
[
|
|
|
|
NONE,
|
|
|
|
FAVORITES,
|
|
|
|
] + albums
|
2025-06-17 09:43:09 -05:00
|
|
|
|
2025-07-15 21:17:24 -05:00
|
|
|
return options
|
2025-06-17 09:43:09 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static var defaultQuery = AlbumQuery()
|
|
|
|
static var typeDisplayRepresentation = TypeDisplayRepresentation(
|
|
|
|
name: "Album"
|
|
|
|
)
|
|
|
|
|
|
|
|
var displayRepresentation: DisplayRepresentation {
|
|
|
|
DisplayRepresentation(title: "\(albumName)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
|
|
|
static var title: LocalizedStringResource { "Select Album" }
|
|
|
|
static var description: IntentDescription {
|
|
|
|
"Choose an album to show images from"
|
|
|
|
}
|
|
|
|
|
2025-06-19 12:00:54 -05:00
|
|
|
@Parameter(title: "Album")
|
2025-06-17 09:43:09 -05:00
|
|
|
var album: Album?
|
2025-07-09 13:59:54 -05:00
|
|
|
|
2025-06-17 09:43:09 -05:00
|
|
|
@Parameter(title: "Show Album Name", default: false)
|
|
|
|
var showAlbumName: Bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Provider
|
|
|
|
|
|
|
|
struct ImmichRandomProvider: AppIntentTimelineProvider {
|
|
|
|
func placeholder(in context: Context) -> ImageEntry {
|
2025-07-09 13:59:54 -05:00
|
|
|
ImageEntry(date: Date())
|
2025-06-17 09:43:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func snapshot(
|
|
|
|
for configuration: RandomConfigurationAppIntent,
|
|
|
|
in context: Context
|
|
|
|
) async
|
|
|
|
-> ImageEntry
|
|
|
|
{
|
2025-07-09 13:59:54 -05:00
|
|
|
let cacheKey = "random_none_\(context.family.rawValue)"
|
2025-07-15 21:17:24 -05:00
|
|
|
|
2025-06-17 09:43:09 -05:00
|
|
|
guard let api = try? await ImmichAPI() else {
|
2025-07-15 21:17:24 -05:00
|
|
|
return ImageEntry.handleError(for: cacheKey, error: .noLogin).entries
|
|
|
|
.first!
|
2025-06-17 09:43:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
guard
|
|
|
|
let randomImage = try? await api.fetchSearchResults(
|
2025-07-15 21:17:24 -05:00
|
|
|
with: Album.NONE.filter
|
2025-07-09 13:59:54 -05:00
|
|
|
).first,
|
2025-07-15 21:17:24 -05:00
|
|
|
let entry = try? await ImageEntry.build(
|
2025-06-17 09:43:09 -05:00
|
|
|
api: api,
|
|
|
|
asset: randomImage,
|
|
|
|
dateOffset: 0
|
|
|
|
)
|
|
|
|
else {
|
2025-07-15 21:17:24 -05:00
|
|
|
return ImageEntry.handleError(for: cacheKey).entries.first!
|
2025-06-17 09:43:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return entry
|
|
|
|
}
|
|
|
|
|
|
|
|
func timeline(
|
|
|
|
for configuration: RandomConfigurationAppIntent,
|
|
|
|
in context: Context
|
|
|
|
) async
|
|
|
|
-> Timeline<ImageEntry>
|
|
|
|
{
|
|
|
|
var entries: [ImageEntry] = []
|
|
|
|
let now = Date()
|
|
|
|
|
2025-07-09 13:59:54 -05:00
|
|
|
// nil if album is NONE or nil
|
2025-07-15 21:17:24 -05:00
|
|
|
let album = configuration.album ?? Album.NONE
|
|
|
|
let albumName = album.isVirtual ? nil : album.albumName
|
2025-07-09 13:59:54 -05:00
|
|
|
|
2025-07-15 21:17:24 -05:00
|
|
|
let cacheKey = "random_\(album.id)_\(context.family.rawValue)"
|
2025-07-09 13:59:54 -05:00
|
|
|
|
2025-06-17 09:43:09 -05:00
|
|
|
// If we don't have a server config, return an entry with an error
|
|
|
|
guard let api = try? await ImmichAPI() else {
|
2025-07-15 21:17:24 -05:00
|
|
|
return ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
2025-06-17 09:43:09 -05:00
|
|
|
}
|
|
|
|
|
2025-07-09 13:59:54 -05:00
|
|
|
// build entries
|
2025-07-15 21:17:24 -05:00
|
|
|
// 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(
|
2025-06-17 09:43:09 -05:00
|
|
|
api: api,
|
|
|
|
now: now,
|
|
|
|
count: 12,
|
2025-07-15 21:17:24 -05:00
|
|
|
filter: album.filter,
|
2025-06-17 09:43:09 -05:00
|
|
|
subtitle: configuration.showAlbumName ? albumName : nil
|
2025-07-15 21:17:24 -05:00
|
|
|
)
|
2025-06-17 09:43:09 -05:00
|
|
|
|
2025-07-15 21:17:24 -05:00
|
|
|
// Load or save a cached asset for when network conditions are bad
|
|
|
|
if search.count == 0 {
|
|
|
|
return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
|
|
|
}
|
2025-06-17 09:43:09 -05:00
|
|
|
|
2025-07-15 21:17:24 -05:00
|
|
|
entries.append(contentsOf: search)
|
|
|
|
} catch {
|
|
|
|
return ImageEntry.handleError(for: cacheKey)
|
2025-06-17 09:43:09 -05:00
|
|
|
}
|
|
|
|
|
2025-07-09 13:59:54 -05:00
|
|
|
// cache the last image
|
|
|
|
try? entries.last!.cache(for: cacheKey)
|
|
|
|
|
2025-06-17 09:43:09 -05:00
|
|
|
return Timeline(entries: entries, policy: .atEnd)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ImmichRandomWidget: Widget {
|
|
|
|
let kind: String = "com.immich.widget.random"
|
|
|
|
|
|
|
|
var body: some WidgetConfiguration {
|
|
|
|
AppIntentConfiguration(
|
|
|
|
kind: kind,
|
|
|
|
intent: RandomConfigurationAppIntent.self,
|
|
|
|
provider: ImmichRandomProvider()
|
|
|
|
) { entry in
|
|
|
|
ImmichWidgetView(entry: entry)
|
|
|
|
.containerBackground(.regularMaterial, for: .widget)
|
|
|
|
}
|
|
|
|
// allow image to take up entire widget
|
|
|
|
.contentMarginsDisabled()
|
|
|
|
|
|
|
|
// widget picker info
|
|
|
|
.configurationDisplayName("Random")
|
|
|
|
.description("View a random image from your library or a specific album.")
|
|
|
|
}
|
|
|
|
}
|