mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat(widgets): iOS widget improvements (#19893)
* 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)
This commit is contained in:
parent
34620e1e9a
commit
743b6644e9
@ -14,19 +14,6 @@ struct ImageEntry: TimelineEntry {
|
|||||||
var deepLink: URL? = nil
|
var deepLink: URL? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resizes the stored image to a maximum width of 450 pixels
|
|
||||||
mutating func resize() {
|
|
||||||
if image == nil || image!.size.height < 450 || image!.size.width < 450 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
image = image?.resized(toWidth: 450)
|
|
||||||
|
|
||||||
if image == nil {
|
|
||||||
metadata.error = .unableToResize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func build(
|
static func build(
|
||||||
api: ImmichAPI,
|
api: ImmichAPI,
|
||||||
asset: Asset,
|
asset: Asset,
|
||||||
@ -83,7 +70,10 @@ struct ImageEntry: TimelineEntry {
|
|||||||
|
|
||||||
guard let imageData = try? Data(contentsOf: imageURL),
|
guard let imageData = try? Data(contentsOf: imageURL),
|
||||||
let metadataJSON = try? Data(contentsOf: metadataURL),
|
let metadataJSON = try? Data(contentsOf: metadataURL),
|
||||||
let decodedMetadata = try? JSONDecoder().decode(Metadata.self, from: metadataJSON)
|
let decodedMetadata = try? JSONDecoder().decode(
|
||||||
|
Metadata.self,
|
||||||
|
from: metadataJSON
|
||||||
|
)
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -98,7 +88,7 @@ struct ImageEntry: TimelineEntry {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
static func handleCacheFallback(
|
static func handleError(
|
||||||
for key: String,
|
for key: String,
|
||||||
error: WidgetError = .fetchFailed
|
error: WidgetError = .fetchFailed
|
||||||
) -> Timeline<ImageEntry> {
|
) -> Timeline<ImageEntry> {
|
||||||
@ -108,35 +98,32 @@ struct ImageEntry: TimelineEntry {
|
|||||||
metadata: EntryMetadata(error: error)
|
metadata: EntryMetadata(error: error)
|
||||||
)
|
)
|
||||||
|
|
||||||
// skip cache if album not found or no login
|
// use cache if generic failed error
|
||||||
// we want to show these errors to the user since without intervention,
|
// we want to show the other errors to the user since without intervention,
|
||||||
// it will never succeed
|
// it will never succeed
|
||||||
if error != .noLogin && error != .albumNotFound {
|
if error == .fetchFailed, let cachedEntry = ImageEntry.loadCached(for: key)
|
||||||
if let cachedEntry = ImageEntry.loadCached(for: key) {
|
{
|
||||||
timelineEntry = cachedEntry
|
timelineEntry = cachedEntry
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return Timeline(entries: [timelineEntry], policy: .atEnd)
|
return Timeline(entries: [timelineEntry], policy: .atEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateRandomEntries(
|
func generateRandomEntries(
|
||||||
api: ImmichAPI,
|
api: ImmichAPI,
|
||||||
now: Date,
|
now: Date,
|
||||||
count: Int,
|
count: Int,
|
||||||
albumId: String? = nil,
|
filter: SearchFilter = Album.NONE.filter,
|
||||||
subtitle: String? = nil
|
subtitle: String? = nil
|
||||||
)
|
)
|
||||||
async throws -> [ImageEntry]
|
async throws -> [ImageEntry]
|
||||||
{
|
{
|
||||||
|
|
||||||
var entries: [ImageEntry] = []
|
var entries: [ImageEntry] = []
|
||||||
let albumIds = albumId != nil ? [albumId!] : []
|
|
||||||
|
|
||||||
let randomAssets = try await api.fetchSearchResults(
|
let randomAssets = try await api.fetchSearchResults(with: filter)
|
||||||
with: SearchFilters(size: count, albumIds: albumIds)
|
|
||||||
)
|
|
||||||
|
|
||||||
await withTaskGroup(of: ImageEntry?.self) { group in
|
await withTaskGroup(of: ImageEntry?.self) { group in
|
||||||
for (dateOffset, asset) in randomAssets.enumerated() {
|
for (dateOffset, asset) in randomAssets.enumerated() {
|
||||||
|
@ -1,6 +1,18 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
|
extension Image {
|
||||||
|
@ViewBuilder
|
||||||
|
func tintedWidgetImageModifier() -> some View {
|
||||||
|
if #available(iOS 18.0, *) {
|
||||||
|
self
|
||||||
|
.widgetAccentedRenderingMode(.accentedDesaturated)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ImmichWidgetView: View {
|
struct ImmichWidgetView: View {
|
||||||
var entry: ImageEntry
|
var entry: ImageEntry
|
||||||
|
|
||||||
@ -8,6 +20,7 @@ struct ImmichWidgetView: View {
|
|||||||
if entry.image == nil {
|
if entry.image == nil {
|
||||||
VStack {
|
VStack {
|
||||||
Image("LaunchImage")
|
Image("LaunchImage")
|
||||||
|
.tintedWidgetImageModifier()
|
||||||
Text(entry.metadata.error?.errorDescription ?? "")
|
Text(entry.metadata.error?.errorDescription ?? "")
|
||||||
.minimumScaleFactor(0.25)
|
.minimumScaleFactor(0.25)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@ -19,7 +32,9 @@ struct ImmichWidgetView: View {
|
|||||||
Color.clear.overlay(
|
Color.clear.overlay(
|
||||||
Image(uiImage: entry.image!)
|
Image(uiImage: entry.image!)
|
||||||
.resizable()
|
.resizable()
|
||||||
|
.tintedWidgetImageModifier()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
|
|
||||||
)
|
)
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -2,14 +2,20 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
|
let IMMICH_SHARE_GROUP = "group.app.immich.share"
|
||||||
|
|
||||||
enum WidgetError: Error, Codable {
|
enum WidgetError: Error, Codable {
|
||||||
case noLogin
|
case noLogin
|
||||||
case fetchFailed
|
case fetchFailed
|
||||||
case unknown
|
|
||||||
case albumNotFound
|
case albumNotFound
|
||||||
|
case noAssetsAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FetchError: Error {
|
||||||
case unableToResize
|
case unableToResize
|
||||||
case invalidImage
|
case invalidImage
|
||||||
case invalidURL
|
case invalidURL
|
||||||
|
case fetchFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WidgetError: LocalizedError {
|
extension WidgetError: LocalizedError {
|
||||||
@ -24,14 +30,8 @@ extension WidgetError: LocalizedError {
|
|||||||
case .albumNotFound:
|
case .albumNotFound:
|
||||||
return "Album not found"
|
return "Album not found"
|
||||||
|
|
||||||
case .invalidURL:
|
case .noAssetsAvailable:
|
||||||
return "An invalid URL was used"
|
return "No assets available"
|
||||||
|
|
||||||
case .invalidImage:
|
|
||||||
return "An invalid image was received"
|
|
||||||
|
|
||||||
default:
|
|
||||||
return "An unknown error occured"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,10 +52,11 @@ struct Asset: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SearchFilters: Codable {
|
struct SearchFilter: Codable {
|
||||||
var type: AssetType = .image
|
var type = AssetType.image
|
||||||
let size: Int
|
var size = 1
|
||||||
var albumIds: [String] = []
|
var albumIds: [String] = []
|
||||||
|
var isFavorite: Bool? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MemoryResult: Codable {
|
struct MemoryResult: Codable {
|
||||||
@ -70,12 +71,35 @@ struct MemoryResult: Codable {
|
|||||||
let data: MemoryData
|
let data: MemoryData
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Album: Codable {
|
struct Album: Codable, Equatable {
|
||||||
let id: String
|
let id: String
|
||||||
let albumName: String
|
let albumName: String
|
||||||
}
|
|
||||||
|
|
||||||
let IMMICH_SHARE_GROUP = "group.app.immich.share"
|
static let NONE = Album(id: "NONE", albumName: "None")
|
||||||
|
static let FAVORITES = Album(id: "FAVORITES", albumName: "Favorites")
|
||||||
|
|
||||||
|
var filter: SearchFilter {
|
||||||
|
switch self {
|
||||||
|
case Album.NONE:
|
||||||
|
return SearchFilter()
|
||||||
|
case Album.FAVORITES:
|
||||||
|
return SearchFilter(isFavorite: true)
|
||||||
|
|
||||||
|
// regular album
|
||||||
|
default:
|
||||||
|
return SearchFilter(albumIds: [id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isVirtual: Bool {
|
||||||
|
switch self {
|
||||||
|
case Album.NONE, Album.FAVORITES:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: API
|
// MARK: API
|
||||||
|
|
||||||
@ -132,7 +156,8 @@ class ImmichAPI {
|
|||||||
return components?.url
|
return components?.url
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchSearchResults(with filters: SearchFilters) async throws
|
func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter)
|
||||||
|
async throws
|
||||||
-> [Asset]
|
-> [Asset]
|
||||||
{
|
{
|
||||||
// get URL
|
// get URL
|
||||||
@ -178,7 +203,7 @@ class ImmichAPI {
|
|||||||
return try JSONDecoder().decode([MemoryResult].self, from: data)
|
return try JSONDecoder().decode([MemoryResult].self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchImage(asset: Asset) async throws(WidgetError) -> UIImage {
|
func fetchImage(asset: Asset) async throws(FetchError) -> UIImage {
|
||||||
let thumbnailParams = [URLQueryItem(name: "size", value: "preview")]
|
let thumbnailParams = [URLQueryItem(name: "size", value: "preview")]
|
||||||
let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
|
let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
|
||||||
|
|
||||||
@ -199,7 +224,7 @@ class ImmichAPI {
|
|||||||
|
|
||||||
let decodeOptions: [NSString: Any] = [
|
let decodeOptions: [NSString: Any] = [
|
||||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||||
kCGImageSourceThumbnailMaxPixelSize: 400,
|
kCGImageSourceThumbnailMaxPixelSize: 512,
|
||||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -23,26 +23,27 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
|
|
||||||
Task {
|
Task {
|
||||||
guard let api = try? await ImmichAPI() else {
|
guard let api = try? await ImmichAPI() else {
|
||||||
completion(ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin).entries.first!)
|
completion(
|
||||||
|
ImageEntry.handleError(for: cacheKey, error: .noLogin).entries.first!
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let memories = try? await api.fetchMemory(for: Date.now)
|
guard let memories = try? await api.fetchMemory(for: Date.now)
|
||||||
else {
|
else {
|
||||||
completion(ImageEntry.handleCacheFallback(for: cacheKey).entries.first!)
|
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for memory in memories {
|
for memory in memories {
|
||||||
if let asset = memory.assets.first(where: { $0.type == .image }),
|
if let asset = memory.assets.first(where: { $0.type == .image }),
|
||||||
var entry = try? await ImageEntry.build(
|
let entry = try? await ImageEntry.build(
|
||||||
api: api,
|
api: api,
|
||||||
asset: asset,
|
asset: asset,
|
||||||
dateOffset: 0,
|
dateOffset: 0,
|
||||||
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
|
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
entry.resize()
|
|
||||||
completion(entry)
|
completion(entry)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -50,20 +51,17 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
|
|
||||||
// fallback to random image
|
// fallback to random image
|
||||||
guard
|
guard
|
||||||
let randomImage = try? await api.fetchSearchResults(
|
let randomImage = try? await api.fetchSearchResults().first,
|
||||||
with: SearchFilters(size: 1)
|
let imageEntry = try? await ImageEntry.build(
|
||||||
).first,
|
|
||||||
var imageEntry = try? await ImageEntry.build(
|
|
||||||
api: api,
|
api: api,
|
||||||
asset: randomImage,
|
asset: randomImage,
|
||||||
dateOffset: 0
|
dateOffset: 0
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
completion(ImageEntry.handleCacheFallback(for: cacheKey).entries.first!)
|
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
imageEntry.resize()
|
|
||||||
completion(imageEntry)
|
completion(imageEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,7 +78,7 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
|
|
||||||
guard let api = try? await ImmichAPI() else {
|
guard let api = try? await ImmichAPI() else {
|
||||||
completion(
|
completion(
|
||||||
ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin)
|
ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -119,25 +117,28 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
// If we didnt add any memory images (some failure occured or no images in memory),
|
// If we didnt add any memory images (some failure occured or no images in memory),
|
||||||
// default to 12 hours of random photos
|
// default to 12 hours of random photos
|
||||||
if entries.count == 0 {
|
if entries.count == 0 {
|
||||||
entries.append(
|
// this must be a do/catch since we need to
|
||||||
contentsOf: (try? await generateRandomEntries(
|
// distinguish between a network fail and an empty search
|
||||||
|
do {
|
||||||
|
let search = try await generateRandomEntries(
|
||||||
api: api,
|
api: api,
|
||||||
now: now,
|
now: now,
|
||||||
count: 12
|
count: 12
|
||||||
)) ?? []
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// If we fail to fetch images, we still want to add an entry
|
// Load or save a cached asset for when network conditions are bad
|
||||||
// with a nil image and an error
|
if search.count == 0 {
|
||||||
if entries.count == 0 {
|
completion(
|
||||||
completion(ImageEntry.handleCacheFallback(for: cacheKey))
|
ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize all images to something that can be stored by iOS
|
entries.append(contentsOf: search)
|
||||||
for i in entries.indices {
|
} catch {
|
||||||
entries[i].resize()
|
completion(ImageEntry.handleError(for: cacheKey))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache the last image
|
// cache the last image
|
||||||
|
@ -8,20 +8,21 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
|
|||||||
|
|
||||||
struct AlbumQuery: EntityQuery {
|
struct AlbumQuery: EntityQuery {
|
||||||
func entities(for identifiers: [Album.ID]) async throws -> [Album] {
|
func entities(for identifiers: [Album.ID]) async throws -> [Album] {
|
||||||
// use cached albums to search
|
return await suggestedEntities().filter {
|
||||||
var albums = (try? await AlbumCache.shared.getAlbums()) ?? []
|
|
||||||
albums.insert(NO_ALBUM, at: 0)
|
|
||||||
|
|
||||||
return albums.filter {
|
|
||||||
identifiers.contains($0.id)
|
identifiers.contains($0.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func suggestedEntities() async throws -> [Album] {
|
func suggestedEntities() async -> [Album] {
|
||||||
var albums = (try? await AlbumCache.shared.getAlbums(refresh: true)) ?? []
|
let albums = (try? await AlbumCache.shared.getAlbums()) ?? []
|
||||||
albums.insert(NO_ALBUM, at: 0)
|
|
||||||
|
|
||||||
return albums
|
let options =
|
||||||
|
[
|
||||||
|
NONE,
|
||||||
|
FAVORITES,
|
||||||
|
] + albums
|
||||||
|
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,8 +36,6 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let NO_ALBUM = Album(id: "NONE", albumName: "None")
|
|
||||||
|
|
||||||
struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
||||||
static var title: LocalizedStringResource { "Select Album" }
|
static var title: LocalizedStringResource { "Select Album" }
|
||||||
static var description: IntentDescription {
|
static var description: IntentDescription {
|
||||||
@ -66,24 +65,23 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
|||||||
let cacheKey = "random_none_\(context.family.rawValue)"
|
let cacheKey = "random_none_\(context.family.rawValue)"
|
||||||
|
|
||||||
guard let api = try? await ImmichAPI() else {
|
guard let api = try? await ImmichAPI() else {
|
||||||
return ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin).entries.first!
|
return ImageEntry.handleError(for: cacheKey, error: .noLogin).entries
|
||||||
|
.first!
|
||||||
}
|
}
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let randomImage = try? await api.fetchSearchResults(
|
let randomImage = try? await api.fetchSearchResults(
|
||||||
with: SearchFilters(size: 1)
|
with: Album.NONE.filter
|
||||||
).first,
|
).first,
|
||||||
var entry = try? await ImageEntry.build(
|
let entry = try? await ImageEntry.build(
|
||||||
api: api,
|
api: api,
|
||||||
asset: randomImage,
|
asset: randomImage,
|
||||||
dateOffset: 0
|
dateOffset: 0
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
return ImageEntry.handleCacheFallback(for: cacheKey).entries.first!
|
return ImageEntry.handleError(for: cacheKey).entries.first!
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.resize()
|
|
||||||
|
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,52 +95,36 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
|||||||
let now = Date()
|
let now = Date()
|
||||||
|
|
||||||
// nil if album is NONE or nil
|
// nil if album is NONE or nil
|
||||||
let albumId =
|
let album = configuration.album ?? Album.NONE
|
||||||
configuration.album?.id != "NONE" ? configuration.album?.id : nil
|
let albumName = album.isVirtual ? nil : album.albumName
|
||||||
let albumName: String? =
|
|
||||||
albumId != nil ? configuration.album?.albumName : nil
|
|
||||||
|
|
||||||
let cacheKey = "random_\(albumId ?? "none")_\(context.family.rawValue)"
|
let cacheKey = "random_\(album.id)_\(context.family.rawValue)"
|
||||||
|
|
||||||
// If we don't have a server config, return an entry with an error
|
// If we don't have a server config, return an entry with an error
|
||||||
guard let api = try? await ImmichAPI() else {
|
guard let api = try? await ImmichAPI() else {
|
||||||
return ImageEntry.handleCacheFallback(for: cacheKey, error: .noLogin)
|
return ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
||||||
}
|
|
||||||
|
|
||||||
if albumId != nil {
|
|
||||||
// make sure the album exists on server, otherwise show error
|
|
||||||
guard let albums = try? await api.fetchAlbums() else {
|
|
||||||
return ImageEntry.handleCacheFallback(for: cacheKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !albums.contains(where: { $0.id == albumId }) {
|
|
||||||
return ImageEntry.handleCacheFallback(
|
|
||||||
for: cacheKey,
|
|
||||||
error: .albumNotFound
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// build entries
|
// build entries
|
||||||
entries.append(
|
// this must be a do/catch since we need to
|
||||||
contentsOf: (try? await generateRandomEntries(
|
// distinguish between a network fail and an empty search
|
||||||
|
do {
|
||||||
|
let search = try await generateRandomEntries(
|
||||||
api: api,
|
api: api,
|
||||||
now: now,
|
now: now,
|
||||||
count: 12,
|
count: 12,
|
||||||
albumId: albumId,
|
filter: album.filter,
|
||||||
subtitle: configuration.showAlbumName ? albumName : nil
|
subtitle: configuration.showAlbumName ? albumName : nil
|
||||||
))
|
|
||||||
?? []
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Load or save a cached asset for when network conditions are bad
|
// Load or save a cached asset for when network conditions are bad
|
||||||
if entries.count == 0 {
|
if search.count == 0 {
|
||||||
return ImageEntry.handleCacheFallback(for: cacheKey)
|
return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize all images to something that can be stored by iOS
|
entries.append(contentsOf: search)
|
||||||
for i in entries.indices {
|
} catch {
|
||||||
entries[i].resize()
|
return ImageEntry.handleError(for: cacheKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache the last image
|
// cache the last image
|
||||||
|
Loading…
x
Reference in New Issue
Block a user