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>
220 lines
5.1 KiB
Swift
220 lines
5.1 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
import WidgetKit
|
|
|
|
enum WidgetError: Error {
|
|
case noLogin
|
|
case fetchFailed
|
|
case unknown
|
|
case albumNotFound
|
|
case unableToResize
|
|
}
|
|
|
|
enum AssetType: String, Codable {
|
|
case image = "IMAGE"
|
|
case video = "VIDEO"
|
|
case audio = "AUDIO"
|
|
case other = "OTHER"
|
|
}
|
|
|
|
struct SearchResult: Codable {
|
|
let id: String
|
|
let type: AssetType
|
|
}
|
|
|
|
struct SearchFilters: Codable {
|
|
var type: AssetType = .image
|
|
let size: Int
|
|
var albumIds: [String] = []
|
|
}
|
|
|
|
struct MemoryResult: Codable {
|
|
let id: String
|
|
var assets: [SearchResult]
|
|
let type: String
|
|
|
|
struct MemoryData: Codable {
|
|
let year: Int
|
|
}
|
|
|
|
let data: MemoryData
|
|
}
|
|
|
|
struct Album: Codable {
|
|
let id: String
|
|
let albumName: String
|
|
}
|
|
|
|
// MARK: API
|
|
|
|
class ImmichAPI {
|
|
struct ServerConfig {
|
|
let serverEndpoint: String
|
|
let sessionKey: String
|
|
}
|
|
let serverConfig: ServerConfig
|
|
|
|
init() async throws {
|
|
// fetch the credentials from the UserDefaults store that dart placed here
|
|
guard let defaults = UserDefaults(suiteName: "group.app.immich.share"),
|
|
let serverURL = defaults.string(forKey: "widget_server_url"),
|
|
let sessionKey = defaults.string(forKey: "widget_auth_token")
|
|
else {
|
|
throw WidgetError.noLogin
|
|
}
|
|
|
|
if serverURL == "" || sessionKey == "" {
|
|
throw WidgetError.noLogin
|
|
}
|
|
|
|
serverConfig = ServerConfig(
|
|
serverEndpoint: serverURL,
|
|
sessionKey: sessionKey
|
|
)
|
|
}
|
|
|
|
private func buildRequestURL(
|
|
serverConfig: ServerConfig,
|
|
endpoint: String,
|
|
params: [URLQueryItem] = []
|
|
) -> URL? {
|
|
guard let baseURL = URL(string: serverConfig.serverEndpoint) else {
|
|
fatalError("Invalid base URL")
|
|
}
|
|
|
|
// Combine the base URL and API path
|
|
let fullPath = baseURL.appendingPathComponent(
|
|
endpoint.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
|
)
|
|
|
|
// Add the session key as a query parameter
|
|
var components = URLComponents(
|
|
url: fullPath,
|
|
resolvingAgainstBaseURL: false
|
|
)
|
|
components?.queryItems = [
|
|
URLQueryItem(name: "sessionKey", value: serverConfig.sessionKey)
|
|
]
|
|
components?.queryItems?.append(contentsOf: params)
|
|
|
|
return components?.url
|
|
}
|
|
|
|
func fetchSearchResults(with filters: SearchFilters) async throws
|
|
-> [SearchResult]
|
|
{
|
|
// get URL
|
|
guard
|
|
let searchURL = buildRequestURL(
|
|
serverConfig: serverConfig,
|
|
endpoint: "/search/random"
|
|
)
|
|
else {
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
var request = URLRequest(url: searchURL)
|
|
request.httpMethod = "POST"
|
|
request.httpBody = try JSONEncoder().encode(filters)
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
|
|
let (data, _) = try await URLSession.shared.data(for: request)
|
|
|
|
// decode data
|
|
return try JSONDecoder().decode([SearchResult].self, from: data)
|
|
}
|
|
|
|
func fetchMemory(for date: Date) async throws -> [MemoryResult] {
|
|
// get URL
|
|
let memoryParams = [URLQueryItem(name: "for", value: date.ISO8601Format())]
|
|
guard
|
|
let searchURL = buildRequestURL(
|
|
serverConfig: serverConfig,
|
|
endpoint: "/memories",
|
|
params: memoryParams
|
|
)
|
|
else {
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
var request = URLRequest(url: searchURL)
|
|
request.httpMethod = "GET"
|
|
|
|
let (data, _) = try await URLSession.shared.data(for: request)
|
|
|
|
// decode data
|
|
return try JSONDecoder().decode([MemoryResult].self, from: data)
|
|
}
|
|
|
|
func fetchImage(asset: SearchResult) async throws -> UIImage {
|
|
let thumbnailParams = [URLQueryItem(name: "size", value: "preview")]
|
|
let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
|
|
|
|
guard
|
|
let fetchURL = buildRequestURL(
|
|
serverConfig: serverConfig,
|
|
endpoint: assetEndpoint,
|
|
params: thumbnailParams
|
|
)
|
|
else {
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
let (data, _) = try await URLSession.shared.data(from: fetchURL)
|
|
|
|
guard let img = UIImage(data: data) else {
|
|
throw URLError(.badServerResponse)
|
|
}
|
|
|
|
return img
|
|
}
|
|
|
|
func fetchAlbums() async throws -> [Album] {
|
|
// get URL
|
|
guard
|
|
let searchURL = buildRequestURL(
|
|
serverConfig: serverConfig,
|
|
endpoint: "/albums"
|
|
)
|
|
else {
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
var request = URLRequest(url: searchURL)
|
|
request.httpMethod = "GET"
|
|
|
|
let (data, _) = try await URLSession.shared.data(for: request)
|
|
|
|
// decode data
|
|
return try JSONDecoder().decode([Album].self, from: data)
|
|
}
|
|
}
|
|
|
|
// We need a shared cache for albums to efficiently handle the album picker queries
|
|
actor AlbumCache {
|
|
static let shared = AlbumCache()
|
|
|
|
private var api: ImmichAPI? = nil
|
|
private var albums: [Album]? = nil
|
|
|
|
func getAlbums(refresh: Bool = false) async throws -> [Album] {
|
|
// Check the API before we try to show cached albums
|
|
// Sometimes iOS caches this object and keeps it around
|
|
// even after nuking the timeline
|
|
|
|
api = try? await ImmichAPI()
|
|
|
|
guard api != nil else {
|
|
throw WidgetError.noLogin
|
|
}
|
|
|
|
if let albums, !refresh {
|
|
return albums
|
|
}
|
|
|
|
let fetched = try await api!.fetchAlbums()
|
|
albums = fetched
|
|
return fetched
|
|
}
|
|
}
|