merge main
commit
1233277f46
|
|
@ -54,16 +54,10 @@ jobs:
|
|||
issues: write
|
||||
discussions: write
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Close issue
|
||||
if: ${{ github.event_name == 'issues' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.token.outputs.token }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NODE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
gh api graphql \
|
||||
|
|
@ -89,7 +83,7 @@ jobs:
|
|||
- name: Close discussion
|
||||
if: ${{ github.event_name == 'discussion' && github.event.discussion.category.name == 'Feature Request' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.token.outputs.token }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NODE_ID: ${{ github.event.discussion.node_id }}
|
||||
run: |
|
||||
gh api graphql \
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ on:
|
|||
types:
|
||||
- completed
|
||||
|
||||
env:
|
||||
TG_NON_INTERACTIVE: 'true'
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
name: Docs Deploy Checks
|
||||
|
|
@ -182,7 +185,7 @@ jobs:
|
|||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
||||
working-directory: 'deployment/modules/cloudflare/docs'
|
||||
run: 'mise run tf output -json'
|
||||
run: 'mise run tf output -- -json'
|
||||
|
||||
- name: Output Cleaning
|
||||
id: clean
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ on:
|
|||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TG_NON_INTERACTIVE: 'true'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Docs Destroy
|
||||
|
|
@ -36,7 +39,7 @@ jobs:
|
|||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
||||
working-directory: 'deployment/modules/cloudflare/docs'
|
||||
run: 'mise run tf destroy -refresh=false'
|
||||
run: 'mise run tf destroy -- -refresh=false'
|
||||
|
||||
- name: Comment
|
||||
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { serverInfo } from 'src/commands/server-info';
|
|||
import { version } from '../package.json';
|
||||
|
||||
const defaultConfigDirectory = path.join(os.homedir(), '.config/immich/');
|
||||
const defaultConcurrency = Math.max(1, os.cpus().length - 1);
|
||||
|
||||
const program = new Command()
|
||||
.name('immich')
|
||||
|
|
@ -66,7 +67,7 @@ program
|
|||
.addOption(
|
||||
new Option('-c, --concurrency <number>', 'Number of assets to upload at the same time')
|
||||
.env('IMMICH_UPLOAD_CONCURRENCY')
|
||||
.default(4),
|
||||
.default(defaultConcurrency),
|
||||
)
|
||||
.addOption(
|
||||
new Option('-j, --json-output', 'Output detailed information in json format')
|
||||
|
|
|
|||
|
|
@ -582,7 +582,7 @@ describe('/tags', () => {
|
|||
expect(body).toEqual([expect.objectContaining({ id: userAsset.id, success: true })]);
|
||||
});
|
||||
|
||||
it('should remove duplicate assets only once', async () => {
|
||||
it.skip('should remove duplicate assets only once', async () => {
|
||||
const tagA = await create(user.accessToken, { name: 'TagA' });
|
||||
await tagAssets(
|
||||
{ id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } },
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
JobName,
|
||||
LoginResponseDto,
|
||||
createStack,
|
||||
deleteUserAdmin,
|
||||
|
|
@ -327,6 +328,8 @@ describe('/admin/users', () => {
|
|||
{ headers: asBearerAuth(user.accessToken) },
|
||||
);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.BackgroundTask);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/admin/users/${user.userId}`)
|
||||
.send({ force: true })
|
||||
|
|
|
|||
|
|
@ -474,6 +474,7 @@
|
|||
"app_bar_signout_dialog_title": "Sign out",
|
||||
"app_download_links": "App Download Links",
|
||||
"app_settings": "App Settings",
|
||||
"app_stores": "App Stores",
|
||||
"app_update_available": "App update is available",
|
||||
"appears_in": "Appears in",
|
||||
"apply_count": "Apply ({count, number})",
|
||||
|
|
@ -745,6 +746,7 @@
|
|||
"create": "Create",
|
||||
"create_album": "Create album",
|
||||
"create_album_page_untitled": "Untitled",
|
||||
"create_api_key": "Create API key",
|
||||
"create_library": "Create Library",
|
||||
"create_link": "Create link",
|
||||
"create_link_to_share": "Create link to share",
|
||||
|
|
@ -1352,7 +1354,7 @@
|
|||
"minutes": "Minutes",
|
||||
"missing": "Missing",
|
||||
"mobile_app": "Mobile App",
|
||||
"mobile_app_download_onboarding_note": "You can access these options again from the Utilities page.",
|
||||
"mobile_app_download_onboarding_note": "Download the companion mobile app using the following options",
|
||||
"model": "Model",
|
||||
"month": "Month",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
|
|
@ -1435,7 +1437,7 @@
|
|||
"notifications_setting_description": "Manage notifications",
|
||||
"oauth": "OAuth",
|
||||
"obtainium_configurator": "Obtainium Configurator",
|
||||
"obtainium_configurator_instructions": "Please create an API key and select a variant to create your Obtainium configuration link.",
|
||||
"obtainium_configurator_instructions": "Use Obtainium to install and update the Android app directly from Immich GitHub's release. Create an API key and select a variant to create your Obtainium configuration link",
|
||||
"official_immich_resources": "Official Immich Resources",
|
||||
"offline": "Offline",
|
||||
"offset": "Offset",
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
node = "22.20.0"
|
||||
flutter = "3.35.6"
|
||||
pnpm = "10.18.1"
|
||||
terragrunt = "0.58.12"
|
||||
opentofu = "1.7.1"
|
||||
terragrunt = "0.91.2"
|
||||
opentofu = "1.10.6"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"]
|
||||
version = "1.30.0"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ PODS:
|
|||
- FlutterMacOS
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- photo_manager (2.0.0):
|
||||
- photo_manager (3.7.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- SAMKeychain (1.5.3)
|
||||
|
|
@ -268,7 +268,7 @@ SPEC CHECKSUMS:
|
|||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||
geolocator_apple: 1560c3c875af2a412242c7a923e15d0d401966ff
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
|
|
@ -277,9 +277,9 @@ SPEC CHECKSUMS:
|
|||
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
||||
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||
|
|
@ -291,7 +291,7 @@ SPEC CHECKSUMS:
|
|||
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
|
||||
PODFILE CHECKSUM: 7ce312f2beab01395db96f6969d90a447279cf45
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ import 'package:immich_mobile/services/upload.service.dart';
|
|||
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||
import 'package:immich_mobile/wm_executor.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:worker_manager/worker_manager.dart';
|
||||
|
||||
class BackgroundWorkerFgService {
|
||||
final BackgroundWorkerFgHostApi _foregroundHostApi;
|
||||
|
|
@ -94,7 +94,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||
await Future.wait(
|
||||
[
|
||||
loadTranslations(),
|
||||
workerManager.init(dynamicSpawning: true),
|
||||
workerManagerPatch.init(dynamicSpawning: true),
|
||||
_ref?.read(authServiceProvider).setOpenApiServiceEndpoint(),
|
||||
// Initialize the file downloader
|
||||
FileDownloader().configure(
|
||||
|
|
@ -193,7 +193,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||
_logger.info("Cleaning up background worker");
|
||||
final cleanupFutures = [
|
||||
nativeSyncApi?.cancelHashing(),
|
||||
workerManager.dispose().catchError((_) async {
|
||||
workerManagerPatch.dispose().catchError((_) async {
|
||||
// Discard any errors on the dispose call
|
||||
return;
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
|
|
@ -40,10 +41,10 @@ import 'package:immich_mobile/utils/debug_print.dart';
|
|||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||
import 'package:immich_mobile/utils/licenses.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
import 'package:immich_mobile/wm_executor.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:timezone/data/latest.dart';
|
||||
import 'package:worker_manager/worker_manager.dart';
|
||||
|
||||
void main() async {
|
||||
ImmichWidgetsBinding();
|
||||
|
|
@ -52,7 +53,7 @@ void main() async {
|
|||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
await initApp();
|
||||
// Warm-up isolate pool for worker manager
|
||||
await workerManager.init(dynamicSpawning: true);
|
||||
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
|
||||
await migrateDatabaseIfNeeded(isar, drift);
|
||||
HttpSSLOptions.apply();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||
final actions = <Widget>[
|
||||
const ShareActionButton(source: ActionSource.viewer),
|
||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||
if (asset.type == AssetType.image && isOwner) const EditImageActionButton(),
|
||||
if (asset.type == AssetType.image) const EditImageActionButton(),
|
||||
if (isOwner) ...[
|
||||
if (asset.hasRemote && isOwner && isArchived)
|
||||
const UnArchiveActionButton(source: ActionSource.viewer)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
|||
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||
import 'package:immich_mobile/wm_executor.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:worker_manager/worker_manager.dart';
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ Cancelable<T?> runInIsolateGentle<T>({
|
|||
throw const InvalidIsolateUsageException();
|
||||
}
|
||||
|
||||
return workerManager.executeGentle((cancelledChecker) async {
|
||||
return workerManagerPatch.executeGentle((cancelledChecker) async {
|
||||
T? result;
|
||||
await runZonedGuarded(
|
||||
() async {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class SemVer {
|
|||
}
|
||||
|
||||
factory SemVer.fromString(String version) {
|
||||
final parts = version.split('.');
|
||||
final parts = version.split("-")[0].split('.');
|
||||
return SemVer(major: int.parse(parts[0]), minor: int.parse(parts[1]), patch: int.parse(parts[2]));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,251 @@
|
|||
// part of 'package:worker_manager/worker_manager.dart';
|
||||
// ignore_for_file: implementation_imports, avoid_print
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:worker_manager/src/number_of_processors/processors_io.dart';
|
||||
import 'package:worker_manager/src/worker/worker.dart';
|
||||
import 'package:worker_manager/worker_manager.dart';
|
||||
|
||||
final workerManagerPatch = _Executor();
|
||||
|
||||
// [-2^54; 2^53] is compatible with dart2js, see core.int doc
|
||||
const _minId = -9007199254740992;
|
||||
const _maxId = 9007199254740992;
|
||||
|
||||
class Mixinable<T> {
|
||||
late final itSelf = this as T;
|
||||
}
|
||||
|
||||
mixin _ExecutorLogger on Mixinable<_Executor> {
|
||||
var log = false;
|
||||
|
||||
@mustCallSuper
|
||||
void init() {
|
||||
logMessage("${itSelf._isolatesCount} workers have been spawned and initialized");
|
||||
}
|
||||
|
||||
void logTaskAdded<R>(String uid) {
|
||||
logMessage("added task with number $uid");
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
void dispose() {
|
||||
logMessage("worker_manager have been disposed");
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
void _cancel(Task task) {
|
||||
logMessage("Task ${task.id} have been canceled");
|
||||
}
|
||||
|
||||
void logMessage(String message) {
|
||||
if (log) print(message);
|
||||
}
|
||||
}
|
||||
|
||||
class _Executor extends Mixinable<_Executor> with _ExecutorLogger {
|
||||
final _queue = PriorityQueue<Task>();
|
||||
final _pool = <Worker>[];
|
||||
var _nextTaskId = _minId;
|
||||
var _dynamicSpawning = false;
|
||||
var _isolatesCount = numberOfProcessors;
|
||||
|
||||
@override
|
||||
Future<void> init({int? isolatesCount, bool? dynamicSpawning}) async {
|
||||
if (_pool.isNotEmpty) {
|
||||
print("worker_manager already warmed up, init is ignored. Dispose before init");
|
||||
return;
|
||||
}
|
||||
if (isolatesCount != null) {
|
||||
if (isolatesCount < 0) {
|
||||
throw Exception("isolatesCount must be greater than 0");
|
||||
}
|
||||
|
||||
_isolatesCount = isolatesCount;
|
||||
}
|
||||
_dynamicSpawning = dynamicSpawning ?? false;
|
||||
await _ensureWorkersInitialized();
|
||||
super.init();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_queue.clear();
|
||||
for (final worker in _pool) {
|
||||
worker.kill();
|
||||
}
|
||||
_pool.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Cancelable<R> execute<R>(Execute<R> execution, {WorkPriority priority = WorkPriority.immediately}) {
|
||||
return _createCancelable<R>(execution: execution, priority: priority);
|
||||
}
|
||||
|
||||
Cancelable<R> executeNow<R>(ExecuteGentle<R> execution) {
|
||||
final task = TaskGentle<R>(
|
||||
id: "",
|
||||
workPriority: WorkPriority.immediately,
|
||||
execution: execution,
|
||||
completer: Completer<R>(),
|
||||
);
|
||||
|
||||
Future<void> run() async {
|
||||
try {
|
||||
final result = await execution(() => task.canceled);
|
||||
task.complete(result, null, null);
|
||||
} catch (error, st) {
|
||||
task.complete(null, error, st);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
return Cancelable(completer: task.completer, onCancel: () => _cancel(task));
|
||||
}
|
||||
|
||||
Cancelable<R> executeWithPort<R, T>(
|
||||
ExecuteWithPort<R> execution, {
|
||||
WorkPriority priority = WorkPriority.immediately,
|
||||
required void Function(T value) onMessage,
|
||||
}) {
|
||||
return _createCancelable<R>(
|
||||
execution: execution,
|
||||
priority: priority,
|
||||
onMessage: (message) => onMessage(message as T),
|
||||
);
|
||||
}
|
||||
|
||||
Cancelable<R> executeGentle<R>(ExecuteGentle<R> execution, {WorkPriority priority = WorkPriority.immediately}) {
|
||||
return _createCancelable<R>(execution: execution, priority: priority);
|
||||
}
|
||||
|
||||
Cancelable<R> executeGentleWithPort<R, T>(
|
||||
ExecuteGentleWithPort<R> execution, {
|
||||
WorkPriority priority = WorkPriority.immediately,
|
||||
required void Function(T value) onMessage,
|
||||
}) {
|
||||
return _createCancelable<R>(
|
||||
execution: execution,
|
||||
priority: priority,
|
||||
onMessage: (message) => onMessage(message as T),
|
||||
);
|
||||
}
|
||||
|
||||
void _createWorkers() {
|
||||
for (var i = 0; i < _isolatesCount; i++) {
|
||||
_pool.add(Worker());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initializeWorkers() async {
|
||||
await Future.wait(_pool.map((e) => e.initialize()));
|
||||
}
|
||||
|
||||
Cancelable<R> _createCancelable<R>({
|
||||
required Function execution,
|
||||
WorkPriority priority = WorkPriority.immediately,
|
||||
void Function(Object value)? onMessage,
|
||||
}) {
|
||||
if (_nextTaskId + 1 == _maxId) {
|
||||
_nextTaskId = _minId;
|
||||
}
|
||||
final id = _nextTaskId.toString();
|
||||
_nextTaskId++;
|
||||
late final Task<R> task;
|
||||
final completer = Completer<R>();
|
||||
if (execution is Execute<R>) {
|
||||
task = TaskRegular<R>(id: id, workPriority: priority, execution: execution, completer: completer);
|
||||
} else if (execution is ExecuteWithPort<R>) {
|
||||
task = TaskWithPort<R>(
|
||||
id: id,
|
||||
workPriority: priority,
|
||||
execution: execution,
|
||||
completer: completer,
|
||||
onMessage: onMessage!,
|
||||
);
|
||||
} else if (execution is ExecuteGentle<R>) {
|
||||
task = TaskGentle<R>(id: id, workPriority: priority, execution: execution, completer: completer);
|
||||
} else if (execution is ExecuteGentleWithPort<R>) {
|
||||
task = TaskGentleWithPort<R>(
|
||||
id: id,
|
||||
workPriority: priority,
|
||||
execution: execution,
|
||||
completer: completer,
|
||||
onMessage: onMessage!,
|
||||
);
|
||||
}
|
||||
_queue.add(task);
|
||||
_schedule();
|
||||
logTaskAdded(task.id);
|
||||
return Cancelable(completer: task.completer, onCancel: () => _cancel(task));
|
||||
}
|
||||
|
||||
Future<void> _ensureWorkersInitialized() async {
|
||||
if (_pool.isEmpty) {
|
||||
_createWorkers();
|
||||
if (!_dynamicSpawning) {
|
||||
await _initializeWorkers();
|
||||
final poolSize = _pool.length;
|
||||
final queueSize = _queue.length;
|
||||
for (int i = 0; i <= min(poolSize, queueSize); i++) {
|
||||
_schedule();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_pool.every((worker) => worker.taskId != null)) {
|
||||
return;
|
||||
}
|
||||
if (_dynamicSpawning) {
|
||||
final freeWorker = _pool.firstWhereOrNull(
|
||||
(worker) => worker.taskId == null && !worker.initialized && !worker.initializing,
|
||||
);
|
||||
await freeWorker?.initialize();
|
||||
_schedule();
|
||||
}
|
||||
}
|
||||
|
||||
void _schedule() {
|
||||
final availableWorker = _pool.firstWhereOrNull((worker) => worker.taskId == null && worker.initialized);
|
||||
if (availableWorker == null) {
|
||||
_ensureWorkersInitialized();
|
||||
return;
|
||||
}
|
||||
if (_queue.isEmpty) return;
|
||||
final task = _queue.removeFirst();
|
||||
|
||||
availableWorker
|
||||
.work(task)
|
||||
.then(
|
||||
(value) {
|
||||
//could be completed already by cancel and it is normal.
|
||||
//Assuming that worker finished with error and cleaned gracefully
|
||||
task.complete(value, null, null);
|
||||
},
|
||||
onError: (error, st) {
|
||||
task.complete(null, error, st);
|
||||
},
|
||||
)
|
||||
.whenComplete(() {
|
||||
if (_dynamicSpawning && _queue.isEmpty) availableWorker.kill();
|
||||
_schedule();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void _cancel(Task task) {
|
||||
task.cancel();
|
||||
_queue.remove(task);
|
||||
final targetWorker = _pool.firstWhereOrNull((worker) => worker.taskId == task.id);
|
||||
if (task is Gentle) {
|
||||
targetWorker?.cancelGentle();
|
||||
} else {
|
||||
targetWorker?.kill();
|
||||
if (!_dynamicSpawning) targetWorker?.initialize();
|
||||
}
|
||||
super._cancel(task);
|
||||
}
|
||||
}
|
||||
|
|
@ -53,10 +53,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -85,10 +85,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: background_downloader
|
||||
sha256: "9ed74c55750932178f6989ba8a659687c2a102e05b70f561a1b3f047a5dda790"
|
||||
sha256: a22acfa37aa06ba5cfe6eb7b1aa700c78af64770ff450c73dd3d279d7c37d4ac
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.5"
|
||||
version: "9.2.6"
|
||||
bonsoir:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -445,10 +445,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "49413c8ca514dea7633e8def233b25efdf83ec8522955cc2c0e3ad802927e7c6"
|
||||
sha256: dd0e8e02186b2196c7848c9d394a5fd6e5b57a43a546082c5820b1ec72317e33
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.1.0"
|
||||
version: "12.2.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -477,26 +477,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: drift_flutter
|
||||
sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922"
|
||||
sha256: b52bd710f809db11e25259d429d799d034ba1c5224ce6a73fe8419feb980d44c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.4"
|
||||
version: "0.2.6"
|
||||
dynamic_color:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dynamic_color
|
||||
sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d
|
||||
sha256: "43a5a6679649a7731ab860334a5812f2067c2d9ce6452cf069c5e0c25336c17c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
version: "1.8.1"
|
||||
easy_localization:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: easy_localization
|
||||
sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12"
|
||||
sha256: "2ccdf9db8fe4d9c5a75c122e6275674508fd0f0d49c827354967b8afcc56bbed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7+1"
|
||||
version: "3.0.8"
|
||||
easy_logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -594,10 +594,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_displaymode
|
||||
sha256: "42c5e9abd13d28ed74f701b60529d7f8416947e58256e6659c5550db719c57ef"
|
||||
sha256: ecd44b1e902b0073b42ff5b55bf283f38e088270724cdbb7f7065ccf54aa60a8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
version: "0.7.0"
|
||||
flutter_driver:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
@ -607,18 +607,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_hooks
|
||||
sha256: b772e710d16d7a20c0740c4f855095026b31c7eb5ba3ab67d2bd52021cd9461d
|
||||
sha256: "8ae1f090e5f4ef5cfa6670ce1ab5dddadd33f3533a7f9ba19d9f958aa2a89f42"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.21.2"
|
||||
version: "0.21.3+1"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
|
||||
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.3"
|
||||
version: "0.14.4"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
|
@ -660,10 +660,10 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_native_splash
|
||||
sha256: edb09c35ee9230c4b03f13dd45bb3a276d0801865f0a4650b7e2a3bba61a803a
|
||||
sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.5"
|
||||
version: "2.4.7"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -732,10 +732,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
|
||||
sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.17"
|
||||
version: "2.2.1"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
|
@ -745,10 +745,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_udid
|
||||
sha256: be464dc5b1fb7ee894f6a32d65c086ca5e177fdcf9375ac08d77495b98150f84
|
||||
sha256: "166bee5989a58c66b8b62000ea65edccc7c8167bbafdbb08022638db330dd030"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "4.0.0"
|
||||
flutter_web_auth_2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -799,14 +799,22 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
geoclue:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geoclue
|
||||
sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
geolocator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: geolocator
|
||||
sha256: e7ebfa04ce451daf39b5499108c973189a71a919aa53c1204effda1c5b93b822
|
||||
sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.0.0"
|
||||
version: "14.0.2"
|
||||
geolocator_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -823,6 +831,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.9"
|
||||
geolocator_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_linux
|
||||
sha256: c4e966f0a7a87e70049eac7a2617f9e16fd4c585a26e4330bdfc3a71e6a721f3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
geolocator_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -863,14 +879,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
gsettings:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gsettings
|
||||
sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.8"
|
||||
home_widget:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: home_widget
|
||||
sha256: ad9634ef5894f3bac73f04d59e2e5151a39798f49985399fd928dadc828d974a
|
||||
sha256: "908d033514a981f829fd98213909e11a428104327be3b422718aa643ac9d084a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
version: "0.8.1"
|
||||
hooks_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -891,18 +915,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.5"
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.5.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -923,74 +947,74 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "13d3349ace88f12f4a0d175eb5c12dcdd39d35c4c109a8a13dfeb6d0bd9e31c3"
|
||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.3"
|
||||
version: "4.5.4"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
|
||||
sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.2.0"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9"
|
||||
sha256: "58a85e6f09fe9c4484d53d18a0bd6271b72c53fce1d05e6f745ae36d8c18efca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+22"
|
||||
version: "0.8.13+5"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
|
||||
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.1.0"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
|
||||
sha256: e675c22790bcc24e9abd455deead2b7a88de4b79f7327a281812f14de1a56f58
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+2"
|
||||
version: "0.8.13+1"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
|
||||
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
version: "0.2.2"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
|
||||
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+2"
|
||||
version: "0.2.2+1"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
|
||||
sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.1"
|
||||
version: "2.11.0"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_windows
|
||||
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
||||
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
version: "0.2.2"
|
||||
immich_mobile_immich_lint:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
|
@ -1321,10 +1345,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||
sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.3"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1401,34 +1425,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
version: "7.0.1"
|
||||
photo_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: photo_manager
|
||||
sha256: "0bc7548fd3111eb93a3b0abf1c57364e40aeda32512c100085a48dade60e574f"
|
||||
sha256: a0d9a7a9bc35eda02d33766412bde6d883a8b0acb86bbe37dac5f691a0894e8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.4"
|
||||
version: "3.7.1"
|
||||
pigeon:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: pigeon
|
||||
sha256: b65acb352dc5a5f8615d074a83419388cbcc249f07c6d8c78b5bc16680a55dda
|
||||
sha256: "0045b172d1da43c40cb3f58e80e04b50a65cba20b8b70dc880af04181f7758da"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "26.0.0"
|
||||
version: "26.0.2"
|
||||
pinput:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pinput
|
||||
sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a"
|
||||
sha256: c41f42ee301505ae2375ec32871c985d3717bf8aee845620465b286e0140aad2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
version: "5.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1577,18 +1601,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: share_handler
|
||||
sha256: "76575533be04df3fecbebd3c5b5325a8271b5973131f8b8b0ab8490c395a5d37"
|
||||
sha256: "0a6d007f0e44fbee27164adcd159ecbc88238864313f4e5c58161cae2180328d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.22"
|
||||
version: "0.0.25"
|
||||
share_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_handler_android
|
||||
sha256: "124dcc914fb7ecd89076d3dc28435b98fe2129a988bf7742f7a01dcb66a95667"
|
||||
sha256: caf555b933dc72783aa37fef75688c7b86bd6f7bc17d80fbf585bc42f123cc8d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.9"
|
||||
version: "0.0.11"
|
||||
share_handler_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1942,10 +1966,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
version: "6.3.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2030,10 +2054,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
|
||||
sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.16"
|
||||
version: "1.1.19"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2054,18 +2078,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: wakelock_plus
|
||||
sha256: "36c88af0b930121941345306d259ec4cc4ecca3b151c02e3a9e71aede83c615e"
|
||||
sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.10"
|
||||
version: "1.3.3"
|
||||
wakelock_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_plus_platform_interface
|
||||
sha256: "70e780bc99796e1db82fe764b1e7dcb89a86f1e5b3afb1db354de50f2e41eb7a"
|
||||
sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
version: "1.3.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2134,10 +2158,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: worker_manager
|
||||
sha256: "086ed63e9b36266e851404ca90fd44e37c0f4c9bbf819e5f8d7c87f9741c0591"
|
||||
sha256: "1bce9f894a0c187856f5fc0e150e7fe1facce326f048ca6172947754dac3d4f3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.3"
|
||||
version: "7.2.7"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2150,10 +2174,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
version: "6.6.1"
|
||||
xxh3:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2171,5 +2195,5 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.8.0 <4.0.0"
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
flutter: ">=3.35.6"
|
||||
|
|
|
|||
|
|
@ -10,41 +10,41 @@ environment:
|
|||
|
||||
dependencies:
|
||||
app_settings: ^6.1.1
|
||||
async: ^2.11.0
|
||||
async: ^2.13.0
|
||||
auto_route: ^9.2.0
|
||||
background_downloader: ^9.2.5
|
||||
background_downloader: ^9.2.6
|
||||
cached_network_image: ^3.4.1
|
||||
cancellation_token_http: ^2.1.0
|
||||
cast: ^2.1.0
|
||||
collection: ^1.18.0
|
||||
collection: ^1.19.1
|
||||
connectivity_plus: ^6.1.3
|
||||
crop_image: ^1.0.16
|
||||
crypto: ^3.0.6
|
||||
device_info_plus: ^12.0.0
|
||||
device_info_plus: ^12.2.0
|
||||
# DB
|
||||
drift: ^2.23.1
|
||||
drift_flutter: ^0.2.4
|
||||
dynamic_color: ^1.7.0
|
||||
easy_localization: ^3.0.7+1
|
||||
drift: ^2.26.0
|
||||
drift_flutter: ^0.2.6
|
||||
dynamic_color: ^1.8.1
|
||||
easy_localization: ^3.0.8
|
||||
ffi: ^2.1.4
|
||||
file_picker: ^8.0.0+1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_cache_manager: ^3.4.1
|
||||
flutter_displaymode: ^0.6.0
|
||||
flutter_hooks: ^0.21.2
|
||||
flutter_displaymode: ^0.7.0
|
||||
flutter_hooks: ^0.21.3+1
|
||||
flutter_local_notifications: ^17.2.1+2
|
||||
flutter_secure_storage: ^9.2.4
|
||||
flutter_svg: ^2.0.17
|
||||
flutter_udid: ^3.0.0
|
||||
flutter_svg: ^2.2.1
|
||||
flutter_udid: ^4.0.0
|
||||
flutter_web_auth_2: ^5.0.0-alpha.0
|
||||
fluttertoast: ^8.2.12
|
||||
geolocator: ^14.0.0
|
||||
home_widget: ^0.8.0
|
||||
geolocator: ^14.0.2
|
||||
home_widget: ^0.8.1
|
||||
hooks_riverpod: ^2.6.1
|
||||
http: ^1.3.0
|
||||
image_picker: ^1.1.2
|
||||
intl: ^0.20.0
|
||||
http: ^1.5.0
|
||||
image_picker: ^1.2.0
|
||||
intl: ^0.20.2
|
||||
isar:
|
||||
git:
|
||||
url: https://github.com/immich-app/isar
|
||||
|
|
@ -66,37 +66,37 @@ dependencies:
|
|||
package_info_plus: ^8.3.0
|
||||
path: ^1.9.1
|
||||
path_provider: ^2.1.5
|
||||
path_provider_foundation: ^2.4.1
|
||||
path_provider_foundation: ^2.4.3
|
||||
permission_handler: ^11.4.0
|
||||
photo_manager: ^3.6.4
|
||||
pinput: ^5.0.1
|
||||
photo_manager: ^3.7.1
|
||||
pinput: ^5.0.2
|
||||
punycode: ^1.0.0
|
||||
riverpod_annotation: ^2.6.1
|
||||
scroll_date_picker: ^3.8.0
|
||||
scrollable_positioned_list: ^0.3.8
|
||||
share_handler: ^0.0.22
|
||||
share_handler: ^0.0.25
|
||||
share_plus: ^10.1.4
|
||||
sliver_tools: ^0.2.12
|
||||
socket_io_client: ^2.0.3+1
|
||||
stream_transform: ^2.1.1
|
||||
thumbhash: 0.1.0+1
|
||||
timezone: ^0.9.4
|
||||
url_launcher: ^6.3.1
|
||||
url_launcher: ^6.3.2
|
||||
uuid: ^4.5.1
|
||||
wakelock_plus: ^1.2.10
|
||||
worker_manager: ^7.2.3
|
||||
wakelock_plus: ^1.3.0
|
||||
worker_manager: ^7.2.7
|
||||
|
||||
dev_dependencies:
|
||||
auto_route_generator: ^9.0.0
|
||||
build_runner: ^2.4.8
|
||||
custom_lint: ^0.7.5
|
||||
# Drift generator
|
||||
drift_dev: ^2.23.1
|
||||
fake_async: ^1.3.1
|
||||
drift_dev: ^2.26.0
|
||||
fake_async: ^1.3.3
|
||||
file: ^7.0.1 # for MemoryFileSystem
|
||||
flutter_launcher_icons: ^0.14.3
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_native_splash: ^2.4.5
|
||||
flutter_native_splash: ^2.4.7
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
immich_mobile_immich_lint:
|
||||
|
|
@ -110,7 +110,7 @@ dev_dependencies:
|
|||
path: packages/isar_generator/
|
||||
mocktail: ^1.0.4
|
||||
# Type safe platform code
|
||||
pigeon: ^26.0.0
|
||||
pigeon: ^26.0.2
|
||||
riverpod_generator: ^2.6.1
|
||||
riverpod_lint: ^2.6.1
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import { ConfigRepository } from 'src/repositories/config.repository';
|
|||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { services } from 'src/services';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
|
|
@ -56,7 +55,6 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
|||
private jobService: JobService,
|
||||
private telemetryRepository: TelemetryRepository,
|
||||
private authService: AuthService,
|
||||
private userRepository: UserRepository,
|
||||
) {
|
||||
logger.setAppName(this.worker);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
|||
import { NotificationDto } from 'src/dtos/notification.dto';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { SyncAssetExifV1, SyncAssetV1 } from 'src/dtos/sync.dto';
|
||||
import { ImmichWorker, MetadataKey, QueueName } from 'src/enum';
|
||||
import { ImmichWorker, JobStatus, MetadataKey, QueueName, UserAvatarColor, UserStatus } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { JobItem, JobSource } from 'src/types';
|
||||
|
|
@ -66,8 +66,19 @@ type EventMap = {
|
|||
AssetDeleteAll: [{ assetIds: string[]; userId: string }];
|
||||
AssetRestoreAll: [{ assetIds: string[]; userId: string }];
|
||||
|
||||
/** a worker receives a job and emits this event to run it */
|
||||
JobRun: [QueueName, JobItem];
|
||||
/** job pre-hook */
|
||||
JobStart: [QueueName, JobItem];
|
||||
JobFailed: [{ job: JobItem; error: Error | any }];
|
||||
/** job post-hook */
|
||||
JobComplete: [QueueName, JobItem];
|
||||
/** job finishes without error */
|
||||
JobSuccess: [JobSuccessEvent];
|
||||
/** job finishes with error */
|
||||
JobError: [JobErrorEvent];
|
||||
|
||||
// queue events
|
||||
QueueStart: [QueueStartEvent];
|
||||
|
||||
// session events
|
||||
SessionDelete: [{ sessionId: string }];
|
||||
|
|
@ -82,11 +93,43 @@ type EventMap = {
|
|||
|
||||
// user events
|
||||
UserSignup: [{ notify: boolean; id: string; password?: string }];
|
||||
UserCreate: [UserEvent];
|
||||
/** user is soft deleted */
|
||||
UserTrash: [UserEvent];
|
||||
/** user is permanently deleted */
|
||||
UserDelete: [UserEvent];
|
||||
UserRestore: [UserEvent];
|
||||
|
||||
// websocket events
|
||||
WebsocketConnect: [{ userId: string }];
|
||||
};
|
||||
|
||||
type JobSuccessEvent = { job: JobItem; response?: JobStatus };
|
||||
type JobErrorEvent = { job: JobItem; error: Error | any };
|
||||
|
||||
type QueueStartEvent = {
|
||||
name: QueueName;
|
||||
};
|
||||
|
||||
type UserEvent = {
|
||||
name: string;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt: Date | null;
|
||||
status: UserStatus;
|
||||
email: string;
|
||||
profileImagePath: string;
|
||||
isAdmin: boolean;
|
||||
shouldChangePassword: boolean;
|
||||
avatarColor: UserAvatarColor | null;
|
||||
oauthId: string;
|
||||
storageLabel: string | null;
|
||||
quotaSizeInBytes: number | null;
|
||||
quotaUsageInBytes: number;
|
||||
profileChangedAt: Date;
|
||||
};
|
||||
|
||||
export const serverEvents = ['ConfigUpdate'] as const;
|
||||
export type ServerEvents = (typeof serverEvents)[number];
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export class JobRepository {
|
|||
this.logger.debug(`Starting worker for queue: ${queueName}`);
|
||||
this.workers[queueName] = new Worker(
|
||||
queueName,
|
||||
(job) => this.eventRepository.emit('JobStart', queueName, job as JobItem),
|
||||
(job) => this.eventRepository.emit('JobRun', queueName, job as JobItem),
|
||||
{ ...bull.config, concurrency: 1 },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,8 +198,8 @@ export class BaseService {
|
|||
}
|
||||
|
||||
async createUser(dto: Insertable<UserTable> & { email: string }): Promise<UserAdmin> {
|
||||
const user = await this.userRepository.getByEmail(dto.email);
|
||||
if (user) {
|
||||
const exists = await this.userRepository.getByEmail(dto.email);
|
||||
if (exists) {
|
||||
throw new BadRequestException('User exists');
|
||||
}
|
||||
|
||||
|
|
@ -218,7 +218,10 @@ export class BaseService {
|
|||
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
|
||||
}
|
||||
|
||||
this.telemetryRepository.api.addToGauge(`immich.users.total`, 1);
|
||||
return this.userRepository.create(payload);
|
||||
const user = await this.userRepository.create(payload);
|
||||
|
||||
await this.eventRepository.emit('UserCreate', user);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { SyncService } from 'src/services/sync.service';
|
|||
import { SystemConfigService } from 'src/services/system-config.service';
|
||||
import { SystemMetadataService } from 'src/services/system-metadata.service';
|
||||
import { TagService } from 'src/services/tag.service';
|
||||
import { TelemetryService } from 'src/services/telemetry.service';
|
||||
import { TimelineService } from 'src/services/timeline.service';
|
||||
import { TrashService } from 'src/services/trash.service';
|
||||
import { UserAdminService } from 'src/services/user-admin.service';
|
||||
|
|
@ -78,6 +79,7 @@ export const services = [
|
|||
SystemConfigService,
|
||||
SystemMetadataService,
|
||||
TagService,
|
||||
TelemetryService,
|
||||
TimelineService,
|
||||
TrashService,
|
||||
UserAdminService,
|
||||
|
|
|
|||
|
|
@ -222,18 +222,16 @@ describe(JobService.name, () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('onJobStart', () => {
|
||||
describe('onJobRun', () => {
|
||||
it('should process a successful job', async () => {
|
||||
mocks.job.run.mockResolvedValue(JobStatus.Success);
|
||||
|
||||
await sut.onJobStart(QueueName.BackgroundTask, {
|
||||
name: JobName.FileDelete,
|
||||
data: { files: ['path/to/file'] },
|
||||
});
|
||||
const job: JobItem = { name: JobName.FileDelete, data: { files: ['path/to/file'] } };
|
||||
await sut.onJobRun(QueueName.BackgroundTask, job);
|
||||
|
||||
expect(mocks.telemetry.jobs.addToGauge).toHaveBeenCalledWith('immich.queues.background_task.active', 1);
|
||||
expect(mocks.telemetry.jobs.addToGauge).toHaveBeenCalledWith('immich.queues.background_task.active', -1);
|
||||
expect(mocks.telemetry.jobs.addToCounter).toHaveBeenCalledWith('immich.jobs.file_delete.success', 1);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('JobStart', QueueName.BackgroundTask, job);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('JobSuccess', { job, response: JobStatus.Success });
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('JobComplete', QueueName.BackgroundTask, job);
|
||||
expect(mocks.logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -300,7 +298,7 @@ describe(JobService.name, () => {
|
|||
|
||||
mocks.job.run.mockResolvedValue(JobStatus.Success);
|
||||
|
||||
await sut.onJobStart(QueueName.BackgroundTask, item);
|
||||
await sut.onJobRun(QueueName.BackgroundTask, item);
|
||||
|
||||
if (jobs.length > 1) {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith(
|
||||
|
|
@ -317,7 +315,7 @@ describe(JobService.name, () => {
|
|||
it(`should not queue any jobs when ${item.name} fails`, async () => {
|
||||
mocks.job.run.mockResolvedValue(JobStatus.Failed);
|
||||
|
||||
await sut.onJobStart(QueueName.BackgroundTask, item);
|
||||
await sut.onJobRun(QueueName.BackgroundTask, item);
|
||||
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { snakeCase } from 'lodash';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
|
|
@ -186,7 +185,7 @@ export class JobService extends BaseService {
|
|||
throw new BadRequestException(`Job is already running`);
|
||||
}
|
||||
|
||||
this.telemetryRepository.jobs.addToCounter(`immich.queues.${snakeCase(name)}.started`, 1);
|
||||
await this.eventRepository.emit('QueueStart', { name });
|
||||
|
||||
switch (name) {
|
||||
case QueueName.VideoConversion: {
|
||||
|
|
@ -243,21 +242,19 @@ export class JobService extends BaseService {
|
|||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'JobStart' })
|
||||
async onJobStart(...[queueName, job]: ArgsOf<'JobStart'>) {
|
||||
const queueMetric = `immich.queues.${snakeCase(queueName)}.active`;
|
||||
this.telemetryRepository.jobs.addToGauge(queueMetric, 1);
|
||||
@OnEvent({ name: 'JobRun' })
|
||||
async onJobRun(...[queueName, job]: ArgsOf<'JobRun'>) {
|
||||
try {
|
||||
const status = await this.jobRepository.run(job);
|
||||
const jobMetric = `immich.jobs.${snakeCase(job.name)}.${status}`;
|
||||
this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
|
||||
if (status === JobStatus.Success || status == JobStatus.Skipped) {
|
||||
await this.eventRepository.emit('JobStart', queueName, job);
|
||||
const response = await this.jobRepository.run(job);
|
||||
await this.eventRepository.emit('JobSuccess', { job, response });
|
||||
if (response && typeof response === 'string' && [JobStatus.Success, JobStatus.Skipped].includes(response)) {
|
||||
await this.onDone(job);
|
||||
}
|
||||
} catch (error: Error | any) {
|
||||
await this.eventRepository.emit('JobFailed', { job, error });
|
||||
await this.eventRepository.emit('JobError', { job, error });
|
||||
} finally {
|
||||
this.telemetryRepository.jobs.addToGauge(queueMetric, -1);
|
||||
await this.eventRepository.emit('JobComplete', queueName, job);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -424,11 +421,6 @@ export class JobService extends BaseService {
|
|||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case JobName.UserDelete: {
|
||||
this.eventRepository.clientBroadcast('on_user_delete', item.data.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,8 +78,8 @@ export class NotificationService extends BaseService {
|
|||
await this.notificationRepository.cleanup();
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'JobFailed' })
|
||||
async onJobFailed({ job, error }: ArgOf<'JobFailed'>) {
|
||||
@OnEvent({ name: 'JobError' })
|
||||
async onJobError({ job, error }: ArgOf<'JobError'>) {
|
||||
const admin = await this.userRepository.getAdmin();
|
||||
if (!admin) {
|
||||
return;
|
||||
|
|
@ -202,6 +202,11 @@ export class NotificationService extends BaseService {
|
|||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'UserDelete' })
|
||||
onUserDelete({ id }: ArgOf<'UserDelete'>) {
|
||||
this.eventRepository.clientBroadcast('on_user_delete', id);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AlbumUpdate' })
|
||||
async onAlbumUpdate({ id, recipientId }: ArgOf<'AlbumUpdate'>) {
|
||||
await this.jobRepository.removeJob(JobName.NotifyAlbumUpdate, `${id}/${recipientId}`);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { snakeCase } from 'lodash';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { ImmichWorker, JobStatus } from 'src/enum';
|
||||
import { ArgOf, ArgsOf } from 'src/repositories/event.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
|
||||
export class TelemetryService extends BaseService {
|
||||
@OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Api] })
|
||||
async onBootstrap(): Promise<void> {
|
||||
const userCount = await this.userRepository.getCount();
|
||||
this.telemetryRepository.api.addToGauge('immich.users.total', userCount);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'UserCreate' })
|
||||
onUserCreate() {
|
||||
this.telemetryRepository.api.addToGauge(`immich.users.total`, 1);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'UserTrash' })
|
||||
onUserTrash() {
|
||||
this.telemetryRepository.api.addToGauge(`immich.users.total`, -1);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'UserRestore' })
|
||||
onUserRestore() {
|
||||
this.telemetryRepository.api.addToGauge(`immich.users.total`, 1);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'JobStart' })
|
||||
onJobStart(...[queueName]: ArgsOf<'JobStart'>) {
|
||||
const queueMetric = `immich.queues.${snakeCase(queueName)}.active`;
|
||||
this.telemetryRepository.jobs.addToGauge(queueMetric, 1);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'JobSuccess' })
|
||||
onJobSuccess({ job, response }: ArgOf<'JobSuccess'>) {
|
||||
if (response && Object.values(JobStatus).includes(response as JobStatus)) {
|
||||
const jobMetric = `immich.jobs.${snakeCase(job.name)}.${response}`;
|
||||
this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'JobError' })
|
||||
onJobError({ job }: ArgOf<'JobError'>) {
|
||||
const jobMetric = `immich.jobs.${snakeCase(job.name)}.${JobStatus.Failed}`;
|
||||
this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'JobComplete' })
|
||||
onJobComplete(...[queueName]: ArgsOf<'JobComplete'>) {
|
||||
const queueMetric = `immich.queues.${snakeCase(queueName)}.active`;
|
||||
this.telemetryRepository.jobs.addToGauge(queueMetric, -1);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'QueueStart' })
|
||||
onQueueStart({ name }: ArgOf<'QueueStart'>) {
|
||||
this.telemetryRepository.jobs.addToCounter(`immich.queues.${snakeCase(name)}.started`, 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -103,7 +103,8 @@ export class UserAdminService extends BaseService {
|
|||
|
||||
const status = force ? UserStatus.Removing : UserStatus.Deleted;
|
||||
const user = await this.userRepository.update(id, { status, deletedAt: new Date() });
|
||||
this.telemetryRepository.api.addToGauge(`immich.users.total`, -1);
|
||||
|
||||
await this.eventRepository.emit('UserTrash', user);
|
||||
|
||||
if (force) {
|
||||
await this.jobRepository.queue({ name: JobName.UserDelete, data: { id: user.id, force } });
|
||||
|
|
@ -116,7 +117,7 @@ export class UserAdminService extends BaseService {
|
|||
await this.findOrFail(id, { withDeleted: true });
|
||||
await this.albumRepository.restoreAll(id);
|
||||
const user = await this.userRepository.restore(id);
|
||||
this.telemetryRepository.api.addToGauge('immich.users.total', 1);
|
||||
await this.eventRepository.emit('UserRestore', user);
|
||||
return mapUserAdmin(user);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ import { Updateable } from 'kysely';
|
|||
import { DateTime } from 'luxon';
|
||||
import { SALT_ROUNDS } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { OnJob } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||
import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||
import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { CacheControl, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
|
||||
import { CacheControl, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
|
||||
import { UserFindOptions } from 'src/repositories/user.repository';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
|
|
@ -213,12 +213,6 @@ export class UserService extends BaseService {
|
|||
};
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Api] })
|
||||
async onBootstrap(): Promise<void> {
|
||||
const userCount = await this.userRepository.getCount();
|
||||
this.telemetryRepository.api.addToGauge('immich.users.total', userCount);
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.UserSyncUsage, queue: QueueName.BackgroundTask })
|
||||
async handleUserSyncUsage(): Promise<JobStatus> {
|
||||
await this.userRepository.syncUsage();
|
||||
|
|
@ -234,17 +228,17 @@ export class UserService extends BaseService {
|
|||
}
|
||||
|
||||
@OnJob({ name: JobName.UserDelete, queue: QueueName.BackgroundTask })
|
||||
async handleUserDelete({ id, force }: JobOf<JobName.UserDelete>): Promise<JobStatus> {
|
||||
async handleUserDelete({ id, force }: JobOf<JobName.UserDelete>) {
|
||||
const config = await this.getConfig({ withCache: false });
|
||||
const user = await this.userRepository.get(id, { withDeleted: true });
|
||||
if (!user) {
|
||||
return JobStatus.Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
// just for extra protection here
|
||||
if (!force && !this.isReadyForDeletion(user, config.user.deleteDelay)) {
|
||||
this.logger.warn(`Skipped user that was not ready for deletion: id=${id}`);
|
||||
return JobStatus.Skipped;
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Deleting user: ${user.id}`);
|
||||
|
|
@ -266,7 +260,7 @@ export class UserService extends BaseService {
|
|||
await this.albumRepository.deleteAll(user.id);
|
||||
await this.userRepository.delete(user, true);
|
||||
|
||||
return JobStatus.Success;
|
||||
await this.eventRepository.emit('UserDelete', user);
|
||||
}
|
||||
|
||||
private isReadyForDeletion(user: { id: string; deletedAt?: Date | null }, deleteDelay: number): boolean {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ beforeAll(async () => {
|
|||
describe(AuthService.name, () => {
|
||||
describe('adminSignUp', () => {
|
||||
it(`should sign up the admin`, async () => {
|
||||
const { sut } = setup();
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||
const dto = { name: 'Admin', email: 'admin@immich.cloud', password: 'password' };
|
||||
|
||||
await expect(sut.adminSignUp(dto)).resolves.toEqual(
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import { DateTime } from 'luxon';
|
|||
import { ImmichEnvironment, JobName, JobStatus } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { UserService } from 'src/services/user.service';
|
||||
|
|
@ -22,7 +22,7 @@ const setup = (db?: Kysely<DB>) => {
|
|||
return newMediumService(UserService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository],
|
||||
mock: [LoggingRepository, JobRepository, TelemetryRepository],
|
||||
mock: [LoggingRepository, JobRepository, EventRepository],
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -35,7 +35,8 @@ beforeAll(async () => {
|
|||
describe(UserService.name, () => {
|
||||
describe('create', () => {
|
||||
it('should create a user', async () => {
|
||||
const { sut } = setup();
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||
const user = mediumFactory.userInsert();
|
||||
await expect(sut.createUser({ name: user.name, email: user.email })).resolves.toEqual(
|
||||
expect.objectContaining({ name: user.name, email: user.email }),
|
||||
|
|
@ -43,14 +44,16 @@ describe(UserService.name, () => {
|
|||
});
|
||||
|
||||
it('should reject user with duplicate email', async () => {
|
||||
const { sut } = setup();
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||
const user = mediumFactory.userInsert();
|
||||
await expect(sut.createUser({ email: user.email })).resolves.toMatchObject({ email: user.email });
|
||||
await expect(sut.createUser({ email: user.email })).rejects.toThrow('User exists');
|
||||
});
|
||||
|
||||
it('should not return password', async () => {
|
||||
const { sut } = setup();
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||
const dto = mediumFactory.userInsert({ password: 'password' });
|
||||
const user = await sut.createUser({ email: dto.email, password: 'password' });
|
||||
expect((user as any).password).toBeUndefined();
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<AppShellHeader>
|
||||
<NavigationBar showUploadButton={false} noBorder />
|
||||
</AppShellHeader>
|
||||
<AppShellSidebar bind:open={sidebarStore.isOpen}>
|
||||
<AppShellSidebar bind:open={sidebarStore.isOpen} class="border-none shadow-none">
|
||||
<AdminSidebar />
|
||||
</AppShellSidebar>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,22 +6,26 @@
|
|||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<HStack wrap>
|
||||
<p>{$t('mobile_app_download_onboarding_note')}</p>
|
||||
|
||||
<HStack>
|
||||
<Button
|
||||
size="large"
|
||||
size="medium"
|
||||
shape="semi-round"
|
||||
fullWidth
|
||||
onclick={() => modalManager.show(AppDownloadModal, {})}
|
||||
leadingIcon={mdiCellphoneArrowDownVariant}
|
||||
>
|
||||
{$t('app_stores')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="medium"
|
||||
shape="semi-round"
|
||||
fullWidth
|
||||
onclick={() => modalManager.show(ObtainiumConfigModal, {})}
|
||||
leadingIcon={mdiLinkEdit}
|
||||
>
|
||||
{$t('obtainium_configurator')}
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
shape="semi-round"
|
||||
onclick={() => modalManager.show(AppDownloadModal, {})}
|
||||
leadingIcon={mdiCellphoneArrowDownVariant}
|
||||
>
|
||||
{$t('app_download_links')}
|
||||
</Button>
|
||||
</HStack>
|
||||
<p>{$t('mobile_app_download_onboarding_note')}</p>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,11 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<div in:fade={{ duration: 250 }} out:fade={{ duration: 100 }} class="flex flex-col rounded-lg text-xs p-2 gap-1">
|
||||
<div
|
||||
in:fade={{ duration: 250 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
class="flex flex-col rounded-xl text-xs p-2 gap-1 border border-gray-300 dark:border-subtle bg-primary/10"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center justify-center">
|
||||
{#if uploadAsset.state === UploadState.PENDING}
|
||||
|
|
@ -91,12 +95,13 @@
|
|||
</div>
|
||||
|
||||
{#if uploadAsset.state === UploadState.STARTED}
|
||||
<div class="text-black relative mt-[5px] h-[15px] w-full rounded-md bg-gray-300 dark:bg-gray-700">
|
||||
<div class="h-[15px] rounded-md bg-immich-primary transition-all" style={`width: ${uploadAsset.progress}%`}></div>
|
||||
<p class="absolute top-0 h-full w-full text-center text-primary text-[10px]">
|
||||
{#if uploadAsset.message}
|
||||
<div class="text-black relative mt-[5px] h-[18px] w-full rounded-md bg-gray-300 dark:bg-gray-700">
|
||||
<div class="h-[18px] rounded-md bg-immich-primary transition-all" style={`width: ${uploadAsset.progress}%`}></div>
|
||||
<p class="absolute top-0.5 h-full w-full text-center text-white text-[10px]">
|
||||
{#if uploadAsset.message === $t('asset_hashing')}
|
||||
{uploadAsset.message}
|
||||
{:else}
|
||||
{uploadAsset.message}
|
||||
{uploadAsset.progress}% - {getByteUnitString(uploadAsset.speed || 0, $locale)}/s - {uploadAsset.eta}s
|
||||
{/if}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
{#if showDetail}
|
||||
<div
|
||||
in:scale={{ duration: 250, easing: quartInOut }}
|
||||
class="w-[300px] rounded-lg border bg-gray-100 p-4 text-sm shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-white"
|
||||
class="w-[325px] rounded-xl border border-gray-200 dark:border-subtle p-4 text-sm shadow-xs bg-subtle"
|
||||
>
|
||||
<div class="place-item-center mb-4 flex justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { appStoreBadge, fdroidBadge, Modal, ModalBody, playStoreBadge } from '@immich/ui';
|
||||
import { appStoreBadge, fdroidBadge, Modal, ModalBody, playStoreBadge, Text } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
|
|
@ -9,35 +9,29 @@
|
|||
|
||||
<Modal title={$t('app_download_links')} size="large" {onClose}>
|
||||
<ModalBody>
|
||||
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-5 text-immich-primary dark:text-immich-dark-primary">
|
||||
<div>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="fdroid-link">
|
||||
F-Droid
|
||||
</label>
|
||||
<a href="https://f-droid.org/packages/app.alextran.immich/" target="_blank" id="fdroid-link">
|
||||
<img class="pt-2 pr-10" alt="Get it on F-Droid" src={fdroidBadge} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="play-store-link">
|
||||
Google Play
|
||||
</label>
|
||||
<div class="sm:grid sm:grid-cols-2 gap-5">
|
||||
<div class="flex flex-col place-items-start">
|
||||
<Text>Google Play</Text>
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=app.alextran.immich"
|
||||
target="_blank"
|
||||
id="play-store-link"
|
||||
>
|
||||
<img alt="Get it on Google Play" src={playStoreBadge} />
|
||||
<img class="w-[200px] mt-2" alt="Get it on Google Play" src={playStoreBadge} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="app-store-link">
|
||||
App Store
|
||||
</label>
|
||||
<div class="flex flex-col place-items-start">
|
||||
<Text>App Store</Text>
|
||||
<a href="https://apps.apple.com/us/app/immich/id1613945652" target="_blank" id="app-store-link">
|
||||
<img class="pt-2 pr-5" alt="Download on the App Store" src={appStoreBadge} width="90%" />
|
||||
<img class="w-[200px] mt-2" alt="Download on the App Store" src={appStoreBadge} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col place-items-start">
|
||||
<Text>F-Droid</Text>
|
||||
<a href="https://f-droid.org/packages/app.alextran.immich/" target="_blank" id="fdroid-link">
|
||||
<img class="w-[200px] mt-2" alt="Get it on F-Droid" src={fdroidBadge} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { ConfirmModal, Input } from '@immich/ui';
|
||||
import { ConfirmModal, Field, Textarea } from '@immich/ui';
|
||||
import { mdiText } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
onClose: (description?: string) => void;
|
||||
}
|
||||
};
|
||||
|
||||
let { onClose }: Props = $props();
|
||||
|
||||
let description = $state('');
|
||||
</script>
|
||||
|
||||
|
|
@ -20,11 +19,8 @@
|
|||
onClose={(confirmed) => (confirmed ? onClose(description) : onClose())}
|
||||
>
|
||||
{#snippet promptSnippet()}
|
||||
<div class="flex flex-col text-start gap-2">
|
||||
<div class="flex flex-col">
|
||||
<label for="description">{$t('description')}</label>
|
||||
<Input class="immich-form-input" id="description" bind:value={description} />
|
||||
</div>
|
||||
</div>
|
||||
<Field label={$t('description')}>
|
||||
<Textarea bind:value={description} grow />
|
||||
</Field>
|
||||
{/snippet}
|
||||
</ConfirmModal>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { createApiKey, Permission } from '@immich/sdk';
|
||||
import { Button, Modal, ModalBody, obtainiumBadge } from '@immich/ui';
|
||||
import { Button, Modal, ModalBody, obtainiumBadge, Text } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
let inputUrl = $state(location.origin);
|
||||
let inputApiKey = $state('');
|
||||
|
|
@ -31,64 +31,53 @@
|
|||
let { onClose }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Modal title={$t('obtainium_configurator')} size="large" {onClose}>
|
||||
<Modal title={$t('obtainium_configurator')} size="medium" {onClose}>
|
||||
<ModalBody>
|
||||
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-5 text-immich-primary dark:text-immich-dark-primary">
|
||||
<div>
|
||||
<label
|
||||
class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm"
|
||||
for="obtainium-configurator"
|
||||
>
|
||||
Obtainium
|
||||
</label>
|
||||
<div id="obtainium-configurator">
|
||||
<form>
|
||||
<div class="mt-2">
|
||||
<SettingInputField inputType={SettingInputFieldType.TEXT} label={$t('url')} bind:value={inputUrl} />
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('api_key')}
|
||||
bind:value={inputApiKey}
|
||||
/>
|
||||
</div>
|
||||
<div class="">
|
||||
<Button shape="round" size="small" onclick={() => handleCreate()}>{$t('new_api_key')}</Button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<SettingSelect
|
||||
label={$t('app_architecture_variant')}
|
||||
bind:value={archVariant}
|
||||
options={[
|
||||
{ value: 'arm64-v8a-release', text: 'arm64-v8a' },
|
||||
{ value: 'armeabi-v7a-release', text: 'armeabi-v7a' },
|
||||
{ value: 'release', text: 'universal' },
|
||||
{ value: 'x86_64-release', text: 'x86_64' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div>
|
||||
<Text color="muted" size="small">
|
||||
{$t('obtainium_configurator_instructions')}
|
||||
</Text>
|
||||
<form class="mt-4">
|
||||
<div class="mt-2">
|
||||
<SettingInputField inputType={SettingInputFieldType.TEXT} label={$t('url')} bind:value={inputUrl} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-center">
|
||||
{#if inputUrl && inputApiKey && archVariant}
|
||||
<a
|
||||
href={obtainiumLink}
|
||||
class="underline text-sm immich-form-label"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
id="obtainium-link"
|
||||
>
|
||||
<img class="pt-2 pr-5" alt="Get it on Obtainium" src={obtainiumBadge} />
|
||||
</a>
|
||||
{:else}
|
||||
<p class="immich-form-label pb-2 text-sm" id="obtainium-link">
|
||||
{$t('obtainium_configurator_instructions')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-2 flex gap-2 place-items-center place-content-center">
|
||||
<SettingInputField inputType={SettingInputFieldType.TEXT} label={$t('api_key')} bind:value={inputApiKey} />
|
||||
|
||||
<div class="translate-y-[3px]">
|
||||
<Button size="small" onclick={() => handleCreate()}>{$t('create_api_key')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('app_architecture_variant')}
|
||||
bind:value={archVariant}
|
||||
options={[
|
||||
{ value: 'arm64-v8a-release', text: 'arm64-v8a' },
|
||||
{ value: 'armeabi-v7a-release', text: 'armeabi-v7a' },
|
||||
{ value: 'release', text: 'universal' },
|
||||
{ value: 'x86_64-release', text: 'x86_64' },
|
||||
]}
|
||||
/>
|
||||
</form>
|
||||
|
||||
{#if inputUrl && inputApiKey && archVariant}
|
||||
<div class="content-center">
|
||||
<hr />
|
||||
<div class="flex place-items-center place-content-center">
|
||||
<a
|
||||
href={obtainiumLink}
|
||||
class="underline text-sm immich-form-label"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
id="obtainium-link"
|
||||
>
|
||||
<img class="pt-2 pr-5 h-[80px]" alt="Get it on Obtainium" src={obtainiumBadge} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -3,57 +3,51 @@
|
|||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { deleteUserAdmin, type UserAdminResponseDto, type UserResponseDto } from '@immich/sdk';
|
||||
import { Checkbox, ConfirmModal, Label } from '@immich/ui';
|
||||
import { Alert, Checkbox, ConfirmModal, Field, Input, Label, Text } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
user: UserResponseDto;
|
||||
onClose: (user?: UserAdminResponseDto) => void;
|
||||
}
|
||||
};
|
||||
|
||||
let { user, onClose }: Props = $props();
|
||||
|
||||
let forceDelete = $state(false);
|
||||
let deleteButtonDisabled = $state(false);
|
||||
let userIdInput: string = '';
|
||||
let force = $state(false);
|
||||
let email = $state('');
|
||||
let disabled = $derived(force && email !== user.email);
|
||||
|
||||
const handleClose = async (confirmed: boolean) => {
|
||||
if (!confirmed) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
const handleDeleteUser = async () => {
|
||||
try {
|
||||
const result = await deleteUserAdmin({
|
||||
id: user.id,
|
||||
userAdminDeleteDto: { force: forceDelete },
|
||||
});
|
||||
|
||||
const result = await deleteUserAdmin({ id: user.id, userAdminDeleteDto: { force } });
|
||||
onClose(result);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_delete_user'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = (e: Event) => {
|
||||
userIdInput = (e.target as HTMLInputElement).value;
|
||||
deleteButtonDisabled = userIdInput != user.email;
|
||||
};
|
||||
</script>
|
||||
|
||||
<ConfirmModal
|
||||
title={$t('delete_user')}
|
||||
confirmText={forceDelete ? $t('permanently_delete') : $t('delete')}
|
||||
onClose={(confirmed) => (confirmed ? handleDeleteUser() : onClose())}
|
||||
disabled={deleteButtonDisabled}
|
||||
confirmText={force ? $t('permanently_delete') : $t('delete')}
|
||||
onClose={handleClose}
|
||||
{disabled}
|
||||
>
|
||||
{#snippet promptSnippet()}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if forceDelete}
|
||||
<p>
|
||||
<Text>
|
||||
{#if force}
|
||||
<FormatMessage key="admin.user_delete_immediately" values={{ user: user.name }}>
|
||||
{#snippet children({ message })}
|
||||
<b>{message}</b>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</p>
|
||||
{:else}
|
||||
<p>
|
||||
{:else}
|
||||
<FormatMessage
|
||||
key="admin.user_delete_delay"
|
||||
values={{ user: user.name, delay: $serverConfig.userDeleteDelay }}
|
||||
|
|
@ -62,34 +56,20 @@
|
|||
<b>{message}</b>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</Text>
|
||||
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<Checkbox
|
||||
id="queue-user-deletion-checkbox"
|
||||
color="secondary"
|
||||
bind:checked={forceDelete}
|
||||
onCheckedChange={() => (deleteButtonDisabled = forceDelete)}
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox id="queue-user-deletion-checkbox" color="secondary" bind:checked={force} />
|
||||
<Label label={$t('admin.user_delete_immediately_checkbox')} for="queue-user-deletion-checkbox" />
|
||||
</div>
|
||||
|
||||
{#if forceDelete}
|
||||
<p class="text-danger">{$t('admin.force_delete_user_warning')}</p>
|
||||
{#if force}
|
||||
<Alert color="danger" icon={false}>{$t('admin.force_delete_user_warning')}</Alert>
|
||||
|
||||
<p class="immich-form-label text-sm" id="confirm-user-desc">
|
||||
{$t('admin.confirm_email_below', { values: { email: user.email } })}
|
||||
</p>
|
||||
|
||||
<input
|
||||
class="immich-form-input w-full pb-2"
|
||||
id="confirm-user-id"
|
||||
aria-describedby="confirm-user-desc"
|
||||
name="confirm-user-id"
|
||||
type="text"
|
||||
oninput={handleConfirm}
|
||||
/>
|
||||
<Field label={$t('admin.confirm_email_below', { values: { email: user.email } })}>
|
||||
<Input bind:value={email} />
|
||||
</Field>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@
|
|||
{#each userSessions as session (session.id)}
|
||||
<DeviceCard {session} />
|
||||
{:else}
|
||||
<span class="text-subtle">No mobile devices</span>
|
||||
<span class="text-dark">No mobile devices</span>
|
||||
{/each}
|
||||
</Stack>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@
|
|||
component: OnboardingMobileApp,
|
||||
role: OnboardingRole.USER,
|
||||
title: $t('mobile_app'),
|
||||
icon: mdiCellphoneArrowDownVariant, // or you can use mdiCellphone
|
||||
icon: mdiCellphoneArrowDownVariant,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
style="width: {(onboardingProgress / onboardingStepCount) * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
<div class="py-8 flex place-content-center place-items-center m-auto">
|
||||
<div class="py-8 flex place-content-center place-items-center m-auto w-[min(100%,_800px)]">
|
||||
<OnboardingCard
|
||||
title={onboardingSteps[index].title}
|
||||
icon={onboardingSteps[index].icon}
|
||||
|
|
|
|||
Loading…
Reference in New Issue