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