mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	Found via `codespell -q 3 -S "*.svg,./i18n,./docs/package-lock.json,./readme_i18n,./mobile/assets/i18n" -L afterall,devlop,finaly,inout,nd,optin,renderd,sade`
		
			
				
	
	
		
			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 didn't add any memory images (some failure occurred 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.")
 | 
						|
  }
 | 
						|
}
 |