1
0
mirror of https://github.com/immich-app/immich.git synced 2025-07-09 06:25:41 +02:00
Files
immich/mobile/ios/WidgetExtension/ImmichAPI.swift

220 lines
5.1 KiB
Swift
Raw Normal View History

feat(mobile): ios widgets (#19148) * 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>
2025-06-17 09:43:09 -05:00
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
}
}