mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
feat(widget): add toggle to match icon theme (#30428)
* feat(widget): add toggle to match icon theme * chore: rename to forceFullColor * fix: default setting
This commit is contained in:
parent
29512081db
commit
cbd2d8a6bd
4 changed files with 180 additions and 144 deletions
|
|
@ -12,13 +12,15 @@ struct ImageEntry: TimelineEntry {
|
|||
var subtitle: String? = nil
|
||||
var error: WidgetError? = nil
|
||||
var deepLink: URL? = nil
|
||||
var forceFullColor: Bool = true
|
||||
}
|
||||
|
||||
static func build(
|
||||
api: ImmichAPI,
|
||||
asset: Asset,
|
||||
dateOffset: Int,
|
||||
subtitle: String? = nil
|
||||
subtitle: String? = nil,
|
||||
forceFullColor: Bool = true
|
||||
)
|
||||
async throws -> Self
|
||||
{
|
||||
|
|
@ -34,7 +36,8 @@ struct ImageEntry: TimelineEntry {
|
|||
image: image,
|
||||
metadata: EntryMetadata(
|
||||
subtitle: subtitle,
|
||||
deepLink: asset.deepLink
|
||||
deepLink: asset.deepLink,
|
||||
forceFullColor: forceFullColor
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -111,12 +114,26 @@ struct ImageEntry: TimelineEntry {
|
|||
|
||||
}
|
||||
|
||||
// Required so that we can decode older versions of the metadata
|
||||
extension ImageEntry.Metadata {
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
|
||||
error = try container.decodeIfPresent(WidgetError.self, forKey: .error)
|
||||
deepLink = try container.decodeIfPresent(URL.self, forKey: .deepLink)
|
||||
|
||||
forceFullColor =
|
||||
try container.decodeIfPresent(Bool.self, forKey: .forceFullColor) ?? true
|
||||
}
|
||||
}
|
||||
|
||||
func generateRandomEntries(
|
||||
api: ImmichAPI,
|
||||
now: Date,
|
||||
count: Int,
|
||||
filter: SearchFilter = Album.NONE.filter,
|
||||
subtitle: String? = nil
|
||||
subtitle: String? = nil,
|
||||
forceFullColor: Bool = true
|
||||
)
|
||||
async throws -> [ImageEntry]
|
||||
{
|
||||
|
|
@ -132,7 +149,8 @@ func generateRandomEntries(
|
|||
api: api,
|
||||
asset: asset,
|
||||
dateOffset: dateOffset,
|
||||
subtitle: subtitle
|
||||
subtitle: subtitle,
|
||||
forceFullColor: forceFullColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import WidgetKit
|
|||
|
||||
extension Image {
|
||||
@ViewBuilder
|
||||
func tintedWidgetImageModifier() -> some View {
|
||||
func tintedWidgetImageModifier(forceFullColor: Bool) -> some View {
|
||||
if #available(iOS 18.0, *) {
|
||||
self
|
||||
.widgetAccentedRenderingMode(.accentedDesaturated)
|
||||
.widgetAccentedRenderingMode(forceFullColor ? .accentedDesaturated : .fullColor)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
|
|
@ -18,15 +18,24 @@ struct ImmichWidgetView: View {
|
|||
|
||||
var body: some View {
|
||||
if let image = entry.image {
|
||||
ImmichWidgetContentView(image: image, subtitle: entry.metadata.subtitle, deepLink: entry.metadata.deepLink)
|
||||
ImmichWidgetContentView(
|
||||
image: image,
|
||||
subtitle: entry.metadata.subtitle,
|
||||
deepLink: entry.metadata.deepLink,
|
||||
forceFullColor: entry.metadata.forceFullColor
|
||||
)
|
||||
} else {
|
||||
ImmichWidgetLoadingView(message: entry.metadata.error?.errorDescription)
|
||||
ImmichWidgetLoadingView(
|
||||
message: entry.metadata.error?.errorDescription,
|
||||
forceFullColor: entry.metadata.forceFullColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ImmichWidgetLoadingView: View {
|
||||
let message: String?
|
||||
let forceFullColor: Bool
|
||||
|
||||
var body: some View {
|
||||
let messageText = Text(message ?? "")
|
||||
|
|
@ -39,7 +48,7 @@ private struct ImmichWidgetLoadingView: View {
|
|||
messageText.hidden()
|
||||
|
||||
Image("LaunchImage")
|
||||
.tintedWidgetImageModifier()
|
||||
.tintedWidgetImageModifier(forceFullColor: forceFullColor)
|
||||
|
||||
messageText
|
||||
}
|
||||
|
|
@ -50,13 +59,14 @@ private struct ImmichWidgetContentView: View {
|
|||
let image: UIImage
|
||||
let subtitle: String?
|
||||
let deepLink: URL?
|
||||
let forceFullColor: Bool
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
Color.clear.overlay(
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.tintedWidgetImageModifier()
|
||||
.tintedWidgetImageModifier(forceFullColor: forceFullColor)
|
||||
.scaledToFill()
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,21 @@ import AppIntents
|
|||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct ImmichMemoryProvider: TimelineProvider {
|
||||
// MARK: Widget Configuration
|
||||
|
||||
struct MemoryConfigurationAppIntent: WidgetConfigurationIntent {
|
||||
static var title: LocalizedStringResource { "Memories" }
|
||||
static var description: IntentDescription {
|
||||
"See memories from Immich."
|
||||
}
|
||||
|
||||
@Parameter(title: "Always Display in Full Color", default: false)
|
||||
var forceFullColor: Bool
|
||||
}
|
||||
|
||||
// MARK: Provider
|
||||
|
||||
struct ImmichMemoryProvider: AppIntentTimelineProvider {
|
||||
func getYearDifferenceSubtitle(assetYear: Int) -> String {
|
||||
let currentYear = Calendar.current.component(.year, from: Date.now)
|
||||
// construct a "X years ago" subtitle
|
||||
|
|
@ -15,24 +29,19 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||
ImageEntry(date: Date(), image: nil)
|
||||
}
|
||||
|
||||
func getSnapshot(
|
||||
in context: Context,
|
||||
completion: @escaping @Sendable (ImageEntry) -> Void
|
||||
) {
|
||||
func snapshot(
|
||||
for configuration: MemoryConfigurationAppIntent,
|
||||
in context: Context
|
||||
) async -> ImageEntry {
|
||||
let cacheKey = "memory_\(context.family.rawValue)"
|
||||
|
||||
Task {
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
completion(
|
||||
ImageEntry.handleError(for: cacheKey, error: .noLogin).entries.first!
|
||||
)
|
||||
return
|
||||
return ImageEntry.handleError(for: cacheKey, error: .noLogin).entries
|
||||
.first!
|
||||
}
|
||||
|
||||
guard let memories = try? await api.fetchMemory(for: Date.now)
|
||||
else {
|
||||
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
||||
return
|
||||
guard let memories = try? await api.fetchMemory(for: Date.now) else {
|
||||
return ImageEntry.handleError(for: cacheKey).entries.first!
|
||||
}
|
||||
|
||||
for memory in memories {
|
||||
|
|
@ -41,11 +50,11 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||
api: api,
|
||||
asset: asset,
|
||||
dateOffset: 0,
|
||||
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
|
||||
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year),
|
||||
forceFullColor: configuration.forceFullColor
|
||||
)
|
||||
{
|
||||
completion(entry)
|
||||
return
|
||||
return entry
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,32 +64,29 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||
let imageEntry = try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: randomImage,
|
||||
dateOffset: 0
|
||||
dateOffset: 0,
|
||||
forceFullColor: configuration.forceFullColor
|
||||
)
|
||||
else {
|
||||
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
||||
return
|
||||
return ImageEntry.handleError(for: cacheKey).entries.first!
|
||||
}
|
||||
|
||||
completion(imageEntry)
|
||||
}
|
||||
return imageEntry
|
||||
}
|
||||
|
||||
func getTimeline(
|
||||
in context: Context,
|
||||
completion: @escaping @Sendable (Timeline<ImageEntry>) -> Void
|
||||
) {
|
||||
Task {
|
||||
func timeline(
|
||||
for configuration: MemoryConfigurationAppIntent,
|
||||
in context: Context
|
||||
) async
|
||||
-> Timeline<ImageEntry>
|
||||
{
|
||||
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
|
||||
return ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
||||
}
|
||||
|
||||
let memories: [MemoryResult]
|
||||
|
|
@ -100,7 +106,8 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||
dateOffset: totalAssets,
|
||||
subtitle: getYearDifferenceSubtitle(
|
||||
assetYear: memory.data.year
|
||||
)
|
||||
),
|
||||
forceFullColor: configuration.forceFullColor
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -117,8 +124,7 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||
}
|
||||
|
||||
} catch {
|
||||
completion(ImageEntry.handleError(for: cacheKey))
|
||||
return
|
||||
return ImageEntry.handleError(for: cacheKey)
|
||||
}
|
||||
|
||||
// If we didn't add any memory images (some failure occurred or no images in memory),
|
||||
|
|
@ -130,29 +136,25 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||
let search = try await generateRandomEntries(
|
||||
api: api,
|
||||
now: now,
|
||||
count: 12
|
||||
count: 12,
|
||||
forceFullColor: configuration.forceFullColor
|
||||
)
|
||||
|
||||
// Load or save a cached asset for when network conditions are bad
|
||||
if search.count == 0 {
|
||||
completion(
|
||||
ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
||||
)
|
||||
return
|
||||
return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
||||
}
|
||||
|
||||
entries.append(contentsOf: search)
|
||||
} catch {
|
||||
completion(ImageEntry.handleError(for: cacheKey))
|
||||
return
|
||||
return ImageEntry.handleError(for: cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
// cache the last image
|
||||
try? entries.last!.cache(for: cacheKey)
|
||||
|
||||
completion(Timeline(entries: entries, policy: .atEnd))
|
||||
}
|
||||
return Timeline(entries: entries, policy: .atEnd)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -160,8 +162,9 @@ struct ImmichMemoryWidget: Widget {
|
|||
let kind: String = "com.immich.widget.memory"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(
|
||||
AppIntentConfiguration(
|
||||
kind: kind,
|
||||
intent: MemoryConfigurationAppIntent.self,
|
||||
provider: ImmichMemoryProvider()
|
||||
) { entry in
|
||||
ImmichWidgetView(entry: entry)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
|||
|
||||
@Parameter(title: "Show Album Name", default: false)
|
||||
var showAlbumName: Bool
|
||||
|
||||
@Parameter(title: "Always Display in Full Color", default: false)
|
||||
var forceFullColor: Bool
|
||||
}
|
||||
|
||||
// MARK: Provider
|
||||
|
|
@ -76,7 +79,8 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
|||
let entry = try? await ImageEntry.build(
|
||||
api: api,
|
||||
asset: randomImage,
|
||||
dateOffset: 0
|
||||
dateOffset: 0,
|
||||
forceFullColor: configuration.forceFullColor
|
||||
)
|
||||
else {
|
||||
return ImageEntry.handleError(for: cacheKey).entries.first!
|
||||
|
|
@ -114,7 +118,8 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
|||
now: now,
|
||||
count: 12,
|
||||
filter: album.filter,
|
||||
subtitle: configuration.showAlbumName ? albumName : nil
|
||||
subtitle: configuration.showAlbumName ? albumName : nil,
|
||||
forceFullColor: configuration.forceFullColor
|
||||
)
|
||||
|
||||
// Load or save a cached asset for when network conditions are bad
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue