Compare commits

..

13 Commits

Author SHA1 Message Date
github-actions
bee49cef02 chore: version v2.7.4 2026-04-10 16:32:26 +00:00
shenlong
6d0c6a4008 chore: pump cronet version (#27685)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-04-10 16:29:05 +00:00
Luis Nachtigall
8a975e5ea9 refactor(mobile): cleanup iOS image loading pipeline (#27672)
* refactor: replace DispatchQueue + DispatchSemaphore with OperationQueue for image processing

* implement RequestRegistry and UnfairLock for managing cancellable requests

* implement requests registry for local and remote image processing

* remove Cancellable protocol and cancel method from request registry

* refactor: introduce ImageRequest base class with unified cancellation and finish helpers

* refactor: add get method to RequestRegistry and streamline request removal in image processing

* add guard to cancel to prevent double onCancel calls

* fix duplicate code merge issue

* refactor(ios): enhance finish method to return callback status

* remove unfitting methods form ImageRequest.swift and fix memory issue

* revert bad merge

* refactor(ios): resolve cancellation issues

* refactor(ios): streamline image request completion handling

* add return statements

* refactor(ios): simplify image request cancellation and registry handling

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-04-10 10:56:35 -05:00
Luis Nachtigall
d39e7da10d fix(mobile): fix flutter cache eviction on thumbnails (#27663)
* fix: add markFinished parameter to loadRequest and loadCodecRequest methods

* update loadRequest and loadCodecRequest methods to use isFinal

* Apply suggestions from code review

Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>

* remove redundant check

* fix: ensure isFinished is set correctly during cache eviction

* formatting

---------

Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
2026-04-10 10:28:55 -05:00
Daniel Dietzler
bc400d68ac chore: move .tsbuildinfo file to dist folder (#27682) 2026-04-10 16:02:25 +02:00
renovate[bot]
d7f038ec60 chore(deps): update dependency eslint-plugin-unicorn to v64 (#27575)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-04-10 10:23:42 +00:00
Mees Frensel
26957f37ce fix(server): hide original filename when not showing metadata (#27581) 2026-04-10 12:07:18 +02:00
github-actions
3254d31cd2 chore: version v2.7.3 2026-04-09 17:51:40 +00:00
Jason Rasmussen
7b269d1638 fix: ssr open graph tags (#27639)
fix: SSR open graph tags
2026-04-09 12:16:41 -04:00
Luis Nachtigall
b5bed02300 fix(mobile): get provider refs before async gaps in backup page (#27597)
* fix(mobile): get provider refs before async gaps in backup page

* fix(mobile): use previously created provider refs in start backup function
2026-04-08 20:55:53 -05:00
Zack Pollard
5553910236 fix(web): don't cache empty search results for people search (#27632) 2026-04-09 02:33:04 +01:00
Zack Pollard
8d67c1f820 fix(server): people search not showing for 3 or less characters (#27629)
Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
2026-04-09 01:56:07 +01:00
Matthew Momjian
ed0ec30917 fix(docs): updated docker deprecation link (#27633)
new link
2026-04-08 20:33:11 -04:00
43 changed files with 267 additions and 653 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.7.2",
"version": "2.7.4",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
@@ -28,7 +28,7 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^63.0.0",
"eslint-plugin-unicorn": "^64.0.0",
"globals": "^17.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.7.4",

View File

@@ -19,6 +19,7 @@
"paths": {
"src/*": ["./src/*"],
},
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
"types": ["vitest/globals"]
},
"exclude": ["dist", "node_modules", "vite.config.ts"]

View File

@@ -49,7 +49,7 @@ Immich requires [**Docker**](https://docs.docker.com/get-started/get-docker/) wi
The Compose plugin will be installed by both Docker Engine and Desktop by following the linked installation guides; it can also be [separately installed](https://docs.docker.com/compose/install/).
:::note
Immich requires the command `docker compose`; the similarly named `docker-compose` is [deprecated](https://docs.docker.com/compose/migrate/) and is no longer supported by Immich.
Immich requires the command `docker compose`; the similarly named `docker-compose` is [deprecated](https://docs.docker.com/retired/#docker-compose-v1-replaced-by-compose-v2) and is no longer supported by Immich.
:::
### Special requirements for Windows users

View File

@@ -1,7 +1,7 @@
[
{
"label": "v2.7.2",
"url": "https://docs.v2.7.2.archive.immich.app"
"label": "v2.7.4",
"url": "https://docs.v2.7.4.archive.immich.app"
},
{
"label": "v2.6.3",

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "2.7.2",
"version": "2.7.4",
"description": "",
"main": "index.js",
"type": "module",
@@ -40,7 +40,7 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^63.0.0",
"eslint-plugin-unicorn": "^64.0.0",
"exiftool-vendored": "^35.0.0",
"globals": "^17.0.0",
"luxon": "^3.4.4",

View File

@@ -1,6 +1,6 @@
{
"name": "immich-i18n",
"version": "2.7.2",
"version": "2.7.4",
"private": true,
"scripts": {
"format": "prettier --cache --check .",

View File

@@ -1,6 +1,6 @@
[project]
name = "immich-ml"
version = "2.7.2"
version = "2.7.4"
description = ""
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
requires-python = ">=3.11,<4.0"

View File

@@ -898,7 +898,7 @@ wheels = [
[[package]]
name = "immich-ml"
version = "2.7.2"
version = "2.7.4"
source = { editable = "." }
dependencies = [
{ name = "aiocache" },

View File

@@ -113,8 +113,8 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation 'org.chromium.net:cronet-embedded:143.7445.0'
implementation("androidx.media3:media3-datasource-okhttp:1.9.2")
implementation("androidx.media3:media3-datasource-cronet:1.9.2")
implementation("androidx.media3:media3-datasource-okhttp:1.10.0")
implementation("androidx.media3:media3-datasource-cronet:1.10.0")
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 3043,
"android.injected.version.name" => "2.7.2",
"android.injected.version.code" => 3045,
"android.injected.version.name" => "2.7.4",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -146,7 +146,7 @@ class URLSessionManager: NSObject {
private static func buildSession(delegate: URLSessionManagerDelegate) -> URLSession {
let config = URLSessionConfiguration.default
// config.urlCache = urlCache
config.urlCache = urlCache
config.httpCookieStorage = cookieStorage
config.httpMaximumConnectionsPerHost = 64
config.timeoutIntervalForRequest = 60

View File

@@ -1,5 +1,32 @@
import Foundation
class ImageRequest: @unchecked Sendable {
private struct State: Sendable {
var isCancelled = false
}
let completion: @Sendable (Result<[String: Int64]?, any Error>) -> Void
private let state: Mutex<State>
var isCancelled: Bool {
get {
state.withLock { $0.isCancelled }
}
set {
state.withLock { $0.isCancelled = newValue }
}
}
init(completion: @escaping @Sendable (Result<[String: Int64]?, any Error>) -> Void) {
self.state = Mutex(State())
self.completion = completion
}
func cancel() {
isCancelled = true
}
}
struct RequestRegistry<T: AnyObject & Sendable>: ~Copyable, Sendable {
private let requests = Mutex<[Int64: T]>([:])

View File

@@ -3,21 +3,6 @@ import Flutter
import MobileCoreServices
import Photos
class LocalImageRequest {
weak var operation: Operation?
var isCancelled = false
let callback: (Result<[String: Int64]?, any Error>) -> Void
init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
self.callback = callback
}
func cancel() {
isCancelled = true
operation?.cancel()
}
}
class LocalImageApiImpl: LocalImageApi {
private static let imageManager = PHImageManager.default()
private static let fetchOptions = {
@@ -36,9 +21,9 @@ class LocalImageApiImpl: LocalImageApi {
return requestOptions
}()
private static let registry = RequestRegistry<LocalImageRequest>()
private static let registry = RequestRegistry<ImageRequest>()
private static var rgbaFormat = vImage_CGImageFormat(
private static let rgbaFormat = vImage_CGImageFormat(
bitsPerComponent: 8,
bitsPerPixel: 32,
colorSpace: CGColorSpaceCreateDeviceRGB(),
@@ -67,21 +52,20 @@ class LocalImageApiImpl: LocalImageApi {
}
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
let request = LocalImageRequest(callback: completion)
let request = ImageRequest(completion: completion)
let operation = BlockOperation {
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
return request.completion(ImageProcessing.cancelledResult)
}
guard let asset = Self.requestAsset(assetId: assetId)
else {
Self.registry.remove(requestId: requestId)
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
return
return request.completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
}
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
return request.completion(ImageProcessing.cancelledResult)
}
if preferEncoded {
@@ -100,12 +84,12 @@ class LocalImageApiImpl: LocalImageApi {
)
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
return request.completion(ImageProcessing.cancelledResult)
}
guard let data = imageData else {
Self.registry.remove(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not get image data for \(assetId)", details: nil)))
return request.completion(.failure(PigeonError(code: "", message: "Could not get image data for \(assetId)", details: nil)))
}
let length = data.count
@@ -114,15 +98,14 @@ class LocalImageApiImpl: LocalImageApi {
if request.isCancelled {
free(pointer)
return completion(ImageProcessing.cancelledResult)
return request.completion(ImageProcessing.cancelledResult)
}
request.callback(.success([
Self.registry.remove(requestId: requestId)
return request.completion(.success([
"pointer": Int64(Int(bitPattern: pointer)),
"length": Int64(length),
]))
Self.registry.remove(requestId: requestId)
return
}
var image: UIImage?
@@ -137,17 +120,17 @@ class LocalImageApiImpl: LocalImageApi {
)
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
return request.completion(ImageProcessing.cancelledResult)
}
guard let image = image,
let cgImage = image.cgImage else {
Self.registry.remove(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
return request.completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
}
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
return request.completion(ImageProcessing.cancelledResult)
}
do {
@@ -155,23 +138,22 @@ class LocalImageApiImpl: LocalImageApi {
if request.isCancelled {
buffer.free()
return completion(ImageProcessing.cancelledResult)
return request.completion(ImageProcessing.cancelledResult)
}
request.callback(.success([
Self.registry.remove(requestId: requestId)
return request.completion(.success([
"pointer": Int64(Int(bitPattern: buffer.data)),
"width": Int64(buffer.width),
"height": Int64(buffer.height),
"rowBytes": Int64(buffer.rowBytes),
]))
Self.registry.remove(requestId: requestId)
} catch {
Self.registry.remove(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", details: nil)))
return request.completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", details: nil)))
}
}
request.operation = operation
Self.registry.add(requestId: requestId, request: request)
ImageProcessing.queue.addOperation(operation)
}

View File

@@ -3,27 +3,24 @@ import Flutter
import MobileCoreServices
import Photos
class RemoteImageRequest {
weak var task: URLSessionDataTask?
final class RemoteImageRequest: ImageRequest {
var task: URLSessionDataTask?
let id: Int64
var isCancelled = false
let completion: (Result<[String: Int64]?, any Error>) -> Void
init(id: Int64, task: URLSessionDataTask, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
init(id: Int64, completion: @escaping @Sendable (Result<[String: Int64]?, any Error>) -> Void) {
self.id = id
self.task = task
self.completion = completion
super.init(completion: completion)
}
func cancel() {
isCancelled = true
override func cancel() {
super.cancel()
task?.cancel()
}
}
class RemoteImageApiImpl: NSObject, RemoteImageApi {
private static let registry = RequestRegistry<RemoteImageRequest>()
private static var rgbaFormat = vImage_CGImageFormat(
private static let rgbaFormat = vImage_CGImageFormat(
bitsPerComponent: 8,
bitsPerPixel: 32,
colorSpace: CGColorSpaceCreateDeviceRGB(),
@@ -41,62 +38,58 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
var urlRequest = URLRequest(url: URL(string: url)!)
urlRequest.cachePolicy = .returnCacheDataElseLoad
let request = RemoteImageRequest(id: requestId, completion: completion)
let task = URLSessionManager.shared.session.dataTask(with: urlRequest) { data, response, error in
Self.handleCompletion(requestId: requestId, encoded: preferEncoded, data: data, response: response, error: error)
Self.handleCompletion(request: request, encoded: preferEncoded, data: data, response: response, error: error)
}
let request = RemoteImageRequest(id: requestId, task: task, completion: completion)
request.task = task
Self.registry.add(requestId: requestId, request: request)
task.resume()
}
private static func handleCompletion(requestId: Int64, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) {
guard let request = registry.remove(requestId: requestId) else {
return
}
if let error = error {
if request.isCancelled || (error as NSError).code == NSURLErrorCancelled {
return request.completion(ImageProcessing.cancelledResult)
}
return request.completion(.failure(error))
}
private static func handleCompletion(request: RemoteImageRequest, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) {
if request.isCancelled {
return request.completion(ImageProcessing.cancelledResult)
}
if let error = error {
registry.remove(requestId: request.id)
return request.completion(.failure(error))
}
guard let data = data else {
registry.remove(requestId: request.id)
return request.completion(.failure(PigeonError(code: "", message: "No data received", details: nil)))
}
if encoded {
let length = data.count
let pointer = malloc(length)!
data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length)
if request.isCancelled {
free(pointer)
return request.completion(ImageProcessing.cancelledResult)
}
registry.remove(requestId: request.id)
return request.completion(
.success([
"pointer": Int64(Int(bitPattern: pointer)),
"length": Int64(length),
]))
}
ImageProcessing.queue.addOperation {
if request.isCancelled {
return request.completion(ImageProcessing.cancelledResult)
}
// Return raw encoded bytes when requested (for animated images)
if encoded {
let length = data.count
let pointer = malloc(length)!
data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length)
if request.isCancelled {
free(pointer)
return request.completion(ImageProcessing.cancelledResult)
}
return request.completion(
.success([
"pointer": Int64(Int(bitPattern: pointer)),
"length": Int64(length),
]))
}
guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil),
let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else {
registry.remove(requestId: request.id)
return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil)))
}
@@ -112,14 +105,16 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
return request.completion(ImageProcessing.cancelledResult)
}
request.completion(
.success([
"pointer": Int64(Int(bitPattern: buffer.data)),
"width": Int64(buffer.width),
"height": Int64(buffer.height),
"rowBytes": Int64(buffer.rowBytes),
]))
registry.remove(requestId: request.id)
return request.completion(
.success([
"pointer": Int64(Int(bitPattern: buffer.data)),
"width": Int64(buffer.width),
"height": Int64(buffer.height),
"rowBytes": Int64(buffer.rowBytes),
]))
} catch {
registry.remove(requestId: request.id)
return request.completion(.failure(PigeonError(code: "", message: "Failed to convert image for request: \(error)", details: nil)))
}
}

View File

@@ -80,7 +80,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.7.2</string>
<string>2.7.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -119,9 +119,7 @@
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
<string>No</string>
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
<true/>
<key>NSAppTransportSecurity</key>

View File

@@ -45,14 +45,17 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
}
WidgetsBinding.instance.addPostFrameCallback((_) async {
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
final backupNotifier = ref.read(driftBackupProvider.notifier);
final syncManager = ref.read(backgroundSyncProvider);
ref.read(driftBackupProvider.notifier).updateSyncing(true);
syncSuccess = await ref.read(backgroundSyncProvider).syncRemote();
ref.read(driftBackupProvider.notifier).updateSyncing(false);
await backupNotifier.getBackupStatus(currentUser.id);
backupNotifier.updateSyncing(true);
syncSuccess = await syncManager.syncRemote();
backupNotifier.updateSyncing(false);
if (mounted) {
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
await backupNotifier.getBackupStatus(currentUser.id);
}
});
}
@@ -82,9 +85,9 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
}
if (syncSuccess == null) {
ref.read(driftBackupProvider.notifier).updateSyncing(true);
backupNotifier.updateSyncing(true);
syncSuccess = await backupSyncManager.syncRemote();
ref.read(driftBackupProvider.notifier).updateSyncing(false);
backupNotifier.updateSyncing(false);
}
await backupNotifier.getBackupStatus(currentUser.id);

View File

@@ -147,7 +147,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
}
void _onAssetInit(Duration timeStamp) {
// _preloader.preload(widget.initialIndex, context.sizeData);
_preloader.preload(widget.initialIndex, context.sizeData);
_handleCasting();
}
@@ -158,7 +158,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
if (asset == null) return;
AssetViewer._setAsset(ref, asset);
// _preloader.preload(index, context.sizeData);
_preloader.preload(index, context.sizeData);
_handleCasting();
_stackChildrenKeepAlive?.close();
_stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive();

View File

@@ -51,7 +51,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
return null;
}
Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode, {bool evictOnError = true}) async* {
Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode, {required bool isFinal}) async* {
if (isCancelled) {
this.request = null;
return;
@@ -59,21 +59,18 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
try {
final image = await request.load(decode);
if (isCancelled) {
return;
}
if (image == null && evictOnError) {
PaintingBinding.instance.imageCache.evict(this);
return;
} else if (image == null) {
if (isCancelled || image == null) {
image?.dispose();
return;
}
isFinished = isFinal;
yield image;
} catch (e, stack) {
if (isCancelled) {
return;
}
if (evictOnError) {
if (isFinal) {
isFinished = true;
PaintingBinding.instance.imageCache.evict(this);
rethrow;
}
@@ -83,7 +80,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
}
}
Future<ui.Codec?> loadCodecRequest(ImageRequest request) async {
Future<ui.Codec?> loadCodecRequest(ImageRequest request, {required bool isFinal}) async {
if (isCancelled) {
this.request = null;
return null;
@@ -91,20 +88,19 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
try {
final codec = await request.loadCodec();
if (isCancelled) {
if (isCancelled || codec == null) {
codec?.dispose();
return null;
}
if (codec == null) {
PaintingBinding.instance.imageCache.evict(this);
return null;
}
isFinished = isFinal;
return codec;
} catch (e) {
if (!isCancelled) {
if (isFinal) {
isFinished = true;
PaintingBinding.instance.imageCache.evict(this);
rethrow;
}
rethrow;
return null;
} finally {
this.request = null;
}

View File

@@ -36,7 +36,7 @@ class LocalThumbProvider extends CancellableImageProvider<LocalThumbProvider>
Stream<ImageInfo> _codec(LocalThumbProvider key, ImageDecoderCallback decode) {
final request = this.request = LocalImageRequest(localId: key.id, size: key.size, assetType: key.assetType);
return loadRequest(request, decode);
return loadRequest(request, decode, isFinal: true);
}
@override
@@ -103,16 +103,16 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
return;
}
final loadOriginal = Store.get(StoreKey.loadOriginal, false);
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
var request = this.request = LocalImageRequest(
localId: key.id,
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
assetType: key.assetType,
);
yield* loadRequest(request, decode);
yield* loadRequest(request, decode, isFinal: !loadOriginal);
if (!Store.get(StoreKey.loadOriginal, false)) {
isFinished = true;
if (!loadOriginal) {
return;
}
@@ -122,8 +122,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
request = this.request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero);
yield* loadRequest(request, decode);
isFinished = true;
yield* loadRequest(request, decode, isFinal: true);
}
Stream<Object> _animatedCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
@@ -139,7 +138,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
assetType: key.assetType,
);
yield* loadRequest(previewRequest, decode);
yield* loadRequest(previewRequest, decode, isFinal: false);
if (isCancelled) {
return;
@@ -147,13 +146,12 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
// always try original for animated, since previews don't support animation
final originalRequest = request = LocalImageRequest(localId: key.id, size: Size.zero, assetType: key.assetType);
final codec = await loadCodecRequest(originalRequest);
final codec = await loadCodecRequest(originalRequest, isFinal: true);
if (codec == null) {
if (isCancelled) return;
throw StateError('Failed to load animated codec for local asset ${key.id}');
}
yield codec;
isFinished = true;
}
@override

View File

@@ -38,7 +38,7 @@ class RemoteImageProvider extends CancellableImageProvider<RemoteImageProvider>
Stream<ImageInfo> _codec(RemoteImageProvider key, ImageDecoderCallback decode) {
final request = this.request = RemoteImageRequest(uri: key.url);
return loadRequest(request, decode);
return loadRequest(request, decode, isFinal: true);
}
@override
@@ -112,10 +112,9 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
);
final loadOriginal = assetType == AssetType.image && AppSetting.get(Setting.loadOriginal);
yield* loadRequest(previewRequest, decode, evictOnError: !loadOriginal);
yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
if (!loadOriginal) {
isFinished = true;
return;
}
@@ -124,8 +123,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
}
final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId));
yield* loadRequest(originalRequest, decode);
isFinished = true;
yield* loadRequest(originalRequest, decode, isFinal: true);
}
Stream<Object> _animatedCodec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* {
@@ -138,7 +136,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
final previewRequest = request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
);
yield* loadRequest(previewRequest, decode, evictOnError: false);
yield* loadRequest(previewRequest, decode, isFinal: false);
if (isCancelled) {
return;
@@ -146,7 +144,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
// always try original for animated, since previews don't support animation
final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId));
final codec = await loadCodecRequest(originalRequest);
final codec = await loadCodecRequest(originalRequest, isFinal: true);
if (codec == null) {
if (isCancelled) {
return;
@@ -154,7 +152,6 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
throw StateError('Failed to load animated codec for asset ${key.assetId}');
}
yield codec;
isFinished = true;
}
@override

View File

@@ -22,7 +22,7 @@ class ThumbHashProvider extends CancellableImageProvider<ThumbHashProvider>
Stream<ImageInfo> _loadCodec(ThumbHashProvider key, ImageDecoderCallback decode) {
final request = this.request = ThumbhashImageRequest(thumbhash: key.thumbHash);
return loadRequest(request, decode);
return loadRequest(request, decode, isFinal: true);
}
@override

View File

@@ -8,16 +8,11 @@ import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/utils/image_load_histogram.dart';
import 'package:logging/logging.dart';
final log = Logger('ThumbnailWidget');
enum ImageType { thumbnail }
final remoteImageHistogram = Histogram<ImageType>(maxSamples: 8192, values: ImageType.values);
int thumbnailId = 0;
enum ThumbhashMode { enabled, disabled, only }
class Thumbnail extends StatefulWidget {
final ImageProvider? imageProvider;
@@ -116,11 +111,8 @@ class _ThumbnailState extends State<Thumbnail> with SingleTickerProviderStateMix
if (imageProvider == null) return;
final imageStream = _imageStream = imageProvider.resolve(ImageConfiguration.empty);
final stopwatch = Stopwatch();
final curThumbnailId = thumbnailId++;
final imageStreamListener = _imageStreamListener = ImageStreamListener(
(ImageInfo imageInfo, bool synchronousCall) {
stopwatch.stop();
_stopListeningToThumbhashStream();
if (!mounted) {
imageInfo.dispose();
@@ -131,27 +123,7 @@ class _ThumbnailState extends State<Thumbnail> with SingleTickerProviderStateMix
return;
}
final renderObject = context.findRenderObject() as RenderBox?;
final double topLeft;
final double bottomRight;
final double contextHeight = context.height;
if (renderObject == null || !renderObject.attached) {
topLeft = double.maxFinite;
bottomRight = double.maxFinite;
} else {
topLeft = renderObject.localToGlobal(Offset.zero).dy;
bottomRight = renderObject.localToGlobal(Offset(renderObject.size.width, renderObject.size.height)).dy;
}
remoteImageHistogram.record(
ImageType.thumbnail,
stopwatch.elapsedMicroseconds,
topLeft.toInt(),
bottomRight.toInt(),
contextHeight.toInt(),
curThumbnailId,
);
if ((synchronousCall && _providerImage == null) || !(topLeft < contextHeight && bottomRight > 0)) {
if ((synchronousCall && _providerImage == null) || !_isVisible()) {
_fadeController.value = 1.0;
} else if (_fadeController.isAnimating) {
_fadeController.forward();
@@ -174,7 +146,6 @@ class _ThumbnailState extends State<Thumbnail> with SingleTickerProviderStateMix
_stopListeningToImageStream();
},
);
stopwatch.start();
imageStream.addListener(imageStreamListener);
}

View File

@@ -7,7 +7,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
@@ -18,7 +17,6 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_status_floating_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
@@ -142,14 +140,10 @@ class _SliverTimeline extends ConsumerStatefulWidget {
ConsumerState createState() => _SliverTimelineState();
}
class _SliverTimelineState extends ConsumerState<_SliverTimeline> with SingleTickerProviderStateMixin {
class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
late final ScrollController _scrollController;
StreamSubscription? _eventSubscription;
Ticker? _autoScrollTicker;
Duration _lastTickTime = Duration.zero;
static const _autoScrollVelocity = 4800.0; // pixels per second
// Drag selection state
bool _dragging = false;
TimelineAssetIndex? _dragAnchorIndex;
@@ -252,52 +246,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with SingleTic
@override
void dispose() {
_stopAutoScroll();
_scrollController.dispose();
_eventSubscription?.cancel();
super.dispose();
}
void _toggleAutoScroll() {
if (_autoScrollTicker?.isActive ?? false) {
_stopAutoScroll();
} else {
_startAutoScroll();
}
}
void _startAutoScroll() {
_lastTickTime = Duration.zero;
_autoScrollTicker = createTicker(_onAutoScrollTick)..start();
}
void _stopAutoScroll() {
_autoScrollTicker?.stop();
_autoScrollTicker?.dispose();
_autoScrollTicker = null;
}
void _onAutoScrollTick(Duration elapsed) {
if (_lastTickTime == Duration.zero) {
_lastTickTime = elapsed;
return;
}
final deltaSeconds = (elapsed - _lastTickTime).inMicroseconds / 1000000.0;
_lastTickTime = elapsed;
final newOffset = _scrollController.offset + (_autoScrollVelocity * deltaSeconds);
final maxOffset = _scrollController.position.maxScrollExtent;
if (newOffset >= maxOffset || remoteImageHistogram.count(ImageType.thumbnail) >= remoteImageHistogram.maxSamples) {
_scrollController.jumpTo(newOffset.clamp(0, maxOffset));
_stopAutoScroll();
remoteImageHistogram.logAll();
remoteImageHistogram.save();
} else {
_scrollController.jumpTo(newOffset);
}
}
void _scrollToDate(DateTime date) {
final asyncSegments = ref.read(timelineSegmentProvider);
asyncSegments.whenData((segments) {
@@ -481,16 +434,6 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with SingleTic
controller: _scrollController,
child: RawGestureDetector(
gestures: {
SerialTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<SerialTapGestureRecognizer>(
() => SerialTapGestureRecognizer(),
(SerialTapGestureRecognizer tap) {
tap.onSerialTapDown = (details) {
if (details.count == 3) {
_toggleAutoScroll();
}
};
},
),
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
() => CustomScaleGestureRecognizer(),
(CustomScaleGestureRecognizer scale) {

View File

@@ -1,154 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
/// Ring buffer histogram for performance profiling.
class Histogram<T extends Enum> {
final int _stride;
final int _strideMask;
final List<T> _values;
final Int64List _counts;
final Int64List _data;
final Stopwatch _clock;
static final _log = Logger('Histogram');
Histogram({required int maxSamples, required List<T> values})
: assert(maxSamples & (maxSamples - 1) == 0, 'maxSamples must be power of 2'),
_stride = maxSamples,
_strideMask = maxSamples - 1,
_values = values,
_counts = Int64List(values.length),
_data = Int64List(maxSamples * values.length * 6),
_clock = Stopwatch()..start();
@pragma("vm:prefer-inline")
@pragma("vm:unsafe:no-bounds-checks")
void record(T type, int microseconds, int topLeft, int bottomRight, int contextHeight, int id) {
final i = type.index;
final count = _counts[i];
final slot = count & _strideMask;
final offset = (i * _stride + slot) * 6;
_data[offset] = microseconds;
_data[offset + 1] = _clock.elapsedMicroseconds;
_data[offset + 2] = topLeft;
_data[offset + 3] = bottomRight;
_data[offset + 4] = contextHeight;
_data[offset + 5] = id;
_counts[i] = count + 1;
}
int count(T type) => _counts[type.index].clamp(0, _stride);
int get maxSamples => _stride;
@pragma("vm:unsafe:no-bounds-checks")
void log(T type) {
final index = type.index;
final total = _counts[index];
final count = min(total, _stride);
if (count == 0) return;
final baseOffset = index * _stride * 6;
final scratch = Int64List(count);
for (int i = 0; i < count; i++) {
scratch[i] = _data[baseOffset + i * 6];
}
scratch.sort();
int sum = 0;
for (int i = 0; i < count; i++) {
sum += scratch[i];
}
_log.info(
'${type.name} (n=$total, sampled=$count) - '
'Avg: ${(sum / count / 1000.0).toStringAsFixed(2)}ms, '
'Min: ${(scratch[0] / 1000.0).toStringAsFixed(2)}ms, '
'Max: ${(scratch[count - 1] / 1000.0).toStringAsFixed(2)}ms, '
'P25: ${(_percentile(scratch, count, 0.25) / 1000.0).toStringAsFixed(2)}ms, '
'P50: ${(_percentile(scratch, count, 0.50) / 1000.0).toStringAsFixed(2)}ms, '
'P75: ${(_percentile(scratch, count, 0.75) / 1000.0).toStringAsFixed(2)}ms, '
'P90: ${(_percentile(scratch, count, 0.90) / 1000.0).toStringAsFixed(2)}ms, '
'P95: ${(_percentile(scratch, count, 0.95) / 1000.0).toStringAsFixed(2)}ms, '
'P99: ${(_percentile(scratch, count, 0.99) / 1000.0).toStringAsFixed(2)}ms',
);
}
void logAll() {
for (final value in _values) {
log(value);
}
}
@pragma("vm:unsafe:no-bounds-checks")
(Int64List, Int64List, Int64List, Int64List, Int64List, Int64List) getSamples(T type) {
final index = type.index;
final count = min(_counts[index], _stride);
final samples = Int64List(count);
final timestamps = Int64List(count);
final topLeft = Int64List(count);
final bottomRight = Int64List(count);
final contextHeight = Int64List(count);
final id = Int64List(count);
final baseOffset = index * _stride * 6;
for (int i = 0; i < count; i++) {
samples[i] = _data[baseOffset + i * 6];
timestamps[i] = _data[baseOffset + i * 6 + 1];
topLeft[i] = _data[baseOffset + i * 6 + 2];
bottomRight[i] = _data[baseOffset + i * 6 + 3];
contextHeight[i] = _data[baseOffset + i * 6 + 4];
id[i] = _data[baseOffset + i * 6 + 5];
}
return (samples, timestamps, topLeft, bottomRight, contextHeight, id);
}
@pragma("vm:unsafe:no-bounds-checks")
Future<File> save({bool share = true}) async {
final dir = await getApplicationDocumentsDirectory();
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-');
final file = File('${dir.path}/samples_$timestamp.json');
final data = {};
for (int i = 0; i < _counts.length; i++) {
final name = _values[i].name;
final (samples, timestamps, topLeft, bottomRight, contextHeight, id) = getSamples(_values[i]);
data['${name}_us'] = samples;
data['${name}_ts'] = timestamps;
data['${name}_top_left'] = topLeft;
data['${name}_bottom_right'] = bottomRight;
data['${name}_context_height'] = contextHeight;
data['${name}_id'] = id;
}
data['timestamp'] = DateTime.now().toIso8601String();
await file.writeAsString(jsonEncode(data));
_log.info('Saved samples to ${file.path}');
if (share) {
await Share.shareXFiles([XFile(file.path)]);
}
return file;
}
void reset(T type) {
_counts[type.index] = 0;
}
void resetAll() {
_counts.fillRange(0, _counts.length, 0);
}
@pragma("vm:prefer-inline")
int _percentile(Int64List sorted, int count, double p) {
final idx = ((count - 1) * p).round();
return sorted[idx];
}
}

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 2.7.2
- API version: 2.7.4
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 2.7.2+3043
version: 2.7.4+3045
environment:
sdk: '>=3.8.0 <4.0.0'

View File

@@ -15225,7 +15225,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "2.7.2",
"version": "2.7.4",
"contact": {}
},
"tags": [

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "2.7.2",
"version": "2.7.4",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 2.7.2
* 2.7.4
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

@@ -1,6 +1,6 @@
{
"name": "immich-monorepo",
"version": "2.7.2",
"version": "2.7.4",
"description": "Monorepo for Immich",
"private": true,
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be",

268
pnpm-lock.yaml generated
View File

@@ -87,8 +87,8 @@ importers:
specifier: ^5.1.3
version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1))(prettier@3.8.1)
eslint-plugin-unicorn:
specifier: ^63.0.0
version: 63.0.0(eslint@10.1.0(jiti@2.6.1))
specifier: ^64.0.0
version: 64.0.0(eslint@10.1.0(jiti@2.6.1))
globals:
specifier: ^17.0.0
version: 17.4.0
@@ -241,8 +241,8 @@ importers:
specifier: ^5.1.3
version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1))(prettier@3.8.1)
eslint-plugin-unicorn:
specifier: ^63.0.0
version: 63.0.0(eslint@10.1.0(jiti@2.6.1))
specifier: ^64.0.0
version: 64.0.0(eslint@10.1.0(jiti@2.6.1))
exiftool-vendored:
specifier: ^35.0.0
version: 35.15.1
@@ -278,7 +278,7 @@ importers:
version: 6.0.2
typescript-eslint:
specifier: ^8.28.0
version: 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
version: 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
utimes:
specifier: ^5.2.1
version: 5.2.1(encoding@0.1.13)
@@ -553,9 +553,6 @@ importers:
sanitize-filename:
specifier: ^1.6.3
version: 1.6.4
sanitize-html:
specifier: ^2.14.0
version: 2.17.2
semver:
specifier: ^7.6.2
version: 7.7.4
@@ -659,9 +656,6 @@ importers:
'@types/react':
specifier: ^19.0.0
version: 19.2.14
'@types/sanitize-html':
specifier: ^2.13.0
version: 2.16.1
'@types/semver':
specifier: ^7.5.8
version: 7.7.1
@@ -687,8 +681,8 @@ importers:
specifier: ^5.1.3
version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1))(prettier@3.8.1)
eslint-plugin-unicorn:
specifier: ^63.0.0
version: 63.0.0(eslint@10.1.0(jiti@2.6.1))
specifier: ^64.0.0
version: 64.0.0(eslint@10.1.0(jiti@2.6.1))
globals:
specifier: ^17.0.0
version: 17.4.0
@@ -724,7 +718,7 @@ importers:
version: 6.0.2
typescript-eslint:
specifier: ^8.28.0
version: 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
version: 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
unplugin-swc:
specifier: ^1.4.5
version: 1.5.9(@swc/core@1.15.18(@swc/helpers@0.5.17))(rollup@4.55.1)
@@ -934,8 +928,8 @@ importers:
specifier: ^3.12.4
version: 3.16.0(eslint@10.1.0(jiti@2.6.1))(svelte@5.54.1)
eslint-plugin-unicorn:
specifier: ^63.0.0
version: 63.0.0(eslint@10.1.0(jiti@2.6.1))
specifier: ^64.0.0
version: 64.0.0(eslint@10.1.0(jiti@2.6.1))
factory.ts:
specifier: ^1.4.1
version: 1.4.2
@@ -974,7 +968,7 @@ importers:
version: 6.0.2
typescript-eslint:
specifier: ^8.45.0
version: 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
version: 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
vite:
specifier: ^8.0.0
version: 8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
@@ -5167,9 +5161,6 @@ packages:
'@types/retry@0.12.2':
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
'@types/sanitize-html@2.16.1':
resolution: {integrity: sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==}
'@types/sax@1.2.7':
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
@@ -5242,14 +5233,6 @@ packages:
'@types/yargs@17.0.35':
resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
'@typescript-eslint/eslint-plugin@8.57.1':
resolution: {integrity: sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.57.1
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/eslint-plugin@8.58.0':
resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -5258,13 +5241,6 @@ packages:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/parser@8.57.1':
resolution: {integrity: sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/parser@8.58.0':
resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -5272,45 +5248,22 @@ packages:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.57.1':
resolution: {integrity: sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/project-service@8.58.0':
resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@8.57.1':
resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.58.0':
resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.57.1':
resolution: {integrity: sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/tsconfig-utils@8.58.0':
resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@8.57.1':
resolution: {integrity: sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.58.0':
resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -5318,33 +5271,16 @@ packages:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@8.57.1':
resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.58.0':
resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.57.1':
resolution: {integrity: sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/typescript-estree@8.58.0':
resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.57.1':
resolution: {integrity: sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.58.0':
resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -5352,10 +5288,6 @@ packages:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@8.57.1':
resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.58.0':
resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -6138,8 +6070,8 @@ packages:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'}
ci-info@4.3.1:
resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==}
ci-info@4.4.0:
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
engines: {node: '>=8'}
citty@0.1.6:
@@ -6420,8 +6352,8 @@ packages:
peerDependencies:
webpack: ^5.1.0
core-js-compat@3.47.0:
resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==}
core-js-compat@3.49.0:
resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==}
core-js-pure@3.47.0:
resolution: {integrity: sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==}
@@ -7244,8 +7176,8 @@ packages:
svelte:
optional: true
eslint-plugin-unicorn@63.0.0:
resolution: {integrity: sha512-Iqecl9118uQEXYh7adylgEmGfkn5es3/mlQTLLkd4pXkIk9CTGrAbeUux+YljSa2ohXCBmQQ0+Ej1kZaFgcfkA==}
eslint-plugin-unicorn@64.0.0:
resolution: {integrity: sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==}
engines: {node: ^20.10.0 || >=21.0.0}
peerDependencies:
eslint: '>=9.38.0'
@@ -7971,9 +7903,6 @@ packages:
webpack:
optional: true
htmlparser2@10.1.0:
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
htmlparser2@6.1.0:
resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==}
@@ -8303,10 +8232,6 @@ packages:
resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
engines: {node: '>=0.10.0'}
is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
@@ -9692,9 +9617,6 @@ packages:
parse-numeric-range@1.3.0:
resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
parse-srcset@1.0.2:
resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
parse5-htmlparser2-tree-adapter@7.1.0:
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
@@ -10892,9 +10814,6 @@ packages:
sanitize-filename@1.6.4:
resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==}
sanitize-html@2.17.2:
resolution: {integrity: sha512-EnffJUl46VE9uvZ0XeWzObHLurClLlT12gsOk1cHyP2Ol1P0BnBnsXmShlBmWVJM+dKieQI68R0tsPY5m/B+Jg==}
sass@1.97.1:
resolution: {integrity: sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==}
engines: {node: '>=14.0.0'}
@@ -11777,13 +11696,6 @@ packages:
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
typescript-eslint@8.57.1:
resolution: {integrity: sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
typescript-eslint@8.58.0:
resolution: {integrity: sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -13460,7 +13372,7 @@ snapshots:
babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.5)
babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.5)
babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.5)
core-js-compat: 3.47.0
core-js-compat: 3.49.0
semver: 6.3.1
transitivePeerDependencies:
- supports-color
@@ -15512,8 +15424,8 @@ snapshots:
'@koddsson/eslint-plugin-tscompat@0.2.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)':
dependencies:
'@mdn/browser-compat-data': 6.1.5
'@typescript-eslint/type-utils': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
'@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
'@typescript-eslint/type-utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
'@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
browserslist: 4.28.1
transitivePeerDependencies:
- eslint
@@ -17459,10 +17371,6 @@ snapshots:
'@types/retry@0.12.2': {}
'@types/sanitize-html@2.16.1':
dependencies:
htmlparser2: 10.1.0
'@types/sax@1.2.7':
dependencies:
'@types/node': 24.12.2
@@ -17552,22 +17460,6 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
'@typescript-eslint/scope-manager': 8.57.1
'@typescript-eslint/type-utils': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
'@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
'@typescript-eslint/visitor-keys': 8.57.1
eslint: 10.1.0(jiti@2.6.1)
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.2)
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
'@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -17584,18 +17476,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)':
dependencies:
'@typescript-eslint/scope-manager': 8.57.1
'@typescript-eslint/types': 8.57.1
'@typescript-eslint/typescript-estree': 8.57.1(typescript@6.0.2)
'@typescript-eslint/visitor-keys': 8.57.1
debug: 4.4.3
eslint: 10.1.0(jiti@2.6.1)
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)':
dependencies:
'@typescript-eslint/scope-manager': 8.58.0
@@ -17608,15 +17488,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.57.1(typescript@6.0.2)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.57.1(typescript@6.0.2)
'@typescript-eslint/types': 8.57.1
debug: 4.4.3
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.58.0(typescript@6.0.2)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2)
@@ -17626,36 +17497,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.57.1':
dependencies:
'@typescript-eslint/types': 8.57.1
'@typescript-eslint/visitor-keys': 8.57.1
'@typescript-eslint/scope-manager@8.58.0':
dependencies:
'@typescript-eslint/types': 8.58.0
'@typescript-eslint/visitor-keys': 8.58.0
'@typescript-eslint/tsconfig-utils@8.57.1(typescript@6.0.2)':
dependencies:
typescript: 6.0.2
'@typescript-eslint/tsconfig-utils@8.58.0(typescript@6.0.2)':
dependencies:
typescript: 6.0.2
'@typescript-eslint/type-utils@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)':
dependencies:
'@typescript-eslint/types': 8.57.1
'@typescript-eslint/typescript-estree': 8.57.1(typescript@6.0.2)
'@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
debug: 4.4.3
eslint: 10.1.0(jiti@2.6.1)
ts-api-utils: 2.5.0(typescript@6.0.2)
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
'@typescript-eslint/type-utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)':
dependencies:
'@typescript-eslint/types': 8.58.0
@@ -17668,25 +17518,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.57.1': {}
'@typescript-eslint/types@8.58.0': {}
'@typescript-eslint/typescript-estree@8.57.1(typescript@6.0.2)':
dependencies:
'@typescript-eslint/project-service': 8.57.1(typescript@6.0.2)
'@typescript-eslint/tsconfig-utils': 8.57.1(typescript@6.0.2)
'@typescript-eslint/types': 8.57.1
'@typescript-eslint/visitor-keys': 8.57.1
debug: 4.4.3
minimatch: 10.2.4
semver: 7.7.4
tinyglobby: 0.2.15
ts-api-utils: 2.5.0(typescript@6.0.2)
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.58.0(typescript@6.0.2)':
dependencies:
'@typescript-eslint/project-service': 8.58.0(typescript@6.0.2)
@@ -17702,17 +17535,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1))
'@typescript-eslint/scope-manager': 8.57.1
'@typescript-eslint/types': 8.57.1
'@typescript-eslint/typescript-estree': 8.57.1(typescript@6.0.2)
eslint: 10.1.0(jiti@2.6.1)
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1))
@@ -17724,11 +17546,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.57.1':
dependencies:
'@typescript-eslint/types': 8.57.1
eslint-visitor-keys: 5.0.1
'@typescript-eslint/visitor-keys@8.58.0':
dependencies:
'@typescript-eslint/types': 8.58.0
@@ -18249,7 +18066,7 @@ snapshots:
dependencies:
'@babel/core': 7.28.5
'@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.5)
core-js-compat: 3.47.0
core-js-compat: 3.49.0
transitivePeerDependencies:
- supports-color
@@ -18664,7 +18481,7 @@ snapshots:
ci-info@3.9.0: {}
ci-info@4.3.1: {}
ci-info@4.4.0: {}
citty@0.1.6:
dependencies:
@@ -18916,7 +18733,7 @@ snapshots:
serialize-javascript: 6.0.2
webpack: 5.104.1
core-js-compat@3.47.0:
core-js-compat@3.49.0:
dependencies:
browserslist: 4.28.1
@@ -19870,17 +19687,17 @@ snapshots:
transitivePeerDependencies:
- ts-node
eslint-plugin-unicorn@63.0.0(eslint@10.1.0(jiti@2.6.1)):
eslint-plugin-unicorn@64.0.0(eslint@10.1.0(jiti@2.6.1)):
dependencies:
'@babel/helper-validator-identifier': 7.28.5
'@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1))
change-case: 5.4.4
ci-info: 4.3.1
ci-info: 4.4.0
clean-regexp: 1.0.0
core-js-compat: 3.47.0
core-js-compat: 3.49.0
eslint: 10.1.0(jiti@2.6.1)
find-up-simple: 1.0.1
globals: 16.5.0
globals: 17.4.0
indent-string: 5.0.0
is-builtin-module: 5.0.0
jsesc: 3.1.0
@@ -20865,13 +20682,6 @@ snapshots:
optionalDependencies:
webpack: 5.104.1
htmlparser2@10.1.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
entities: 7.0.1
htmlparser2@6.1.0:
dependencies:
domelementtype: 2.3.0
@@ -21200,8 +21010,6 @@ snapshots:
dependencies:
isobject: 3.0.1
is-plain-object@5.0.0: {}
is-potential-custom-element-name@1.0.1:
optional: true
@@ -22933,8 +22741,6 @@ snapshots:
parse-numeric-range@1.3.0: {}
parse-srcset@1.0.2: {}
parse5-htmlparser2-tree-adapter@7.1.0:
dependencies:
domhandler: 5.0.3
@@ -24278,15 +24084,6 @@ snapshots:
dependencies:
truncate-utf8-bytes: 1.0.2
sanitize-html@2.17.2:
dependencies:
deepmerge: 4.3.1
escape-string-regexp: 4.0.0
htmlparser2: 10.1.0
is-plain-object: 5.0.0
parse-srcset: 1.0.2
postcss: 8.5.8
sass@1.97.1:
dependencies:
chokidar: 4.0.3
@@ -25404,17 +25201,6 @@ snapshots:
typedarray@0.0.6: {}
typescript-eslint@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2):
dependencies:
'@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
'@typescript-eslint/parser': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
'@typescript-eslint/typescript-estree': 8.57.1(typescript@6.0.2)
'@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
eslint: 10.1.0(jiti@2.6.1)
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
typescript-eslint@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2):
dependencies:
'@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "2.7.2",
"version": "2.7.4",
"description": "",
"author": "",
"private": true,
@@ -110,7 +110,6 @@
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"sanitize-filename": "^1.6.3",
"sanitize-html": "^2.14.0",
"semver": "^7.6.2",
"sharp": "^0.34.5",
"sirv": "^3.0.0",
@@ -147,7 +146,6 @@
"@types/picomatch": "^4.0.0",
"@types/pngjs": "^6.0.5",
"@types/react": "^19.0.0",
"@types/sanitize-html": "^2.13.0",
"@types/semver": "^7.5.8",
"@types/supertest": "^7.0.0",
"@types/ua-parser-js": "^0.7.36",
@@ -156,7 +154,7 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^63.0.0",
"eslint-plugin-unicorn": "^64.0.0",
"globals": "^17.0.0",
"mock-fs": "^5.2.0",
"node-gyp": "^12.0.0",

View File

@@ -195,13 +195,19 @@ where
"asset_face"."id" = $2
-- PersonRepository.getByName
with
"similarity_threshold" as (
select
set_config('pg_trgm.word_similarity_threshold', '0.5', true) as "thresh"
)
select
"person".*
from
"similarity_threshold",
"person"
where
"person"."ownerId" = $1
and f_unaccent ("person"."name") %>> f_unaccent ($2)
and f_unaccent ("person"."name") %> f_unaccent ($2)
order by
f_unaccent ("person"."name") <->>> f_unaccent ($3)
limit

View File

@@ -58,6 +58,7 @@ export class OcrRepository {
})
upsert(assetId: string, ocrDataList: Insertable<AssetOcrTable>[], searchText: string) {
let query = this.db.with('deleted_ocr', (db) => db.deleteFrom('asset_ocr').where('assetId', '=', assetId));
// eslint-disable-next-line unicorn/prefer-ternary
if (ocrDataList.length > 0) {
(query as any) = query
.with('inserted_ocr', (db) => db.insertInto('asset_ocr').values(ocrDataList))

View File

@@ -310,10 +310,13 @@ export class PersonRepository {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, { withHidden: true }] })
getByName(userId: string, personName: string, { withHidden }: PersonNameSearchOptions) {
return this.db
.selectFrom('person')
.with('similarity_threshold', (db) =>
db.selectNoFrom(sql`set_config('pg_trgm.word_similarity_threshold', '0.5', true)`.as('thresh')),
)
.selectFrom(['similarity_threshold', 'person'])
.selectAll('person')
.where('person.ownerId', '=', userId)
.where(() => sql`f_unaccent("person"."name") %>> f_unaccent(${personName})`)
.where(() => sql`f_unaccent("person"."name") %> f_unaccent(${personName})`)
.orderBy(sql`f_unaccent("person"."name") <->>> f_unaccent(${personName})`)
.limit(100)
.$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))

View File

@@ -0,0 +1,36 @@
import { ApiService, render } from 'src/services/api.service';
describe(ApiService.name, () => {
describe('render', () => {
it('should correctly render open graph tags', () => {
const output = render('<!-- metadata:tags -->', {
title: 'title',
description: 'description',
imageUrl: 'https://demo.immich.app/api/assets/123',
});
expect(output).toContain('<meta property="og:title" content="title" />');
expect(output).toContain('<meta property="og:description" content="description" />');
expect(output).toContain('<meta property="og:image" content="https://demo.immich.app/api/assets/123" />');
});
it('should escape html tags', () => {
expect(
render('<!-- metadata:tags -->', {
title: "<script>console.log('hello')</script>Test",
description: 'description',
}),
).toContain(
'<meta property="og:title" content="&lt;script&gt;console.log(&#39;hello&#39;)&lt;/script&gt;Test" />',
);
});
it('should escape quotes', () => {
expect(
render('<!-- metadata:tags -->', {
title: `0;url=https://example.com" http-equiv="refresh`,
description: 'description',
}),
).toContain('<meta property="og:title" content="0;url=https://example.com&quot; http-equiv=&quot;refresh" />');
});
});
});

View File

@@ -1,7 +1,7 @@
import { Injectable, NotAcceptableException } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
import { escape } from 'lodash';
import { readFileSync } from 'node:fs';
import sanitizeHtml from 'sanitize-html';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { AuthService } from 'src/services/auth.service';
@@ -10,7 +10,7 @@ import { OpenGraphTags } from 'src/utils/misc';
export const render = (index: string, meta: OpenGraphTags) => {
const [title, description, imageUrl] = [meta.title, meta.description, meta.imageUrl].map((item) =>
item ? sanitizeHtml(item, { allowedTags: [] }) : '',
item ? escape(item) : '',
);
const tags = `

View File

@@ -692,6 +692,24 @@ describe(AssetMediaService.name, () => {
);
expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, true);
});
it('should not include original filename if requested using a shared link with showExif false', async () => {
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build();
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path });
const auth = AuthFactory.from().sharedLink({ showExif: false }).build();
await expect(sut.viewThumbnail(auth, asset.id, { size: AssetMediaSize.PREVIEW })).resolves.toEqual(
new ImmichFileResponse({
path: asset.files[0].path,
cacheControl: CacheControl.PrivateWithCache,
contentType: 'image/jpeg',
fileName: `${asset.id}_preview.jpg`,
}),
);
});
});
describe('playbackVideo', () => {

View File

@@ -257,7 +257,9 @@ export class AssetMediaService extends BaseService {
throw new NotFoundException('Asset media not found');
}
const fileName = `${getFileNameWithoutExtension(originalFileName)}_${size}${getFilenameExtension(path)}`;
const fileNameBase =
auth.sharedLink && !auth.sharedLink.showExif ? id : getFileNameWithoutExtension(originalFileName);
const fileName = `${fileNameBase}_${size}${getFilenameExtension(path)}`;
return new ImmichFileResponse({
fileName,

View File

@@ -2,6 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo",
},
"exclude": ["dist", "node_modules", "upload", "test", "e2e", "**/*spec.ts"]
}

View File

@@ -24,6 +24,7 @@
"rootDir": ".",
"jsx": "react",
"types": ["vitest/globals"],
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
"noErrorTruncation": true
},
"exclude": ["dist", "node_modules", "upload"]

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "2.7.2",
"version": "2.7.4",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {
@@ -91,7 +91,7 @@
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-compat": "^7.0.0",
"eslint-plugin-svelte": "^3.12.4",
"eslint-plugin-unicorn": "^63.0.0",
"eslint-plugin-unicorn": "^64.0.0",
"factory.ts": "^1.4.1",
"globals": "^17.0.0",
"happy-dom": "^20.0.0",

View File

@@ -70,7 +70,12 @@
reset();
return;
}
if (!force && searchedPeople.length < maximumLengthSearchPeople && searchName.startsWith(searchWord)) {
if (
!force &&
searchedPeople.length > 0 &&
searchedPeople.length < maximumLengthSearchPeople &&
searchName.startsWith(searchWord)
) {
search();
return;
}