Compare commits

..

9 Commits

Author SHA1 Message Date
midzelis 73f53935ca feat: make CI more parallel
Change-Id: I97e07aec57b0ca0626591033bd2b6da26a6a6964
2026-03-21 04:35:53 +00:00
Luis Nachtigall 00dae6ac38 fix(mobile): cronet image cache clearing on android (#27054) 2026-03-20 18:28:24 -04:00
Daniel Dietzler 5a8fd40dc5 fix: svelte reactivity issues (#27109) 2026-03-20 15:56:16 -04:00
Jason Rasmussen 813d684aaa fix: shared link add to album (#27063) 2026-03-20 13:14:07 -05:00
Luis Nachtigall 644f705be1 fix(mobile): correct maximumSizeBytes setter to use maximumSizeBytes property (#27098)
fix: correct maximumSizeBytes setter to use maximumSizeBytes property
2026-03-20 12:22:59 -04:00
Thomas f3e4bcc733 fix(server): queue version check job when config changed (#27094)
Nothing happens when the version checks are enabled, which means the
server may return very stale versions until the next job runs.

Refs: #26935
2026-03-20 10:07:40 -05:00
Michel Heusschen 9a0c17fdb8 fix(web): preserve album scroll when adding to other albums (#27078) 2026-03-20 09:42:19 -05:00
Michel Heusschen b7c4497dfd fix(web): allow showing combobox items outside modals (#27075)
fix(web): allow showing combobox items outside of modals
2026-03-20 09:41:34 -05:00
Yaros 9c227aeaf5 fix(mobile): simplified chinese not available (#27066) 2026-03-20 00:27:50 -05:00
27 changed files with 659 additions and 274 deletions
+207 -100
View File
@@ -8,6 +8,99 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {} permissions: {}
x-e2e-server-job: &e2e-server-job
needs: [pre-job, build-e2e-images]
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).server == true || fromJSON(needs.pre-job.outputs.should_run).cli == true }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
defaults:
run:
working-directory: ./e2e
strategy:
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm]
x-e2e-web-job: &e2e-web-job
needs: [pre-job, build-e2e-images]
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).web == true }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
defaults:
run:
working-directory: ./e2e
strategy:
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm]
x-e2e-steps:
token: &e2e-token-step
id: token
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
checkout: &e2e-checkout-step
name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
setup-pnpm: &e2e-setup-pnpm-step
name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
setup-node: &e2e-setup-node-step
name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
setup-sdk: &e2e-setup-sdk-step
name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
setup-web: &e2e-setup-web-step
name: Run setup web
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
working-directory: ./web
if: ${{ !cancelled() }}
setup-cli: &e2e-setup-cli-step
name: Run setup cli
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./cli
if: ${{ !cancelled() }}
install-deps: &e2e-install-deps-step
name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
install-playwright: &e2e-install-playwright-step
name: Install Playwright Browsers
run: pnpm exec playwright install chromium --only-shell
if: ${{ !cancelled() }}
download-images: &e2e-download-images-step
name: Download Docker images
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: docker-images-${{ matrix.runner }}
path: e2e
load-images: &e2e-load-images-step
name: Load Docker images
run: docker load < docker-images.tar.gz
docker-compose-up: &e2e-docker-compose-up-step
name: Start Docker Compose
run: docker compose up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
capture-docker-logs: &e2e-capture-docker-logs-step
name: Capture Docker logs
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
jobs: jobs:
pre-job: pre-job:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -397,10 +490,10 @@ jobs:
- name: Run medium tests - name: Run medium tests
run: pnpm test:medium run: pnpm test:medium
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
e2e-tests-server-cli: build-e2e-images:
name: End-to-End Tests (Server & CLI) name: Build E2E Docker Images
needs: pre-job needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).server == true || fromJSON(needs.pre-job.outputs.should_run).cli == true }} if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).server == true || fromJSON(needs.pre-job.outputs.should_run).cli == true || fromJSON(needs.pre-job.outputs.should_run).web == true }}
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
permissions: permissions:
contents: read contents: read
@@ -411,111 +504,86 @@ jobs:
matrix: matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm] runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- id: token - *e2e-token-step
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 - *e2e-checkout-step
- name: Build Docker images
run: docker compose build
- name: Save Docker images
run: docker save immich-server:latest immich-e2e-e2e-auth-server | gzip > docker-images.tar.gz
- name: Upload Docker images
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} name: docker-images-${{ matrix.runner }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} path: e2e/docker-images.tar.gz
retention-days: 1
- name: Checkout code e2e-tests-server-cli:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 <<: *e2e-server-job
with: name: End-to-End Tests (Server & CLI)
persist-credentials: false steps:
submodules: 'recursive' - *e2e-token-step
token: ${{ steps.token.outputs.token }} - *e2e-checkout-step
- name: Setup pnpm - *e2e-setup-pnpm-step
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 - *e2e-setup-node-step
- name: Setup Node - *e2e-setup-sdk-step
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - *e2e-setup-web-step
with: - *e2e-setup-cli-step
node-version-file: './e2e/.nvmrc' - *e2e-install-deps-step
cache: 'pnpm' - *e2e-download-images-step
cache-dependency-path: '**/pnpm-lock.yaml' - *e2e-load-images-step
- name: Run setup typescript-sdk - *e2e-docker-compose-up-step
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Run setup web
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
working-directory: ./web
if: ${{ !cancelled() }}
- name: Run setup cli
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./cli
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Start Docker Compose
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli) - name: Run e2e tests (api & cli)
env: env:
VITEST_DISABLE_DOCKER_SETUP: true VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run e2e tests (maintenance) - *e2e-capture-docker-logs-step
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test:maintenance
if: ${{ !cancelled() }}
- name: Capture Docker logs
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
- name: Archive Docker logs - name: Archive Docker logs
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always() if: always()
with: with:
name: e2e-server-docker-logs-${{ matrix.runner }} name: e2e-server-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt path: e2e/docker-compose-logs.txt
e2e-tests-web: e2e-tests-server-maintenance:
name: End-to-End Tests (Web) <<: *e2e-server-job
needs: pre-job name: End-to-End Tests (Server Maintenance)
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).web == true }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
defaults:
run:
working-directory: ./e2e
strategy:
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- id: token - *e2e-token-step
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 - *e2e-checkout-step
- *e2e-setup-pnpm-step
- *e2e-setup-node-step
- *e2e-setup-sdk-step
- *e2e-setup-web-step
- *e2e-setup-cli-step
- *e2e-install-deps-step
- *e2e-download-images-step
- *e2e-load-images-step
- *e2e-docker-compose-up-step
- name: Run e2e tests (maintenance)
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test:maintenance
if: ${{ !cancelled() }}
- *e2e-capture-docker-logs-step
- name: Archive Docker logs
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always()
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} name: e2e-server-maintenance-docker-logs-${{ matrix.runner }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} path: e2e/docker-compose-logs.txt
e2e-tests-web:
- name: Checkout code <<: *e2e-web-job
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 name: End-to-End Tests (Web)
with: steps:
persist-credentials: false - *e2e-token-step
submodules: 'recursive' - *e2e-checkout-step
token: ${{ steps.token.outputs.token }} - *e2e-setup-pnpm-step
- name: Setup pnpm - *e2e-setup-node-step
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 - *e2e-setup-sdk-step
- name: Setup Node - *e2e-install-deps-step
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - *e2e-install-playwright-step
with: - *e2e-download-images-step
node-version-file: './e2e/.nvmrc' - *e2e-load-images-step
cache: 'pnpm' - *e2e-docker-compose-up-step
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --only-shell
if: ${{ !cancelled() }}
- name: Docker build
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
- name: Run e2e tests (web) - name: Run e2e tests (web)
env: env:
PLAYWRIGHT_DISABLE_WEBSERVER: true PLAYWRIGHT_DISABLE_WEBSERVER: true
@@ -527,6 +595,27 @@ jobs:
with: with:
name: e2e-web-test-results-${{ matrix.runner }} name: e2e-web-test-results-${{ matrix.runner }}
path: e2e/playwright-report/ path: e2e/playwright-report/
- *e2e-capture-docker-logs-step
- name: Archive Docker logs
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always()
with:
name: e2e-web-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt
e2e-tests-web-ui:
<<: *e2e-web-job
name: End-to-End Tests (Web UI)
steps:
- *e2e-token-step
- *e2e-checkout-step
- *e2e-setup-pnpm-step
- *e2e-setup-node-step
- *e2e-setup-sdk-step
- *e2e-install-deps-step
- *e2e-install-playwright-step
- *e2e-download-images-step
- *e2e-load-images-step
- *e2e-docker-compose-up-step
- name: Run ui tests (web) - name: Run ui tests (web)
env: env:
PLAYWRIGHT_DISABLE_WEBSERVER: true PLAYWRIGHT_DISABLE_WEBSERVER: true
@@ -538,6 +627,27 @@ jobs:
with: with:
name: e2e-ui-test-results-${{ matrix.runner }} name: e2e-ui-test-results-${{ matrix.runner }}
path: e2e/playwright-report/ path: e2e/playwright-report/
- *e2e-capture-docker-logs-step
- name: Archive Docker logs
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always()
with:
name: e2e-web-ui-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt
e2e-tests-web-maintenance:
<<: *e2e-web-job
name: End-to-End Tests (Web Maintenance)
steps:
- *e2e-token-step
- *e2e-checkout-step
- *e2e-setup-pnpm-step
- *e2e-setup-node-step
- *e2e-setup-sdk-step
- *e2e-install-deps-step
- *e2e-install-playwright-step
- *e2e-download-images-step
- *e2e-load-images-step
- *e2e-docker-compose-up-step
- name: Run maintenance tests - name: Run maintenance tests
env: env:
PLAYWRIGHT_DISABLE_WEBSERVER: true PLAYWRIGHT_DISABLE_WEBSERVER: true
@@ -549,19 +659,16 @@ jobs:
with: with:
name: e2e-maintenance-isolated-test-results-${{ matrix.runner }} name: e2e-maintenance-isolated-test-results-${{ matrix.runner }}
path: e2e/playwright-report/ path: e2e/playwright-report/
- name: Capture Docker logs - *e2e-capture-docker-logs-step
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
- name: Archive Docker logs - name: Archive Docker logs
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always() if: always()
with: with:
name: e2e-web-docker-logs-${{ matrix.runner }} name: e2e-web-maintenance-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt path: e2e/docker-compose-logs.txt
success-check-e2e: success-check-e2e:
name: End-to-End Tests Success name: End-to-End Tests Success
needs: [e2e-tests-server-cli, e2e-tests-web] needs: [e2e-tests-server-cli, e2e-tests-server-maintenance, e2e-tests-web, e2e-tests-web-ui, e2e-tests-web-maintenance]
permissions: {} permissions: {}
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
@@ -23,10 +23,18 @@ import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.chromium.net.CronetEngine import org.chromium.net.CronetEngine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import java.io.IOException
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
import java.net.Authenticator import java.net.Authenticator
import java.net.CookieHandler import java.net.CookieHandler
import java.net.PasswordAuthentication import java.net.PasswordAuthentication
@@ -277,10 +285,13 @@ object HttpClientManager {
return result return result
} }
fun rebuildCronetEngine(): CronetEngine { suspend fun rebuildCronetEngine(): Result<Long> {
val old = cronetEngine!! return runCatching {
cronetEngine = buildCronetEngine() cronetEngine?.shutdown()
return old val deletionResult = deleteFolderAndGetSize(cronetStoragePath.toPath())
cronetEngine = buildCronetEngine()
deletionResult
}
} }
val cronetStoragePath: File get() = cronetStorageDir val cronetStoragePath: File get() = cronetStorageDir
@@ -301,7 +312,7 @@ object HttpClientManager {
} }
} }
private fun buildCronetEngine(): CronetEngine { fun buildCronetEngine(): CronetEngine {
return CronetEngine.Builder(appContext) return CronetEngine.Builder(appContext)
.enableHttp2(true) .enableHttp2(true)
.enableQuic(true) .enableQuic(true)
@@ -312,6 +323,27 @@ object HttpClientManager {
.build() .build()
} }
private suspend fun deleteFolderAndGetSize(root: Path): Long = withContext(Dispatchers.IO) {
var totalSize = 0L
Files.walkFileTree(root, object : SimpleFileVisitor<Path>() {
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
totalSize += attrs.size()
Files.delete(file)
return FileVisitResult.CONTINUE
}
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
if (dir != root) {
Files.delete(dir)
}
return FileVisitResult.CONTINUE
}
})
totalSize
}
private fun build(cacheDir: File): OkHttpClient { private fun build(cacheDir: File): OkHttpClient {
val connectionPool = ConnectionPool( val connectionPool = ConnectionPool(
maxIdleConnections = KEEP_ALIVE_CONNECTIONS, maxIdleConnections = KEEP_ALIVE_CONNECTIONS,
@@ -21,11 +21,6 @@ import java.io.EOFException
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
private class RemoteRequest(val cancellationSignal: CancellationSignal) private class RemoteRequest(val cancellationSignal: CancellationSignal)
@@ -205,18 +200,15 @@ private class CronetImageFetcher : ImageFetcher {
private fun onDrained() { private fun onDrained() {
val onCacheCleared = synchronized(stateLock) { val onCacheCleared = synchronized(stateLock) {
val onCacheCleared = onCacheCleared val onCacheCleared = this.onCacheCleared
this.onCacheCleared = null this.onCacheCleared = null
onCacheCleared onCacheCleared
} } ?: return
if (onCacheCleared != null) {
val oldEngine = HttpClientManager.rebuildCronetEngine() CoroutineScope(Dispatchers.IO).launch {
oldEngine.shutdown() val result = HttpClientManager.rebuildCronetEngine()
CoroutineScope(Dispatchers.IO).launch { synchronized(stateLock) { draining = false }
val result = runCatching { deleteFolderAndGetSize(HttpClientManager.cronetStoragePath.toPath()) } onCacheCleared(result)
synchronized(stateLock) { draining = false }
onCacheCleared(result)
}
} }
} }
@@ -306,26 +298,6 @@ private class CronetImageFetcher : ImageFetcher {
} }
} }
suspend fun deleteFolderAndGetSize(root: Path): Long = withContext(Dispatchers.IO) {
var totalSize = 0L
Files.walkFileTree(root, object : SimpleFileVisitor<Path>() {
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
totalSize += attrs.size()
Files.delete(file)
return FileVisitResult.CONTINUE
}
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
if (dir != root) {
Files.delete(dir)
}
return FileVisitResult.CONTINUE
}
})
totalSize
}
} }
private class OkHttpImageFetcher private constructor( private class OkHttpImageFetcher private constructor(
+1 -1
View File
@@ -7,7 +7,7 @@ const Map<String, Locale> locales = {
'Arabic (ar)': Locale('ar'), 'Arabic (ar)': Locale('ar'),
'Bulgarian (bg)': Locale('bg'), 'Bulgarian (bg)': Locale('bg'),
'Catalan (ca)': Locale('ca'), 'Catalan (ca)': Locale('ca'),
'Chinese Simplified (zh_CN)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'SIMPLIFIED'), 'Chinese Simplified (zh_CN)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'),
'Chinese Traditional (zh_TW)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), 'Chinese Traditional (zh_TW)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'),
'Croatian (hr)': Locale('hr'), 'Croatian (hr)': Locale('hr'),
'Czech (cs)': Locale('cs'), 'Czech (cs)': Locale('cs'),
@@ -9,7 +9,11 @@ import 'package:flutter/painting.dart';
/// Codec is disposed through the MultiFrameImageStreamCompleter's internals onDispose method /// Codec is disposed through the MultiFrameImageStreamCompleter's internals onDispose method
class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter { class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter {
void Function()? _onLastListenerRemoved; void Function()? _onLastListenerRemoved;
ImageStreamListener? _cacheListener; int _listenerCount = 0;
// True once any image or the codec has been provided.
// Until then the image cache holds one listener, so "last real listener gone"
// is _listenerCount == 1, not 0.
bool didProvideImage = false;
AnimatedImageStreamCompleter._({ AnimatedImageStreamCompleter._({
required super.codec, required super.codec,
@@ -34,15 +38,18 @@ class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter {
); );
if (initialImage != null) { if (initialImage != null) {
self.didProvideImage = true;
self.setImage(initialImage); self.setImage(initialImage);
} }
stream.listen( stream.listen(
(item) { (item) {
if (item is ImageInfo) { if (item is ImageInfo) {
self.didProvideImage = true;
self.setImage(item); self.setImage(item);
} else if (item is ui.Codec) { } else if (item is ui.Codec) {
if (!codecCompleter.isCompleted) { if (!codecCompleter.isCompleted) {
self.didProvideImage = true;
codecCompleter.complete(item); codecCompleter.complete(item);
} }
} }
@@ -53,6 +60,8 @@ class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter {
} }
}, },
onDone: () { onDone: () {
// also complete if we are done but no error occurred, and we didn't call complete yet
// could happen on cancellation
if (!codecCompleter.isCompleted) { if (!codecCompleter.isCompleted) {
codecCompleter.completeError(StateError('Stream closed without providing a codec')); codecCompleter.completeError(StateError('Stream closed without providing a codec'));
} }
@@ -64,20 +73,24 @@ class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter {
@override @override
void addListener(ImageStreamListener listener) { void addListener(ImageStreamListener listener) {
_cacheListener ??= listener;
super.addListener(listener); super.addListener(listener);
_listenerCount++;
} }
@override @override
void removeListener(ImageStreamListener listener) { void removeListener(ImageStreamListener listener) {
super.removeListener(listener); super.removeListener(listener);
if (listener != _cacheListener) { _listenerCount--;
_cancel();
final bool onlyCacheListenerLeft = _listenerCount == 1 && !didProvideImage;
final bool noListenersAfterCodec = _listenerCount == 0 && didProvideImage;
if (onlyCacheListenerLeft || noListenersAfterCodec) {
final onLastListenerRemoved = _onLastListenerRemoved;
if (onLastListenerRemoved != null) {
_onLastListenerRemoved = null;
onLastListenerRemoved();
}
} }
} }
void _cancel() {
_onLastListenerRemoved?.call();
_onLastListenerRemoved = null;
}
} }
@@ -10,7 +10,9 @@ import 'package:flutter/painting.dart';
/// An ImageStreamCompleter with support for loading multiple images. /// An ImageStreamCompleter with support for loading multiple images.
class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter { class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
void Function()? _onLastListenerRemoved; void Function()? _onLastListenerRemoved;
ImageStreamListener? _cacheListener; int _listenerCount = 0;
// True once setImage() has been called at least once.
bool didProvideImage = false;
/// The constructor to create an OneFramePlaceholderImageStreamCompleter. The [images] /// The constructor to create an OneFramePlaceholderImageStreamCompleter. The [images]
/// should be the primary images to display (typically asynchronously as they load). /// should be the primary images to display (typically asynchronously as they load).
@@ -21,12 +23,17 @@ class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
ImageInfo? initialImage, ImageInfo? initialImage,
InformationCollector? informationCollector, InformationCollector? informationCollector,
void Function()? onLastListenerRemoved, void Function()? onLastListenerRemoved,
}) : _onLastListenerRemoved = onLastListenerRemoved { }) {
if (initialImage != null) { if (initialImage != null) {
didProvideImage = true;
setImage(initialImage); setImage(initialImage);
} }
_onLastListenerRemoved = onLastListenerRemoved;
images.listen( images.listen(
(image) => setImage(image), (image) {
didProvideImage = true;
setImage(image);
},
onError: (Object error, StackTrace stack) { onError: (Object error, StackTrace stack) {
reportError( reportError(
context: ErrorDescription('resolving a single-frame image stream'), context: ErrorDescription('resolving a single-frame image stream'),
@@ -41,20 +48,23 @@ class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
@override @override
void addListener(ImageStreamListener listener) { void addListener(ImageStreamListener listener) {
_cacheListener ??= listener;
super.addListener(listener); super.addListener(listener);
_listenerCount = _listenerCount + 1;
} }
@override @override
void removeListener(ImageStreamListener listener) { void removeListener(ImageStreamListener listener) {
super.removeListener(listener); super.removeListener(listener);
if (listener != _cacheListener) { _listenerCount = _listenerCount - 1;
_cancel();
final bool onlyCacheListenerLeft = _listenerCount == 1 && !didProvideImage;
final bool noListenersAfterImage = _listenerCount == 0 && didProvideImage;
final onLastListenerRemoved = _onLastListenerRemoved;
if (onLastListenerRemoved != null && (noListenersAfterImage || onlyCacheListenerLeft)) {
_onLastListenerRemoved = null;
onLastListenerRemoved();
} }
} }
void _cancel() {
_onLastListenerRemoved?.call();
_onLastListenerRemoved = null;
}
} }
+1 -1
View File
@@ -20,7 +20,7 @@ final class CustomImageCache implements ImageCache {
set maximumSize(int value) => _small.maximumSize = value; set maximumSize(int value) => _small.maximumSize = value;
@override @override
set maximumSizeBytes(int value) => _small.maximumSize = value; set maximumSizeBytes(int value) => _small.maximumSizeBytes = value;
@override @override
void clear() { void clear() {
+4 -4
View File
@@ -1194,10 +1194,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.17.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -1897,10 +1897,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.6" version: "0.7.7"
thumbhash: thumbhash:
dependency: "direct main" dependency: "direct main"
description: description:
+86 -81
View File
@@ -748,7 +748,7 @@ importers:
version: link:../open-api/typescript-sdk version: link:../open-api/typescript-sdk
'@immich/ui': '@immich/ui':
specifier: ^0.65.3 specifier: ^0.65.3
version: 0.65.3(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7) version: 0.65.3(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)
'@mapbox/mapbox-gl-rtl-text': '@mapbox/mapbox-gl-rtl-text':
specifier: 0.3.0 specifier: 0.3.0
version: 0.3.0 version: 0.3.0
@@ -781,7 +781,7 @@ importers:
version: 0.42.0 version: 0.42.0
'@zoom-image/svelte': '@zoom-image/svelte':
specifier: ^0.3.0 specifier: ^0.3.0
version: 0.3.9(svelte@5.53.7) version: 0.3.9(svelte@5.53.13)
dom-to-image: dom-to-image:
specifier: ^2.6.0 specifier: ^2.6.0
version: 2.6.0 version: 2.6.0
@@ -832,16 +832,16 @@ importers:
version: 5.2.2 version: 5.2.2
svelte-i18n: svelte-i18n:
specifier: ^4.0.1 specifier: ^4.0.1
version: 4.0.1(svelte@5.53.7) version: 4.0.1(svelte@5.53.13)
svelte-jsoneditor: svelte-jsoneditor:
specifier: ^3.10.0 specifier: ^3.10.0
version: 3.11.0(svelte@5.53.7) version: 3.11.0(svelte@5.53.13)
svelte-maplibre: svelte-maplibre:
specifier: ^1.2.5 specifier: ^1.2.5
version: 1.2.6(svelte@5.53.7) version: 1.2.6(svelte@5.53.13)
svelte-persisted-store: svelte-persisted-store:
specifier: ^0.12.0 specifier: ^0.12.0
version: 0.12.0(svelte@5.53.7) version: 0.12.0(svelte@5.53.13)
tabbable: tabbable:
specifier: ^6.2.0 specifier: ^6.2.0
version: 6.4.0 version: 6.4.0
@@ -875,16 +875,16 @@ importers:
version: 3.1.2 version: 3.1.2
'@sveltejs/adapter-static': '@sveltejs/adapter-static':
specifier: ^3.0.8 specifier: ^3.0.8
version: 3.0.10(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))) version: 3.0.10(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))
'@sveltejs/enhanced-img': '@sveltejs/enhanced-img':
specifier: ^0.10.0 specifier: ^0.10.0
version: 0.10.3(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) version: 0.10.3(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
'@sveltejs/kit': '@sveltejs/kit':
specifier: ^2.27.1 specifier: ^2.27.1
version: 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) version: 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: 6.2.4 specifier: 6.2.4
version: 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) version: 6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.7 specifier: ^4.1.7
version: 4.2.1(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) version: 4.2.1(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
@@ -893,7 +893,7 @@ importers:
version: 6.9.1 version: 6.9.1
'@testing-library/svelte': '@testing-library/svelte':
specifier: ^5.2.8 specifier: ^5.2.8
version: 5.3.1(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) version: 5.3.1(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
'@testing-library/user-event': '@testing-library/user-event':
specifier: ^14.5.2 specifier: ^14.5.2
version: 14.6.1(@testing-library/dom@10.4.1) version: 14.6.1(@testing-library/dom@10.4.1)
@@ -932,7 +932,7 @@ importers:
version: 6.2.1(eslint@10.0.2(jiti@2.6.1)) version: 6.2.1(eslint@10.0.2(jiti@2.6.1))
eslint-plugin-svelte: eslint-plugin-svelte:
specifier: ^3.12.4 specifier: ^3.12.4
version: 3.15.0(eslint@10.0.2(jiti@2.6.1))(svelte@5.53.7) version: 3.15.0(eslint@10.0.2(jiti@2.6.1))(svelte@5.53.13)
eslint-plugin-unicorn: eslint-plugin-unicorn:
specifier: ^63.0.0 specifier: ^63.0.0
version: 63.0.0(eslint@10.0.2(jiti@2.6.1)) version: 63.0.0(eslint@10.0.2(jiti@2.6.1))
@@ -953,19 +953,19 @@ importers:
version: 4.2.0(prettier@3.8.1) version: 4.2.0(prettier@3.8.1)
prettier-plugin-svelte: prettier-plugin-svelte:
specifier: ^3.3.3 specifier: ^3.3.3
version: 3.5.1(prettier@3.8.1)(svelte@5.53.7) version: 3.5.1(prettier@3.8.1)(svelte@5.53.13)
rollup-plugin-visualizer: rollup-plugin-visualizer:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.11(rollup@4.55.1) version: 6.0.11(rollup@4.55.1)
svelte: svelte:
specifier: 5.53.7 specifier: 5.53.13
version: 5.53.7 version: 5.53.13
svelte-check: svelte-check:
specifier: ^4.1.5 specifier: ^4.1.5
version: 4.4.4(picomatch@4.0.3)(svelte@5.53.7)(typescript@5.9.3) version: 4.4.4(picomatch@4.0.3)(svelte@5.53.13)(typescript@5.9.3)
svelte-eslint-parser: svelte-eslint-parser:
specifier: ^1.3.3 specifier: ^1.3.3
version: 1.6.0(svelte@5.53.7) version: 1.6.0(svelte@5.53.13)
tailwindcss: tailwindcss:
specifier: ^4.1.7 specifier: ^4.1.7
version: 4.2.1 version: 4.2.1
@@ -6752,6 +6752,9 @@ packages:
devalue@5.6.3: devalue@5.6.3:
resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==}
devalue@5.6.4:
resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==}
devlop@1.1.0: devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
@@ -11218,8 +11221,8 @@ packages:
peerDependencies: peerDependencies:
svelte: ^5.30.2 svelte: ^5.30.2
svelte@5.53.7: svelte@5.53.13:
resolution: {integrity: sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==} resolution: {integrity: sha512-9P6I/jGcQMzAMb76Uyd6L6RELAC7qt53GOSBLCke9lubh9iJjmjCo+EffRH4gOPnTB/x4RR2Tmt6s3o9ywQO3g==}
engines: {node: '>=18'} engines: {node: '>=18'}
svg-parser@2.0.4: svg-parser@2.0.4:
@@ -14948,22 +14951,22 @@ snapshots:
pg-connection-string: 2.12.0 pg-connection-string: 2.12.0
postgres: 3.4.8 postgres: 3.4.8
'@immich/svelte-markdown-preprocess@0.2.1(svelte@5.53.7)': '@immich/svelte-markdown-preprocess@0.2.1(svelte@5.53.13)':
dependencies: dependencies:
front-matter: 4.0.2 front-matter: 4.0.2
marked: 17.0.3 marked: 17.0.3
node-emoji: 2.2.0 node-emoji: 2.2.0
svelte: 5.53.7 svelte: 5.53.13
'@immich/ui@0.65.3(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)': '@immich/ui@0.65.3(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)':
dependencies: dependencies:
'@immich/svelte-markdown-preprocess': 0.2.1(svelte@5.53.7) '@immich/svelte-markdown-preprocess': 0.2.1(svelte@5.53.13)
'@internationalized/date': 3.10.0 '@internationalized/date': 3.10.0
'@mdi/js': 7.4.47 '@mdi/js': 7.4.47
bits-ui: 2.16.0(@internationalized/date@3.10.0)(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7) bits-ui: 2.16.0(@internationalized/date@3.10.0)(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)
luxon: 3.7.2 luxon: 3.7.2
simple-icons: 16.9.0 simple-icons: 16.9.0
svelte: 5.53.7 svelte: 5.53.13
svelte-highlight: 7.9.0 svelte-highlight: 7.9.0
tailwind-merge: 3.5.0 tailwind-merge: 3.5.0
tailwind-variants: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.1) tailwind-variants: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.1)
@@ -16300,17 +16303,17 @@ snapshots:
dependencies: dependencies:
acorn: 8.16.0 acorn: 8.16.0
'@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))':
dependencies: dependencies:
'@sveltejs/kit': 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/kit': 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
'@sveltejs/enhanced-img@0.10.3(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': '@sveltejs/enhanced-img@0.10.3(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
magic-string: 0.30.21 magic-string: 0.30.21
sharp: 0.34.5 sharp: 0.34.5
svelte: 5.53.7 svelte: 5.53.13
svelte-parse-markup: 0.1.5(svelte@5.53.7) svelte-parse-markup: 0.1.5(svelte@5.53.13)
vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
vite-imagetools: 9.0.3(rollup@4.55.1) vite-imagetools: 9.0.3(rollup@4.55.1)
zimmerframe: 1.1.4 zimmerframe: 1.1.4
@@ -16318,11 +16321,11 @@ snapshots:
- rollup - rollup
- supports-color - supports-color
'@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': '@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies: dependencies:
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
'@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0)
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
'@types/cookie': 0.6.0 '@types/cookie': 0.6.0
acorn: 8.16.0 acorn: 8.16.0
cookie: 0.6.0 cookie: 0.6.0
@@ -16333,28 +16336,28 @@ snapshots:
mrmime: 2.0.1 mrmime: 2.0.1
set-cookie-parser: 3.0.1 set-cookie-parser: 3.0.1
sirv: 3.0.2 sirv: 3.0.2
svelte: 5.53.7 svelte: 5.53.13
vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
optionalDependencies: optionalDependencies:
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
typescript: 5.9.3 typescript: 5.9.3
'@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
debug: 4.4.3 debug: 4.4.3
svelte: 5.53.7 svelte: 5.53.13
vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
deepmerge: 4.3.1 deepmerge: 4.3.1
magic-string: 0.30.21 magic-string: 0.30.21
obug: 2.1.1 obug: 2.1.1
svelte: 5.53.7 svelte: 5.53.13
vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
vitefu: 1.1.1(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vitefu: 1.1.1(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
transitivePeerDependencies: transitivePeerDependencies:
@@ -16602,15 +16605,15 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
redent: 3.0.0 redent: 3.0.0
'@testing-library/svelte-core@1.0.0(svelte@5.53.7)': '@testing-library/svelte-core@1.0.0(svelte@5.53.13)':
dependencies: dependencies:
svelte: 5.53.7 svelte: 5.53.13
'@testing-library/svelte@5.3.1(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': '@testing-library/svelte@5.3.1(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies: dependencies:
'@testing-library/dom': 10.4.1 '@testing-library/dom': 10.4.1
'@testing-library/svelte-core': 1.0.0(svelte@5.53.7) '@testing-library/svelte-core': 1.0.0(svelte@5.53.13)
svelte: 5.53.7 svelte: 5.53.13
optionalDependencies: optionalDependencies:
vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
@@ -17527,10 +17530,10 @@ snapshots:
dependencies: dependencies:
'@namnode/store': 0.1.0 '@namnode/store': 0.1.0
'@zoom-image/svelte@0.3.9(svelte@5.53.7)': '@zoom-image/svelte@0.3.9(svelte@5.53.13)':
dependencies: dependencies:
'@zoom-image/core': 0.42.0 '@zoom-image/core': 0.42.0
svelte: 5.53.7 svelte: 5.53.13
abbrev@1.1.1: {} abbrev@1.1.1: {}
@@ -17894,15 +17897,15 @@ snapshots:
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
bits-ui@2.16.0(@internationalized/date@3.10.0)(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7): bits-ui@2.16.0(@internationalized/date@3.10.0)(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13):
dependencies: dependencies:
'@floating-ui/core': 1.7.3 '@floating-ui/core': 1.7.3
'@floating-ui/dom': 1.7.4 '@floating-ui/dom': 1.7.4
'@internationalized/date': 3.10.0 '@internationalized/date': 3.10.0
esm-env: 1.2.2 esm-env: 1.2.2
runed: 0.35.1(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7) runed: 0.35.1(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)
svelte: 5.53.7 svelte: 5.53.13
svelte-toolbelt: 0.10.6(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7) svelte-toolbelt: 0.10.6(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)
tabbable: 6.4.0 tabbable: 6.4.0
transitivePeerDependencies: transitivePeerDependencies:
- '@sveltejs/kit' - '@sveltejs/kit'
@@ -18995,6 +18998,8 @@ snapshots:
devalue@5.6.3: {} devalue@5.6.3: {}
devalue@5.6.4: {}
devlop@1.1.0: devlop@1.1.0:
dependencies: dependencies:
dequal: 2.0.3 dequal: 2.0.3
@@ -19403,7 +19408,7 @@ snapshots:
'@types/eslint': 9.6.1 '@types/eslint': 9.6.1
eslint-config-prettier: 10.1.8(eslint@10.0.2(jiti@2.6.1)) eslint-config-prettier: 10.1.8(eslint@10.0.2(jiti@2.6.1))
eslint-plugin-svelte@3.15.0(eslint@10.0.2(jiti@2.6.1))(svelte@5.53.7): eslint-plugin-svelte@3.15.0(eslint@10.0.2(jiti@2.6.1))(svelte@5.53.13):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1))
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@@ -19415,9 +19420,9 @@ snapshots:
postcss-load-config: 3.1.4(postcss@8.5.8) postcss-load-config: 3.1.4(postcss@8.5.8)
postcss-safe-parser: 7.0.1(postcss@8.5.8) postcss-safe-parser: 7.0.1(postcss@8.5.8)
semver: 7.7.4 semver: 7.7.4
svelte-eslint-parser: 1.6.0(svelte@5.53.7) svelte-eslint-parser: 1.6.0(svelte@5.53.13)
optionalDependencies: optionalDependencies:
svelte: 5.53.7 svelte: 5.53.13
transitivePeerDependencies: transitivePeerDependencies:
- ts-node - ts-node
@@ -23155,10 +23160,10 @@ snapshots:
dependencies: dependencies:
prettier: 3.8.1 prettier: 3.8.1
prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.53.7): prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.53.13):
dependencies: dependencies:
prettier: 3.8.1 prettier: 3.8.1
svelte: 5.53.7 svelte: 5.53.13
prettier@3.8.1: {} prettier@3.8.1: {}
@@ -23763,14 +23768,14 @@ snapshots:
dependencies: dependencies:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
runed@0.35.1(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7): runed@0.35.1(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13):
dependencies: dependencies:
dequal: 2.0.3 dequal: 2.0.3
esm-env: 1.2.2 esm-env: 1.2.2
lz-string: 1.5.0 lz-string: 1.5.0
svelte: 5.53.7 svelte: 5.53.13
optionalDependencies: optionalDependencies:
'@sveltejs/kit': 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/kit': 2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
rw@1.3.3: {} rw@1.3.3: {}
@@ -24394,23 +24399,23 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
svelte-awesome@3.3.5(svelte@5.53.7): svelte-awesome@3.3.5(svelte@5.53.13):
dependencies: dependencies:
svelte: 5.53.7 svelte: 5.53.13
svelte-check@4.4.4(picomatch@4.0.3)(svelte@5.53.7)(typescript@5.9.3): svelte-check@4.4.4(picomatch@4.0.3)(svelte@5.53.13)(typescript@5.9.3):
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
chokidar: 4.0.3 chokidar: 4.0.3
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
picocolors: 1.1.1 picocolors: 1.1.1
sade: 1.8.1 sade: 1.8.1
svelte: 5.53.7 svelte: 5.53.13
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- picomatch - picomatch
svelte-eslint-parser@1.6.0(svelte@5.53.7): svelte-eslint-parser@1.6.0(svelte@5.53.13):
dependencies: dependencies:
eslint-scope: 8.4.0 eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1 eslint-visitor-keys: 4.2.1
@@ -24420,7 +24425,7 @@ snapshots:
postcss-selector-parser: 7.1.1 postcss-selector-parser: 7.1.1
semver: 7.7.4 semver: 7.7.4
optionalDependencies: optionalDependencies:
svelte: 5.53.7 svelte: 5.53.13
svelte-floating-ui@1.5.8: svelte-floating-ui@1.5.8:
dependencies: dependencies:
@@ -24433,7 +24438,7 @@ snapshots:
dependencies: dependencies:
highlight.js: 11.11.1 highlight.js: 11.11.1
svelte-i18n@4.0.1(svelte@5.53.7): svelte-i18n@4.0.1(svelte@5.53.13):
dependencies: dependencies:
cli-color: 2.0.4 cli-color: 2.0.4
deepmerge: 4.3.1 deepmerge: 4.3.1
@@ -24441,10 +24446,10 @@ snapshots:
estree-walker: 2.0.2 estree-walker: 2.0.2
intl-messageformat: 10.7.18 intl-messageformat: 10.7.18
sade: 1.8.1 sade: 1.8.1
svelte: 5.53.7 svelte: 5.53.13
tiny-glob: 0.2.9 tiny-glob: 0.2.9
svelte-jsoneditor@3.11.0(svelte@5.53.7): svelte-jsoneditor@3.11.0(svelte@5.53.13):
dependencies: dependencies:
'@codemirror/autocomplete': 6.20.0 '@codemirror/autocomplete': 6.20.0
'@codemirror/commands': 6.10.1 '@codemirror/commands': 6.10.1
@@ -24471,42 +24476,42 @@ snapshots:
memoize-one: 6.0.0 memoize-one: 6.0.0
natural-compare-lite: 1.4.0 natural-compare-lite: 1.4.0
sass: 1.97.1 sass: 1.97.1
svelte: 5.53.7 svelte: 5.53.13
svelte-awesome: 3.3.5(svelte@5.53.7) svelte-awesome: 3.3.5(svelte@5.53.13)
svelte-select: 5.8.3 svelte-select: 5.8.3
vanilla-picker: 2.12.3 vanilla-picker: 2.12.3
svelte-maplibre@1.2.6(svelte@5.53.7): svelte-maplibre@1.2.6(svelte@5.53.13):
dependencies: dependencies:
d3-geo: 3.1.1 d3-geo: 3.1.1
dequal: 2.0.3 dequal: 2.0.3
just-compare: 2.3.0 just-compare: 2.3.0
maplibre-gl: 5.19.0 maplibre-gl: 5.19.0
pmtiles: 3.2.1 pmtiles: 3.2.1
svelte: 5.53.7 svelte: 5.53.13
svelte-parse-markup@0.1.5(svelte@5.53.7): svelte-parse-markup@0.1.5(svelte@5.53.13):
dependencies: dependencies:
svelte: 5.53.7 svelte: 5.53.13
svelte-persisted-store@0.12.0(svelte@5.53.7): svelte-persisted-store@0.12.0(svelte@5.53.13):
dependencies: dependencies:
svelte: 5.53.7 svelte: 5.53.13
svelte-select@5.8.3: svelte-select@5.8.3:
dependencies: dependencies:
svelte-floating-ui: 1.5.8 svelte-floating-ui: 1.5.8
svelte-toolbelt@0.10.6(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7): svelte-toolbelt@0.10.6(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13):
dependencies: dependencies:
clsx: 2.1.1 clsx: 2.1.1
runed: 0.35.1(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.7) runed: 0.35.1(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.13)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.13)
style-to-object: 1.0.14 style-to-object: 1.0.14
svelte: 5.53.7 svelte: 5.53.13
transitivePeerDependencies: transitivePeerDependencies:
- '@sveltejs/kit' - '@sveltejs/kit'
svelte@5.53.7: svelte@5.53.13:
dependencies: dependencies:
'@jridgewell/remapping': 2.3.5 '@jridgewell/remapping': 2.3.5
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@@ -24517,7 +24522,7 @@ snapshots:
aria-query: 5.3.1 aria-query: 5.3.1
axobject-query: 4.1.0 axobject-query: 4.1.0
clsx: 2.1.1 clsx: 2.1.1
devalue: 5.6.3 devalue: 5.6.4
esm-env: 1.2.2 esm-env: 1.2.2
esrap: 2.2.3 esrap: 2.2.3
is-reference: 3.0.3 is-reference: 3.0.3
+1 -1
View File
@@ -24,7 +24,7 @@
"typeorm": "typeorm", "typeorm": "typeorm",
"migrations:debug": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate --debug", "migrations:debug": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate --debug",
"migrations:generate": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate", "migrations:generate": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate",
"migrations:create": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate", "migrations:create": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations create",
"migrations:run": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations run", "migrations:run": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations run",
"migrations:revert": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations revert", "migrations:revert": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations revert",
"schema:drop": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} query 'DROP schema public cascade; CREATE schema public;'", "schema:drop": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} query 'DROP schema public cascade; CREATE schema public;'",
+1 -9
View File
@@ -169,6 +169,7 @@ export type AuthSharedLink = {
id: string; id: string;
expiresAt: Date | null; expiresAt: Date | null;
userId: string; userId: string;
albumId: string | null;
showExif: boolean; showExif: boolean;
allowUpload: boolean; allowUpload: boolean;
allowDownload: boolean; allowDownload: boolean;
@@ -357,15 +358,6 @@ export const columns = {
authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'], authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'],
authApiKey: ['api_key.id', 'api_key.permissions'], authApiKey: ['api_key.id', 'api_key.permissions'],
authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt', 'session.appVersion'], authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt', 'session.appVersion'],
authSharedLink: [
'shared_link.id',
'shared_link.userId',
'shared_link.expiresAt',
'shared_link.showExif',
'shared_link.allowUpload',
'shared_link.allowDownload',
'shared_link.password',
],
user: userColumns, user: userColumns,
userWithPrefix: userWithPrefixColumns, userWithPrefix: userWithPrefixColumns,
userAdmin: [ userAdmin: [
@@ -173,6 +173,7 @@ order by
select select
"shared_link"."id", "shared_link"."id",
"shared_link"."userId", "shared_link"."userId",
"shared_link"."albumId",
"shared_link"."expiresAt", "shared_link"."expiresAt",
"shared_link"."showExif", "shared_link"."showExif",
"shared_link"."allowUpload", "shared_link"."allowUpload",
@@ -211,6 +212,7 @@ where
select select
"shared_link"."id", "shared_link"."id",
"shared_link"."userId", "shared_link"."userId",
"shared_link"."albumId",
"shared_link"."expiresAt", "shared_link"."expiresAt",
"shared_link"."showExif", "shared_link"."showExif",
"shared_link"."allowUpload", "shared_link"."allowUpload",
@@ -330,6 +330,7 @@ export class AlbumRepository {
await db await db
.insertInto('album_asset') .insertInto('album_asset')
.values(assetIds.map((assetId) => ({ albumId, assetId }))) .values(assetIds.map((assetId) => ({ albumId, assetId })))
.onConflict((oc) => oc.doNothing())
.execute(); .execute();
} }
@@ -202,7 +202,14 @@ export class SharedLinkRepository {
.leftJoin('album', 'album.id', 'shared_link.albumId') .leftJoin('album', 'album.id', 'shared_link.albumId')
.where('album.deletedAt', 'is', null) .where('album.deletedAt', 'is', null)
.select((eb) => [ .select((eb) => [
...columns.authSharedLink, 'shared_link.id',
'shared_link.userId',
'shared_link.albumId',
'shared_link.expiresAt',
'shared_link.showExif',
'shared_link.allowUpload',
'shared_link.allowDownload',
'shared_link.password',
jsonObjectFrom( jsonObjectFrom(
eb.selectFrom('user').select(columns.authUser).whereRef('user.id', '=', 'shared_link.userId'), eb.selectFrom('user').select(columns.authUser).whereRef('user.id', '=', 'shared_link.userId'),
).as('user'), ).as('user'),
@@ -0,0 +1,13 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`
DELETE FROM "shared_link_asset"
USING "shared_link"
WHERE "shared_link_asset"."sharedLinkId" = "shared_link"."id" AND "shared_link"."type" = 'ALBUM';
`.execute(db);
}
export async function down(): Promise<void> {
// noop
}
+12
View File
@@ -165,6 +165,12 @@ export class AlbumService extends BaseService {
} }
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
if (auth.sharedLink) {
this.logger.deprecate(
'Assets uploaded to a shared link are automatically added and calling this endpoint is no longer necessary. It will be removed in the next major release.',
);
}
const album = await this.findOrFail(id, { withAssets: false }); const album = await this.findOrFail(id, { withAssets: false });
await this.requireAccess({ auth, permission: Permission.AlbumAssetCreate, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AlbumAssetCreate, ids: [id] });
@@ -195,6 +201,12 @@ export class AlbumService extends BaseService {
} }
async addAssetsToAlbums(auth: AuthDto, dto: AlbumsAddAssetsDto): Promise<AlbumsAddAssetsResponseDto> { async addAssetsToAlbums(auth: AuthDto, dto: AlbumsAddAssetsDto): Promise<AlbumsAddAssetsResponseDto> {
if (auth.sharedLink) {
this.logger.deprecate(
'Assets uploaded to a shared link are automatically added and calling this endpoint is no longer necessary. It will be removed in the next major release.',
);
}
const results: AlbumsAddAssetsResponseDto = { const results: AlbumsAddAssetsResponseDto = {
success: false, success: false,
error: BulkIdErrorReason.DUPLICATE, error: BulkIdErrorReason.DUPLICATE,
+9 -3
View File
@@ -2,7 +2,7 @@ import { BadRequestException, Injectable, InternalServerErrorException, NotFound
import { extname } from 'node:path'; import { extname } from 'node:path';
import sanitize from 'sanitize-filename'; import sanitize from 'sanitize-filename';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { Asset } from 'src/database'; import { Asset, AuthSharedLink } from 'src/database';
import { import {
AssetBulkUploadCheckResponseDto, AssetBulkUploadCheckResponseDto,
AssetMediaResponseDto, AssetMediaResponseDto,
@@ -152,7 +152,7 @@ export class AssetMediaService extends BaseService {
const asset = await this.create(auth.user.id, dto, file, sidecarFile); const asset = await this.create(auth.user.id, dto, file, sidecarFile);
if (auth.sharedLink) { if (auth.sharedLink) {
await this.sharedLinkRepository.addAssets(auth.sharedLink.id, [asset.id]); await this.addToSharedLink(auth.sharedLink, asset.id);
} }
await this.userRepository.updateUsage(auth.user.id, file.size); await this.userRepository.updateUsage(auth.user.id, file.size);
@@ -326,6 +326,12 @@ export class AssetMediaService extends BaseService {
}; };
} }
private async addToSharedLink(sharedLink: AuthSharedLink, assetId: string) {
await (sharedLink.albumId
? this.albumRepository.addAssetIds(sharedLink.albumId, [assetId])
: this.sharedLinkRepository.addAssets(sharedLink.id, [assetId]));
}
private async handleUploadError( private async handleUploadError(
error: any, error: any,
auth: AuthDto, auth: AuthDto,
@@ -347,7 +353,7 @@ export class AssetMediaService extends BaseService {
} }
if (auth.sharedLink) { if (auth.sharedLink) {
await this.sharedLinkRepository.addAssets(auth.sharedLink.id, [duplicateId]); await this.addToSharedLink(auth.sharedLink, duplicateId);
} }
return { status: AssetMediaStatus.DUPLICATE, id: duplicateId }; return { status: AssetMediaStatus.DUPLICATE, id: duplicateId };
@@ -1,5 +1,6 @@
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { SemVer } from 'semver'; import { SemVer } from 'semver';
import { defaults } from 'src/config';
import { serverVersion } from 'src/constants'; import { serverVersion } from 'src/constants';
import { ImmichEnvironment, JobName, JobStatus, SystemMetadataKey } from 'src/enum'; import { ImmichEnvironment, JobName, JobStatus, SystemMetadataKey } from 'src/enum';
import { VersionService } from 'src/services/version.service'; import { VersionService } from 'src/services/version.service';
@@ -130,6 +131,32 @@ describe(VersionService.name, () => {
}); });
}); });
describe('onConfigUpdate', () => {
it('should queue a version check job when newVersionCheck is enabled', async () => {
await sut.onConfigUpdate({
oldConfig: { ...defaults, newVersionCheck: { enabled: false } },
newConfig: { ...defaults, newVersionCheck: { enabled: true } },
});
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.VersionCheck, data: {} });
});
it('should not queue a version check job when newVersionCheck is disabled', async () => {
await sut.onConfigUpdate({
oldConfig: { ...defaults, newVersionCheck: { enabled: true } },
newConfig: { ...defaults, newVersionCheck: { enabled: false } },
});
expect(mocks.job.queue).not.toHaveBeenCalled();
});
it('should not queue a version check job when newVersionCheck was already enabled', async () => {
await sut.onConfigUpdate({
oldConfig: { ...defaults, newVersionCheck: { enabled: true } },
newConfig: { ...defaults, newVersionCheck: { enabled: true } },
});
expect(mocks.job.queue).not.toHaveBeenCalled();
});
});
describe('onWebsocketConnection', () => { describe('onWebsocketConnection', () => {
it('should send on_server_version client event', async () => { it('should send on_server_version client event', async () => {
await sut.onWebsocketConnection({ userId: '42' }); await sut.onWebsocketConnection({ userId: '42' });
+7
View File
@@ -55,6 +55,13 @@ export class VersionService extends BaseService {
return this.versionRepository.getAll(); return this.versionRepository.getAll();
} }
@OnEvent({ name: 'ConfigUpdate' })
async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'ConfigUpdate'>) {
if (!oldConfig.newVersionCheck.enabled && newConfig.newVersionCheck.enabled) {
await this.handleQueueVersionCheck();
}
}
async handleQueueVersionCheck() { async handleQueueVersionCheck() {
await this.jobRepository.queue({ name: JobName.VersionCheck, data: {} }); await this.jobRepository.queue({ name: JobName.VersionCheck, data: {} });
} }
+1
View File
@@ -48,6 +48,7 @@ export const authStub = {
showExif: true, showExif: true,
allowDownload: true, allowDownload: true,
allowUpload: true, allowUpload: true,
albumId: null,
expiresAt: null, expiresAt: null,
password: null, password: null,
userId: '42', userId: '42',
+2 -2
View File
@@ -220,9 +220,9 @@ export class MediumTestContext<S extends BaseService = BaseService> {
return { result }; return { result };
} }
async newAlbum(dto: Insertable<AlbumTable>) { async newAlbum(dto: Insertable<AlbumTable>, assetIds?: string[]) {
const album = mediumFactory.albumInsert(dto); const album = mediumFactory.albumInsert(dto);
const result = await this.get(AlbumRepository).create(album, [], []); const result = await this.get(AlbumRepository).create(album, assetIds ?? [], []);
return { album, result }; return { album, result };
} }
@@ -1,12 +1,15 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { randomBytes } from 'node:crypto';
import { AssetMediaStatus } from 'src/dtos/asset-media-response.dto'; import { AssetMediaStatus } from 'src/dtos/asset-media-response.dto';
import { AssetMediaSize } from 'src/dtos/asset-media.dto'; import { AssetMediaSize } from 'src/dtos/asset-media.dto';
import { AssetFileType } from 'src/enum'; import { AssetFileType, SharedLinkType } from 'src/enum';
import { AccessRepository } from 'src/repositories/access.repository'; import { AccessRepository } from 'src/repositories/access.repository';
import { AlbumRepository } from 'src/repositories/album.repository';
import { AssetRepository } from 'src/repositories/asset.repository'; import { AssetRepository } from 'src/repositories/asset.repository';
import { EventRepository } from 'src/repositories/event.repository'; import { EventRepository } from 'src/repositories/event.repository';
import { JobRepository } from 'src/repositories/job.repository'; import { JobRepository } from 'src/repositories/job.repository';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
import { StorageRepository } from 'src/repositories/storage.repository'; import { StorageRepository } from 'src/repositories/storage.repository';
import { UserRepository } from 'src/repositories/user.repository'; import { UserRepository } from 'src/repositories/user.repository';
import { DB } from 'src/schema'; import { DB } from 'src/schema';
@@ -22,7 +25,7 @@ let defaultDatabase: Kysely<DB>;
const setup = (db?: Kysely<DB>) => { const setup = (db?: Kysely<DB>) => {
return newMediumService(AssetMediaService, { return newMediumService(AssetMediaService, {
database: db || defaultDatabase, database: db || defaultDatabase,
real: [AccessRepository, AssetRepository, UserRepository], real: [AccessRepository, AlbumRepository, AssetRepository, SharedLinkRepository, UserRepository],
mock: [EventRepository, LoggingRepository, JobRepository, StorageRepository], mock: [EventRepository, LoggingRepository, JobRepository, StorageRepository],
}); });
}; };
@@ -44,7 +47,6 @@ describe(AssetService.name, () => {
const { asset } = await ctx.newAsset({ ownerId: user.id }); const { asset } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({ assetId: asset.id, fileSizeInByte: 12_345 }); await ctx.newExif({ assetId: asset.id, fileSizeInByte: 12_345 });
const auth = factory.auth({ user: { id: user.id } }); const auth = factory.auth({ user: { id: user.id } });
const file = mediumFactory.uploadFile();
await expect( await expect(
sut.uploadAsset( sut.uploadAsset(
@@ -56,7 +58,7 @@ describe(AssetService.name, () => {
fileCreatedAt: new Date(), fileCreatedAt: new Date(),
assetData: Buffer.from('some data'), assetData: Buffer.from('some data'),
}, },
file, mediumFactory.uploadFile(),
), ),
).resolves.toEqual({ ).resolves.toEqual({
id: expect.any(String), id: expect.any(String),
@@ -99,6 +101,168 @@ describe(AssetService.name, () => {
status: AssetMediaStatus.CREATED, status: AssetMediaStatus.CREATED,
}); });
}); });
it('should add to a shared link', async () => {
const { sut, ctx } = setup();
const sharedLinkRepo = ctx.get(SharedLinkRepository);
ctx.getMock(StorageRepository).utimes.mockResolvedValue();
ctx.getMock(EventRepository).emit.mockResolvedValue();
ctx.getMock(JobRepository).queue.mockResolvedValue();
const { user } = await ctx.newUser();
const sharedLink = await sharedLinkRepo.create({
key: randomBytes(50),
type: SharedLinkType.Individual,
description: 'Shared link description',
userId: user.id,
allowDownload: true,
allowUpload: true,
});
const auth = factory.auth({ user: { id: user.id }, sharedLink });
const file = mediumFactory.uploadFile();
const uploadDto = {
deviceId: 'some-id',
deviceAssetId: 'some-id',
fileModifiedAt: new Date(),
fileCreatedAt: new Date(),
assetData: Buffer.from('some data'),
};
const response = await sut.uploadAsset(auth, uploadDto, file);
expect(response).toEqual({ id: expect.any(String), status: AssetMediaStatus.CREATED });
const update = await sharedLinkRepo.get(user.id, sharedLink.id);
const assets = update!.assets;
expect(assets).toHaveLength(1);
expect(assets[0]).toMatchObject({ id: response.id });
});
it('should handle adding a duplicate asset to a shared link', async () => {
const { sut, ctx } = setup();
ctx.getMock(StorageRepository).utimes.mockResolvedValue();
ctx.getMock(EventRepository).emit.mockResolvedValue();
ctx.getMock(JobRepository).queue.mockResolvedValue();
const sharedLinkRepo = ctx.get(SharedLinkRepository);
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({ assetId: asset.id, fileSizeInByte: 12_345 });
const sharedLink = await sharedLinkRepo.create({
key: randomBytes(50),
type: SharedLinkType.Individual,
description: 'Shared link description',
userId: user.id,
allowDownload: true,
allowUpload: true,
assetIds: [asset.id],
});
const auth = factory.auth({ user: { id: user.id }, sharedLink });
const uploadDto = {
deviceId: 'some-id',
deviceAssetId: 'some-id',
fileModifiedAt: new Date(),
fileCreatedAt: new Date(),
assetData: Buffer.from('some data'),
};
const response = await sut.uploadAsset(auth, uploadDto, mediumFactory.uploadFile({ checksum: asset.checksum }));
expect(response).toEqual({ id: expect.any(String), status: AssetMediaStatus.DUPLICATE });
const update = await sharedLinkRepo.get(user.id, sharedLink.id);
const assets = update!.assets;
expect(assets).toHaveLength(1);
expect(assets[0]).toMatchObject({ id: response.id });
});
it('should add to an album shared link', async () => {
const { sut, ctx } = setup();
const sharedLinkRepo = ctx.get(SharedLinkRepository);
ctx.getMock(StorageRepository).utimes.mockResolvedValue();
ctx.getMock(EventRepository).emit.mockResolvedValue();
ctx.getMock(JobRepository).queue.mockResolvedValue();
const { user } = await ctx.newUser();
const { album } = await ctx.newAlbum({ ownerId: user.id });
const sharedLink = await sharedLinkRepo.create({
key: randomBytes(50),
type: SharedLinkType.Album,
albumId: album.id,
description: 'Shared link description',
userId: user.id,
allowDownload: true,
allowUpload: true,
});
const auth = factory.auth({ user: { id: user.id }, sharedLink });
const uploadDto = {
deviceId: 'some-id',
deviceAssetId: 'some-id',
fileModifiedAt: new Date(),
fileCreatedAt: new Date(),
assetData: Buffer.from('some data'),
};
const response = await sut.uploadAsset(auth, uploadDto, mediumFactory.uploadFile());
expect(response).toEqual({ id: expect.any(String), status: AssetMediaStatus.CREATED });
const result = await ctx.get(AlbumRepository).getAssetIds(album.id, [response.id]);
const assets = [...result];
expect(assets).toHaveLength(1);
expect(assets[0]).toEqual(response.id);
});
it('should handle adding a duplicate asset to an album shared link', async () => {
const { sut, ctx } = setup();
const sharedLinkRepo = ctx.get(SharedLinkRepository);
ctx.getMock(StorageRepository).utimes.mockResolvedValue();
ctx.getMock(EventRepository).emit.mockResolvedValue();
ctx.getMock(JobRepository).queue.mockResolvedValue();
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
const { album } = await ctx.newAlbum({ ownerId: user.id }, [asset.id]);
// await ctx.newExif({ assetId: asset.id, fileSizeInByte: 12_345 });
const sharedLink = await sharedLinkRepo.create({
key: randomBytes(50),
type: SharedLinkType.Album,
albumId: album.id,
description: 'Shared link description',
userId: user.id,
allowDownload: true,
allowUpload: true,
});
const auth = factory.auth({ user: { id: user.id }, sharedLink });
const uploadDto = {
deviceId: 'some-id',
deviceAssetId: 'some-id',
fileModifiedAt: new Date(),
fileCreatedAt: new Date(),
assetData: Buffer.from('some data'),
};
const response = await sut.uploadAsset(auth, uploadDto, mediumFactory.uploadFile({ checksum: asset.checksum }));
expect(response).toEqual({ id: expect.any(String), status: AssetMediaStatus.DUPLICATE });
const result = await ctx.get(AlbumRepository).getAssetIds(album.id, [response.id]);
const assets = [...result];
expect(assets).toHaveLength(1);
expect(assets[0]).toEqual(response.id);
});
}); });
describe('viewThumbnail', () => { describe('viewThumbnail', () => {
+11 -1
View File
@@ -63,12 +63,22 @@ const authSharedLinkFactory = (sharedLink: Partial<AuthSharedLink> = {}) => {
expiresAt = null, expiresAt = null,
userId = newUuid(), userId = newUuid(),
showExif = true, showExif = true,
albumId = null,
allowUpload = false, allowUpload = false,
allowDownload = true, allowDownload = true,
password = null, password = null,
} = sharedLink; } = sharedLink;
return { id, expiresAt, userId, showExif, allowUpload, allowDownload, password }; return {
id,
albumId,
expiresAt,
userId,
showExif,
allowUpload,
allowDownload,
password,
};
}; };
const authApiKeyFactory = (apiKey: Partial<AuthApiKey> = {}) => ({ const authApiKeyFactory = (apiKey: Partial<AuthApiKey> = {}) => ({
+1 -1
View File
@@ -100,7 +100,7 @@
"prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"rollup-plugin-visualizer": "^6.0.0", "rollup-plugin-visualizer": "^6.0.0",
"svelte": "5.53.7", "svelte": "5.53.13",
"svelte-check": "^4.1.5", "svelte-check": "^4.1.5",
"svelte-eslint-parser": "^1.3.3", "svelte-eslint-parser": "^1.3.3",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.7",
@@ -212,12 +212,12 @@
bottom: `${rootHeight - top}px`, bottom: `${rootHeight - top}px`,
left: `${left}px`, left: `${left}px`,
width: `${boundary.width}px`, width: `${boundary.width}px`,
maxHeight: maxHeight(top - dropdownOffset), maxHeight: maxHeight(boundary.top - dropdownOffset),
}; };
} }
const viewportHeight = visualViewport?.height || rootHeight; const viewportHeight = visualViewport?.height || window.innerHeight;
const availableHeight = modalBounds ? rootHeight - bottom : viewportHeight - boundary.bottom; const availableHeight = viewportHeight - boundary.bottom;
return { return {
top: `${bottom}px`, top: `${bottom}px`,
left: `${left}px`, left: `${left}px`,
+1 -1
View File
@@ -216,7 +216,7 @@ async function fileUploader({
uploadAssetsStore.track('success'); uploadAssetsStore.track('success');
} }
if (albumId) { if (albumId && !authManager.isSharedLink) {
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_adding_to_album') }); uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_adding_to_album') });
await addAssetsToAlbums([albumId], [responseData.id], { notify: false }); await addAssetsToAlbums([albumId], [responseData.id], { notify: false });
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_added_to_album') }); uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_added_to_album') });
@@ -287,7 +287,11 @@
} }
}; };
const onAlbumAddAssets = async () => { const onAlbumAddAssets = async ({ albumIds }: { albumIds: string[] }) => {
if (!albumIds.includes(album.id)) {
return;
}
await refreshAlbum(); await refreshAlbum();
timelineInteraction.clearMultiselect(); timelineInteraction.clearMultiselect();
await setModeToView(); await setModeToView();