mirror of
https://github.com/immich-app/immich.git
synced 2026-07-22 13:54:45 +03:00
Compare commits
18 Commits
chore/pub-
...
fix/librar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65365f3556 | ||
|
|
3f6897ef80 | ||
|
|
89d0a9d59f | ||
|
|
45a6ea84af | ||
|
|
bf64f3867b | ||
|
|
4a4d468aa2 | ||
|
|
522def1ed6 | ||
|
|
3adc3920fb | ||
|
|
00cb50cc67 | ||
|
|
f2b0b696f6 | ||
|
|
b5401eb120 | ||
|
|
12fc8bac18 | ||
|
|
f2c00c107d | ||
|
|
f19f30ec66 | ||
|
|
297316c7b8 | ||
|
|
f18065b3c2 | ||
|
|
a3dd19cd2f | ||
|
|
41a2fa8a44 |
@@ -7,7 +7,7 @@ sidebar_position: 85
|
||||
:::note
|
||||
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
|
||||
|
||||
Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/).
|
||||
Community support should be directed to Synology-specific support platforms.
|
||||
:::
|
||||
|
||||
Immich can easily be installed on a Synology NAS using Container Manager within DSM. If you have not installed Container Manager already, you can install it in the Packages Center. Refer to the [Container Manager docs](https://kb.synology.com/en-us/DSM/help/ContainerManager/docker_desc?version=7) for more information on using Container Manager.
|
||||
|
||||
@@ -121,7 +121,7 @@ alt="Go to Docker Tab and visit the address listed next to immich-web"
|
||||
width="90%"
|
||||
alt="Go to Docker Tab and visit the address listed next to immich-web"
|
||||
/>
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
:::tip
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools.wrangler]]
|
||||
version = "4.100.0"
|
||||
version = "4.110.0"
|
||||
backend = "npm:wrangler"
|
||||
|
||||
[tools.wrangler.options]
|
||||
|
||||
@@ -28,4 +28,4 @@ run = "prettier --write ."
|
||||
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
|
||||
|
||||
[tools]
|
||||
wrangler = "4.100.0"
|
||||
wrangler = "4.110.0"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.13.3",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^7.0.0",
|
||||
|
||||
38
mise.lock
38
mise.lock
@@ -287,43 +287,43 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536406"
|
||||
|
||||
[[tools.pnpm]]
|
||||
version = "11.6.0"
|
||||
version = "11.11.0"
|
||||
backend = "aqua:pnpm/pnpm"
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64"]
|
||||
checksum = "sha256:2fec653ff6dadab340d1c3d2214688a7451cc471f39710839440b293ca7c53b0"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120174"
|
||||
checksum = "sha256:4871093439f036b3082df6102255ac119f3b6f4f50c51c5711fc7c358b8b22d7"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734816"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:56a78c08cf22adf29e7dacb6f7f100139731693863d20fb94a7883463a62169e"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120177"
|
||||
checksum = "sha256:f97316cee0deb3ae3fd64e8a09e1c8c88a76da33373a2b465bd6a4c3e85e0690"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-arm64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734819"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64"]
|
||||
checksum = "sha256:74d64c1646385fb21691f32f0ab6aca1a9f5c829ba54d3cda3a24838a228e68c"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120176"
|
||||
checksum = "sha256:df4699e897012ab14df2cc6eaa942910e830eb7fcaa420a2a1421a9461fd9108"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734818"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:7a0c463a09d912fba6b7d9eca0a946bc228ea50f3015a05c09a29e7e85a932d7"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120175"
|
||||
checksum = "sha256:aad186f8a8ae72d827d5efe63e99e801456715fb7f2798949405fa33f20260db"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-linux-x64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734821"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.macos-arm64"]
|
||||
checksum = "sha256:87c901635a14481fb30566a3749041134ffd4317bc6fe866c345b69fdf9b6b85"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120180"
|
||||
checksum = "sha256:ad46ad16c2376c2b78354ac488c1b166e076aa59bfcbcfd567d55957d755a690"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734814"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.windows-x64"]
|
||||
checksum = "sha256:91c753435542b04859c689304fae0dd64eba6b40937cfa426a48485b712e4e9c"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120173"
|
||||
checksum = "sha256:13ad8a9b139c4c829fea33462cf04d061d72f0926b2b76a92ac03c9a36e405f9"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.11.0/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/471734820"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[[tools.terragrunt]]
|
||||
|
||||
@@ -16,7 +16,7 @@ config_roots = [
|
||||
|
||||
[tools]
|
||||
node = "24.15.0"
|
||||
pnpm = "11.6.0"
|
||||
pnpm = "11.11.0"
|
||||
terragrunt = "1.0.3"
|
||||
opentofu = "1.11.6"
|
||||
"npm:oazapfts" = "7.5.0"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -586,7 +586,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
|
||||
shellScript = "/bin/bash \"$SRCROOT/scripts/xcode_flutter_build.sh\"\n";
|
||||
};
|
||||
BAEA01ACA3F5C9CD3D732370 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
|
||||
"state" : {
|
||||
"revision" : "84a79bc375a301169390ac110c868f06c857b83f",
|
||||
"version" : "6.27.0"
|
||||
"revision" : "60d9bb85c94ce6e7fc4406cd32529fd12bdb7809",
|
||||
"version" : "6.14.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
|
||||
"state" : {
|
||||
"revision" : "84a79bc375a301169390ac110c868f06c857b83f",
|
||||
"version" : "6.27.0"
|
||||
"revision" : "60d9bb85c94ce6e7fc4406cd32529fd12bdb7809",
|
||||
"version" : "6.14.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
45
mobile/ios/scripts/xcode_flutter_build.sh
Executable file
45
mobile/ios/scripts/xcode_flutter_build.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# Makes Flutter's builds through the Xcode GUI properly display errors and warnings
|
||||
# in the Issue navigator
|
||||
#
|
||||
# Flutter's `xcode_backend.dart` runs `flutter assemble` with `allowFail: true`,
|
||||
# which intentionally does not prefix output with `error:`. Unsure why they do this,
|
||||
# but this script rebuilds the expected output so Xcode can parse and display the errors
|
||||
|
||||
set -o pipefail
|
||||
|
||||
# The Immich mobile root (containing the Dart `lib` directory). This is used to make
|
||||
# absolute paths for Xcode linking
|
||||
app_root="${FLUTTER_APPLICATION_PATH:-$SRCROOT/..}"
|
||||
|
||||
/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build 2>&1 \
|
||||
| awk -v app_root="$app_root" '
|
||||
# Match Dart CFE diagnostics: <path>.dart:<line>:<col>: <Kind>: <message>
|
||||
# Written for macOS/POSIX/BSD awk
|
||||
{
|
||||
# Always pass the original line through to preserve the original build log
|
||||
print
|
||||
|
||||
if ($0 ~ /^.*\.dart:[0-9]+:[0-9]+: (Error|Warning|Context|Info):/) {
|
||||
# Locate the ": Kind:" separator to split location from message.
|
||||
rest = $0
|
||||
if (match(rest, /: Error:/)) { kind = "Error"; keyword = "error" }
|
||||
else if (match(rest, /: Warning:/)) { kind = "Warning"; keyword = "warning" }
|
||||
else if (match(rest, /: Context:/)) { kind = "Context"; keyword = "note" }
|
||||
else if (match(rest, /: Info:/)) { kind = "Info"; keyword = "note" }
|
||||
|
||||
# location = everything before ": Kind:" (e.g. "lib/foo.dart:12:5")
|
||||
location = substr(rest, 1, RSTART - 1)
|
||||
# message = everything after ": Kind:" (leading space preserved)
|
||||
message = substr(rest, RSTART + length(": " kind ":"))
|
||||
|
||||
# Make the path absolute so Xcode links to it
|
||||
if (location !~ /^\//)
|
||||
location = app_root "/" location
|
||||
|
||||
printf "%s: %s:%s\n", location, keyword, message
|
||||
}
|
||||
}
|
||||
'
|
||||
|
||||
exit "${PIPESTATUS[0]}"
|
||||
@@ -70,6 +70,10 @@ class RemoteAsset extends BaseAsset {
|
||||
|
||||
bool get isTrashed => deletedAt != null;
|
||||
|
||||
bool get isStacked => stackId != null;
|
||||
|
||||
bool get isArchived => visibility == .archive;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Asset {
|
||||
|
||||
@@ -85,8 +85,12 @@ class TimelineFactory {
|
||||
TimelineService fromAssetsWithBuckets(List<BaseAsset> assets, TimelineOrigin type) =>
|
||||
TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type));
|
||||
|
||||
TimelineService map(List<String> userIds, TimelineMapOptions options) =>
|
||||
TimelineService(_timelineRepository.map(userIds, options, groupBy));
|
||||
/// Creates a TimelineService for serving geographical map queries, such assets within bounded locations
|
||||
TimelineService geographicMap(
|
||||
List<String> userIds,
|
||||
TimelineMapOptions Function() currentOptions,
|
||||
Stream<TimelineMapOptions> optionsStream,
|
||||
) => TimelineService(_timelineRepository.geographicMap(userIds, currentOptions, optionsStream, groupBy));
|
||||
}
|
||||
|
||||
class TimelineService {
|
||||
|
||||
@@ -508,9 +508,21 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
return query.map((row) => row.toDto()).get();
|
||||
}
|
||||
|
||||
TimelineQuery map(List<String> userIds, TimelineMapOptions options, GroupAssetsBy groupBy) => (
|
||||
bucketSource: () => _watchMapBucket(userIds, options, groupBy: groupBy),
|
||||
assetSource: (offset, count) => _getMapBucketAssets(userIds, options, offset: offset, count: count),
|
||||
/// Creates a geographic map query that can dynamically filter on changing [TimelineMapOptions]
|
||||
/// (most notably the active map bounds)
|
||||
TimelineQuery geographicMap(
|
||||
List<String> userIds,
|
||||
TimelineMapOptions Function() currentOptions,
|
||||
Stream<TimelineMapOptions> optionsStream,
|
||||
GroupAssetsBy groupBy,
|
||||
) => (
|
||||
bucketSource: () => Stream.value(currentOptions())
|
||||
.followedBy(optionsStream)
|
||||
.switchMap(
|
||||
// Any error would kill the stream for all options; make sure the stream stays alive
|
||||
(options) => _watchMapBucket(userIds, options, groupBy: groupBy).handleError((_) {}),
|
||||
),
|
||||
assetSource: (offset, count) => _getMapBucketAssets(userIds, currentOptions(), offset: offset, count: count),
|
||||
origin: TimelineOrigin.map,
|
||||
);
|
||||
|
||||
|
||||
@@ -3,25 +3,23 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/utils/asset_filter.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
class FavoriteAction extends AssetAction<RemoteAsset> {
|
||||
final bool shouldFavorite;
|
||||
final bool favorite;
|
||||
|
||||
FavoriteAction({required super.assets}) : shouldFavorite = assets.any((asset) => !asset.isFavorite);
|
||||
FavoriteAction({required super.assets}) : favorite = assets.any((asset) => !asset.isFavorite);
|
||||
|
||||
@override
|
||||
IconData get icon => shouldFavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded;
|
||||
IconData get icon => favorite ? Icons.favorite_border_rounded : Icons.favorite_rounded;
|
||||
|
||||
@override
|
||||
String label(ActionScope scope) => shouldFavorite ? scope.context.t.favorite : scope.context.t.unfavorite;
|
||||
String label(ActionScope scope) => favorite ? scope.context.t.favorite : scope.context.t.unfavorite;
|
||||
|
||||
@override
|
||||
Iterable<RemoteAsset> filter(ActionScope scope) => assets
|
||||
.where(
|
||||
(asset) => asset is RemoteAsset && asset.ownerId == scope.authUser.id && asset.isFavorite == !shouldFavorite,
|
||||
)
|
||||
.cast<RemoteAsset>();
|
||||
Iterable<RemoteAsset> filter(ActionScope scope) =>
|
||||
AssetFilter(assets).owned(scope.authUser.id).favorite(isFavorite: !favorite);
|
||||
|
||||
@override
|
||||
bool isVisible(ActionScope scope) => filter(scope).isNotEmpty;
|
||||
@@ -31,8 +29,8 @@ class FavoriteAction extends AssetAction<RemoteAsset> {
|
||||
final ActionScope(:ref) = scope;
|
||||
final assets = filter(scope).map((asset) => asset.id).toList(growable: false);
|
||||
|
||||
await ref.read(assetServiceProvider).updateFavorite(assets, shouldFavorite);
|
||||
final message = shouldFavorite
|
||||
await ref.read(assetServiceProvider).updateFavorite(assets, favorite);
|
||||
final message = favorite
|
||||
? StaticTranslations.instance.favorite_action_prompt(count: assets.length)
|
||||
: StaticTranslations.instance.unfavorite_action_prompt(count: assets.length);
|
||||
snackbar.success(message);
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
@@ -129,12 +128,6 @@ class ShareActionButton extends ConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(settingsProvider).write(SettingsKey.shareFileType, fileType);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _share(context, ref, fileType);
|
||||
}
|
||||
|
||||
|
||||
@@ -745,10 +745,15 @@ class AddToAlbumHeader extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Future<void> onCreateAlbum() async {
|
||||
final albumName = await showDialog<String?>(context: context, builder: (context) => const NewAlbumNameModal());
|
||||
if (albumName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final selectedAssets = ref.read(multiSelectProvider).selectedAssets;
|
||||
final newAlbum = await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.createAlbumWithAssets(title: "Untitled Album", assets: selectedAssets);
|
||||
.createAlbumWithAssets(title: albumName, assets: selectedAssets);
|
||||
|
||||
if (newAlbum == null) {
|
||||
ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr());
|
||||
|
||||
@@ -91,7 +91,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
}
|
||||
case AppLifecycleState.paused:
|
||||
_shouldPlayOnForeground = await _controller?.isPlaying() ?? true;
|
||||
if (_shouldPlayOnForeground) {
|
||||
if (_shouldPlayOnForeground && mounted) {
|
||||
await _notifier.pause();
|
||||
}
|
||||
default:
|
||||
@@ -268,10 +268,13 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
return;
|
||||
}
|
||||
|
||||
await _notifier.load(source);
|
||||
// Grab refs to prevent reading after dispose
|
||||
final loopVideo = ref.read(appConfigProvider).viewer.loopVideo;
|
||||
await _notifier.setLoop(!widget.asset.isMotionPhoto && loopVideo);
|
||||
await _notifier.setVolume(1);
|
||||
final localNotifier = _notifier;
|
||||
|
||||
await localNotifier.load(source);
|
||||
await localNotifier.setLoop(!widget.asset.isMotionPhoto && loopVideo);
|
||||
await localNotifier.setVolume(1);
|
||||
}
|
||||
|
||||
void _initController(NativeVideoPlayerController nc) {
|
||||
|
||||
@@ -63,7 +63,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
|
||||
ActionIconButtonWidget(action: FavoriteAction(assets: assetForAction)),
|
||||
|
||||
ViewerKebabMenu(originalTheme: originalTheme),
|
||||
ImmichColorOverride(color: null, child: ViewerKebabMenu(originalTheme: originalTheme)),
|
||||
];
|
||||
|
||||
final lockedViewActions = <Widget>[ViewerKebabMenu(originalTheme: originalTheme)];
|
||||
|
||||
@@ -35,7 +35,6 @@ class _ScopedMapTimeline extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// TODO: this causes the timeline to switch to flicker to "loading" state and back. This is both janky and inefficient.
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
timelineServiceProvider.overrideWith((ref) {
|
||||
@@ -44,13 +43,16 @@ class _ScopedMapTimeline extends StatelessWidget {
|
||||
throw Exception('User must be logged in to access archive');
|
||||
}
|
||||
|
||||
final users = ref.watch(mapStateProvider).withPartners
|
||||
? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id]
|
||||
: [user.id];
|
||||
final withPartners = ref.watch(mapStateProvider.select((s) => s.withPartners));
|
||||
final users = withPartners ? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id] : [user.id];
|
||||
|
||||
final timelineService = ref
|
||||
.watch(timelineFactoryProvider)
|
||||
.map(users, ref.watch(mapStateProvider).toOptions());
|
||||
.geographicMap(
|
||||
users,
|
||||
() => ref.read(mapStateProvider).toOptions(),
|
||||
ref.read(mapStateProvider.notifier).optionsStream,
|
||||
);
|
||||
ref.onDispose(timelineService.dispose);
|
||||
return timelineService;
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
@@ -63,11 +65,16 @@ class MapState {
|
||||
class MapStateNotifier extends Notifier<MapState> {
|
||||
MapStateNotifier();
|
||||
|
||||
final StreamController<TimelineMapOptions> _optionsController = StreamController.broadcast();
|
||||
|
||||
Stream<TimelineMapOptions> get optionsStream => _optionsController.stream;
|
||||
|
||||
bool setBounds(LatLngBounds bounds) {
|
||||
if (state.bounds == bounds) {
|
||||
return false;
|
||||
}
|
||||
state = state.copyWith(bounds: bounds);
|
||||
_optionsController.add(state.toOptions());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -82,12 +89,14 @@ class MapStateNotifier extends Notifier<MapState> {
|
||||
void switchFavoriteOnly(bool isFavoriteOnly) {
|
||||
ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly);
|
||||
state = state.copyWith(onlyFavorites: isFavoriteOnly);
|
||||
_optionsController.add(state.toOptions());
|
||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||
}
|
||||
|
||||
void switchIncludeArchived(bool isIncludeArchived) {
|
||||
ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived);
|
||||
state = state.copyWith(includeArchived: isIncludeArchived);
|
||||
_optionsController.add(state.toOptions());
|
||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||
}
|
||||
|
||||
@@ -100,11 +109,13 @@ class MapStateNotifier extends Notifier<MapState> {
|
||||
void setRelativeTime(int relativeDays) {
|
||||
ref.read(settingsProvider).write(.mapRelativeDate, relativeDays);
|
||||
state = state.copyWith(relativeDays: relativeDays);
|
||||
_optionsController.add(state.toOptions());
|
||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||
}
|
||||
|
||||
@override
|
||||
MapState build() {
|
||||
ref.onDispose(_optionsController.close);
|
||||
final mapConfig = ref.read(appConfigProvider.select((config) => config.map));
|
||||
return MapState(
|
||||
themeMode: mapConfig.themeMode,
|
||||
|
||||
22
mobile/lib/utils/asset_filter.dart
Normal file
22
mobile/lib/utils/asset_filter.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
|
||||
extension type const AssetFilter<T extends BaseAsset>(Iterable<T> assets) implements Iterable<T> {
|
||||
AssetFilter<T> where(bool Function(T asset) test) => AssetFilter(assets.where(test));
|
||||
AssetFilter<T> whereNot(bool Function(T asset) test) => AssetFilter(assets.where((asset) => !test(asset)));
|
||||
|
||||
AssetFilter<T> type(AssetType type) => where((asset) => asset.type == type);
|
||||
AssetFilter<T> favorite({bool isFavorite = true}) => where((asset) => asset.isFavorite == isFavorite);
|
||||
|
||||
AssetFilter<RemoteAsset> remote() => AssetFilter(assets.whereType<RemoteAsset>());
|
||||
AssetFilter<RemoteAsset> owned(String ownerId) => remote().where((asset) => asset.ownerId == ownerId);
|
||||
AssetFilter<RemoteAsset> visibility(AssetVisibility visibility) =>
|
||||
remote().where((asset) => asset.visibility == visibility);
|
||||
AssetFilter<RemoteAsset> notVisibility(AssetVisibility visibility) =>
|
||||
remote().where((asset) => asset.visibility != visibility);
|
||||
AssetFilter<RemoteAsset> archived({bool isArchived = true}) =>
|
||||
remote().where((asset) => asset.isArchived == isArchived);
|
||||
AssetFilter<RemoteAsset> stacked({bool isStacked = true}) => remote().where((asset) => asset.isStacked == isStacked);
|
||||
|
||||
AssetFilter<LocalAsset> local() => AssetFilter(assets.whereType<LocalAsset>());
|
||||
AssetFilter<LocalAsset> backedUp() => local().where((asset) => asset.remoteAssetId != null);
|
||||
}
|
||||
@@ -11,17 +11,20 @@ import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/feature_message.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
|
||||
const int targetVersion = 26;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
||||
final int version = Store.get(StoreKey.version, targetVersion);
|
||||
final int? storedVersion = Store.tryGet(StoreKey.version);
|
||||
final version = storedVersion ?? targetVersion;
|
||||
|
||||
if (version < 25) {
|
||||
await _migrateTo25();
|
||||
@@ -31,6 +34,10 @@ Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
||||
await _migrateTo26(drift);
|
||||
}
|
||||
|
||||
if (storedVersion == null) {
|
||||
await FeatureMessageService(SettingsRepository.instance).markSeen();
|
||||
}
|
||||
|
||||
await Store.put(StoreKey.version, targetVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
7.22.0
|
||||
7.23.0
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -4,7 +4,7 @@ Immich API
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 3.0.3
|
||||
- Generator version: 7.22.0
|
||||
- Generator version: 7.23.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
|
||||
4
mobile/openapi/lib/api_client.dart
generated
4
mobile/openapi/lib/api_client.dart
generated
@@ -97,9 +97,9 @@ class ApiClient {
|
||||
if (nullableHeaderParams != null) {
|
||||
request.headers.addAll(nullableHeaderParams);
|
||||
}
|
||||
if (msgBody is String) {
|
||||
if (msgBody is String && msgBody.isNotEmpty) {
|
||||
request.body = msgBody;
|
||||
} else if (msgBody is List<int>) {
|
||||
} else if (msgBody is List<int> && msgBody.isNotEmpty) {
|
||||
request.bodyBytes = msgBody;
|
||||
} else if (msgBody is Map<String, String>) {
|
||||
request.bodyFields = msgBody;
|
||||
|
||||
@@ -35,16 +35,16 @@ class TimeBucketAssetResponseDto {
|
||||
});
|
||||
|
||||
/// Array of city names extracted from EXIF GPS data
|
||||
Optional<List<String>?> city;
|
||||
Optional<List<String?>?> city;
|
||||
|
||||
/// Array of country names extracted from EXIF GPS data
|
||||
Optional<List<String>?> country;
|
||||
Optional<List<String?>?> country;
|
||||
|
||||
/// Array of UTC timestamps when each asset was originally uploaded to Immich
|
||||
List<String> createdAt;
|
||||
|
||||
/// Array of video/gif durations in milliseconds (null for static images)
|
||||
List<int> duration;
|
||||
List<int?> duration;
|
||||
|
||||
/// Array of file creation timestamps in UTC
|
||||
List<String> fileCreatedAt;
|
||||
@@ -62,22 +62,22 @@ class TimeBucketAssetResponseDto {
|
||||
List<bool> isTrashed;
|
||||
|
||||
/// Array of latitude coordinates extracted from EXIF GPS data
|
||||
Optional<List<num>?> latitude;
|
||||
Optional<List<num?>?> latitude;
|
||||
|
||||
/// Array of live photo video asset IDs (null for non-live photos)
|
||||
List<String> livePhotoVideoId;
|
||||
List<String?> livePhotoVideoId;
|
||||
|
||||
/// Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.
|
||||
List<num> localOffsetHours;
|
||||
|
||||
/// Array of longitude coordinates extracted from EXIF GPS data
|
||||
Optional<List<num>?> longitude;
|
||||
Optional<List<num?>?> longitude;
|
||||
|
||||
/// Array of owner IDs for each asset
|
||||
List<String> ownerId;
|
||||
|
||||
/// Array of projection types for 360° content (e.g., \"EQUIRECTANGULAR\", \"CUBEFACE\", \"CYLINDRICAL\")
|
||||
List<String> projectionType;
|
||||
List<String?> projectionType;
|
||||
|
||||
/// Array of aspect ratios (width/height) for each asset
|
||||
List<num> ratio;
|
||||
@@ -86,7 +86,7 @@ class TimeBucketAssetResponseDto {
|
||||
Optional<List<List<String>?>?> stack;
|
||||
|
||||
/// Array of BlurHash strings for generating asset previews (base64 encoded)
|
||||
List<String> thumbhash;
|
||||
List<String?> thumbhash;
|
||||
|
||||
/// Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)
|
||||
List<AssetVisibility> visibility;
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
|
||||
class ImmichColorOverride extends InheritedWidget {
|
||||
const ImmichColorOverride({super.key, required this.color, required super.child});
|
||||
|
||||
final Color color;
|
||||
final Color? color;
|
||||
|
||||
static Color? maybeOf(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<ImmichColorOverride>()?.color;
|
||||
|
||||
@@ -69,10 +69,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: background_downloader
|
||||
sha256: d27fcaf94f738032f1e6db098230799bf31ae7bf184f173fc9a752fcdae75c0e
|
||||
sha256: aceacec2b2a72ec3a8862ab5895fcbbc71ab33765f3619d57963f3110dd268e3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.5.6"
|
||||
version: "9.5.5"
|
||||
bonsoir:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
@@ -133,10 +133,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae"
|
||||
sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
version: "4.0.6"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -157,10 +157,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16"
|
||||
sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.15.1"
|
||||
version: "2.15.0"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -253,10 +253,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "25cebb79dfe304022550e0c3c893ae051ded07183d2607f7b88473cb3ade33cb"
|
||||
sha256: "62ffa266d9a23b79fb3fcbc206afc00bb979417ba57b1324c546b5aab95ba057"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
version: "7.1.1"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -634,10 +634,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_udid
|
||||
sha256: f23555e9d34d5a7dc70fdfa3ab8915b401786879d278568b3c0e801af9a14d2e
|
||||
sha256: fc599671cbe8b328e509c961ec121880406ed994dde659cc9ece9c7503cd31c7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.6"
|
||||
version: "4.1.2"
|
||||
flutter_web_auth_2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -852,10 +852,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667
|
||||
sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
version: "1.2.2"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1234,10 +1234,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1640,18 +1640,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "61c7930bebf32c552ac5808c91bf33802e66711c9216090d3b5579c9f862e80c"
|
||||
sha256: "9488c7d2cdb1091c91cacf7e207cff81b28bff8e366f042bad3afe7d34afe189"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
version: "3.3.2"
|
||||
sqlite3_connection_pool:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_connection_pool
|
||||
sha256: ad51972d37bdf82929888e4c85e722bd9a0b5ff8ad389f89b1b00d19603c0d2d
|
||||
sha256: "9d2b3b398b03c96743fd071521fc665be73c33c9cd5c56d87196baff8d8b4398"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.7"
|
||||
version: "0.2.6"
|
||||
sqlite3_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1848,10 +1848,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
|
||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
version: "4.5.3"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -11,10 +11,10 @@ environment:
|
||||
dependencies:
|
||||
async: ^2.13.1
|
||||
auto_route: ^11.1.0
|
||||
background_downloader: ^9.5.6
|
||||
background_downloader: ^9.5.4
|
||||
cast: ^2.1.0
|
||||
collection: ^1.19.1
|
||||
connectivity_plus: ^7.3.0
|
||||
connectivity_plus: ^7.0.0
|
||||
crop_image: ^1.0.17
|
||||
crypto: ^3.0.7
|
||||
device_info_plus: ^12.4.0
|
||||
@@ -31,14 +31,14 @@ dependencies:
|
||||
flutter_local_notifications: ^17.2.4
|
||||
flutter_secure_storage: ^9.2.4
|
||||
flutter_svg: ^2.2.4
|
||||
flutter_udid: ^4.1.6
|
||||
flutter_udid: ^4.1.2
|
||||
flutter_web_auth_2: ^5.0.2
|
||||
fluttertoast: ^8.2.14
|
||||
geolocator: ^14.0.2
|
||||
home_widget: ^0.9.3
|
||||
hooks_riverpod: ^2.6.1
|
||||
http: ^1.6.0
|
||||
image_picker: ^1.2.3
|
||||
image_picker: ^1.2.1
|
||||
immich_ui:
|
||||
path: './packages/ui'
|
||||
intl: ^0.20.2
|
||||
@@ -55,7 +55,7 @@ dependencies:
|
||||
path: openapi
|
||||
package_info_plus: ^8.3.1
|
||||
path: ^1.9.1
|
||||
path_provider: ^2.1.6
|
||||
path_provider: ^2.1.5
|
||||
path_provider_foundation: ^2.6.0
|
||||
permission_handler: ^11.4.0
|
||||
photo_manager: ^3.9.0
|
||||
@@ -67,13 +67,13 @@ dependencies:
|
||||
share_plus: ^10.1.4
|
||||
sliver_tools: ^0.2.12
|
||||
stream_transform: ^2.1.1
|
||||
sqlite3: ^3.4.0
|
||||
sqlite3: ^3.3.2
|
||||
sqlite_async: 0.14.2
|
||||
sqlite3_connection_pool: ^0.2.7
|
||||
sqlite3_connection_pool: ^0.2.6
|
||||
thumbhash: 0.1.0+1
|
||||
timezone: ^0.9.4
|
||||
url_launcher: ^6.3.2
|
||||
uuid: ^4.6.0
|
||||
uuid: ^4.5.3
|
||||
wakelock_plus: ^1.3.3
|
||||
worker_manager: ^7.2.9
|
||||
web_socket: ^1.0.1
|
||||
@@ -94,7 +94,7 @@ dependencies:
|
||||
|
||||
dev_dependencies:
|
||||
auto_route_generator: ^10.5.0
|
||||
build_runner: ^2.15.1
|
||||
build_runner: ^2.13.1
|
||||
# Drift generator
|
||||
drift_dev: ^2.34.0
|
||||
fake_async: ^1.3.3
|
||||
|
||||
@@ -5,7 +5,14 @@ import '../../utils.dart';
|
||||
class RemoteAssetFactory {
|
||||
const RemoteAssetFactory();
|
||||
|
||||
static RemoteAsset create({String? id, String? name, String? ownerId, bool isFavorite = false}) {
|
||||
static RemoteAsset create({
|
||||
String? id,
|
||||
String? name,
|
||||
String? ownerId,
|
||||
bool isFavorite = false,
|
||||
AssetVisibility visibility = AssetVisibility.timeline,
|
||||
String? stackId,
|
||||
}) {
|
||||
id = TestUtils.uuid(id);
|
||||
|
||||
return RemoteAsset(
|
||||
@@ -17,6 +24,8 @@ class RemoteAssetFactory {
|
||||
createdAt: TestUtils.yesterday(),
|
||||
updatedAt: TestUtils.now(),
|
||||
isFavorite: isFavorite,
|
||||
visibility: visibility,
|
||||
stackId: stackId,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
|
||||
import '../../factories/remote_asset_factory.dart';
|
||||
import '../presentation_context.dart';
|
||||
|
||||
class _RecordingActionNotifier extends ActionNotifier {
|
||||
final List<ShareAssetType> sharedFileTypes = [];
|
||||
|
||||
@override
|
||||
void build() {}
|
||||
|
||||
@override
|
||||
Future<ActionResult> shareAssets(
|
||||
ActionSource source,
|
||||
BuildContext context, {
|
||||
ShareAssetType fileType = ShareAssetType.original,
|
||||
Completer<void>? cancelCompleter,
|
||||
void Function(double progress)? onAssetDownloadProgress,
|
||||
}) async {
|
||||
sharedFileTypes.add(fileType);
|
||||
return const ActionResult(count: 1, success: true);
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeAssetViewerNotifier extends AssetViewerStateNotifier {
|
||||
final BaseAsset asset;
|
||||
|
||||
_FakeAssetViewerNotifier(this.asset);
|
||||
|
||||
@override
|
||||
AssetViewerState build() => AssetViewerState(currentAsset: asset);
|
||||
}
|
||||
|
||||
void main() {
|
||||
late PresentationContext context;
|
||||
late _RecordingActionNotifier actionNotifier;
|
||||
|
||||
setUpAll(() async {
|
||||
final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
await SettingsRepository.ensureInitialized(db);
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
context = await PresentationContext.create();
|
||||
actionNotifier = _RecordingActionNotifier();
|
||||
await SettingsRepository.instance.clear([SettingsKey.shareFileType]);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
context.dispose();
|
||||
});
|
||||
|
||||
Future<void> pumpShareButton(WidgetTester tester) async {
|
||||
final asset = RemoteAssetFactory.create(ownerId: context.currentUser.id);
|
||||
await tester.pumpTestWidget(
|
||||
context,
|
||||
const ShareActionButton(source: ActionSource.viewer),
|
||||
overrides: [
|
||||
actionProvider.overrideWith(() => actionNotifier),
|
||||
assetViewerProvider.overrideWith(() => _FakeAssetViewerNotifier(asset)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> longPressAndPickPreview(WidgetTester tester) async {
|
||||
await tester.longPress(find.byType(BaseActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byIcon(Icons.photo_size_select_large_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
group('ShareActionButton', () {
|
||||
testWidgets('single press shares with the configured default quality', (tester) async {
|
||||
await pumpShareButton(tester);
|
||||
|
||||
await tester.tap(find.byType(BaseActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(actionNotifier.sharedFileTypes, [ShareAssetType.original]);
|
||||
});
|
||||
|
||||
testWidgets('long press shares with the quality picked in the dialog', (tester) async {
|
||||
await pumpShareButton(tester);
|
||||
|
||||
await longPressAndPickPreview(tester);
|
||||
|
||||
expect(actionNotifier.sharedFileTypes, [ShareAssetType.preview]);
|
||||
});
|
||||
|
||||
testWidgets('quality picked on long press is a one-time choice and does not change the default', (tester) async {
|
||||
await pumpShareButton(tester);
|
||||
|
||||
await longPressAndPickPreview(tester);
|
||||
expect(actionNotifier.sharedFileTypes, [ShareAssetType.preview]);
|
||||
|
||||
await tester.tap(find.byType(BaseActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(actionNotifier.sharedFileTypes, [ShareAssetType.preview, ShareAssetType.original]);
|
||||
expect(SettingsRepository.instance.appConfig.share.fileType, ShareAssetType.original);
|
||||
});
|
||||
});
|
||||
}
|
||||
200
mobile/test/unit/utils/asset_filter_test.dart
Normal file
200
mobile/test/unit/utils/asset_filter_test.dart
Normal file
@@ -0,0 +1,200 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/utils/asset_filter.dart';
|
||||
|
||||
import '../factories/local_asset_factory.dart';
|
||||
import '../factories/remote_asset_factory.dart';
|
||||
|
||||
void main() {
|
||||
group('AssetFilter', () {
|
||||
group('type promotion', () {
|
||||
test('a bare filter retains every BaseAsset', () {
|
||||
final remoteAsset = RemoteAssetFactory.create();
|
||||
final localAsset = LocalAssetFactory.create();
|
||||
|
||||
final AssetFilter<BaseAsset> filter = AssetFilter(<BaseAsset>[remoteAsset, localAsset]);
|
||||
|
||||
expect(filter.toList(), [remoteAsset, localAsset]);
|
||||
});
|
||||
|
||||
test('remote keeps only remote assets', () {
|
||||
final remoteAsset = RemoteAssetFactory.create();
|
||||
final localAsset = LocalAssetFactory.create();
|
||||
|
||||
final AssetFilter<RemoteAsset> remoteOnly = AssetFilter(<BaseAsset>[remoteAsset, localAsset]).remote();
|
||||
|
||||
expect(remoteOnly.toList(), [remoteAsset]);
|
||||
});
|
||||
|
||||
test('local keeps only local assets', () {
|
||||
final remoteAsset = RemoteAssetFactory.create();
|
||||
final localAsset = LocalAssetFactory.create();
|
||||
|
||||
final AssetFilter<LocalAsset> localOnly = AssetFilter(<BaseAsset>[remoteAsset, localAsset]).local();
|
||||
|
||||
expect(localOnly.toList(), [localAsset]);
|
||||
});
|
||||
|
||||
test('owned promotes to RemoteAsset and drops local assets', () {
|
||||
final remoteAsset = RemoteAssetFactory.create();
|
||||
final localAsset = LocalAssetFactory.create();
|
||||
|
||||
final AssetFilter<RemoteAsset> remoteOnly = AssetFilter(<BaseAsset>[
|
||||
remoteAsset,
|
||||
localAsset,
|
||||
]).owned(remoteAsset.ownerId);
|
||||
|
||||
expect(remoteOnly.toList(), [remoteAsset]);
|
||||
});
|
||||
|
||||
test('backedUp promotes to LocalAsset and drops remote assets', () {
|
||||
final syncedPhoto = LocalAssetFactory.create().copyWith(remoteId: 'remote');
|
||||
final offlinePhoto = LocalAssetFactory.create();
|
||||
final remotePhoto = RemoteAssetFactory.create();
|
||||
|
||||
final AssetFilter<LocalAsset> syncedPhotos = AssetFilter(<BaseAsset>[
|
||||
syncedPhoto,
|
||||
offlinePhoto,
|
||||
remotePhoto,
|
||||
]).backedUp();
|
||||
|
||||
expect(syncedPhotos.toList(), [syncedPhoto]);
|
||||
});
|
||||
});
|
||||
|
||||
group('named filters', () {
|
||||
test('owned keeps only assets of the given owner', () {
|
||||
final asset1 = RemoteAssetFactory.create();
|
||||
final asset2 = RemoteAssetFactory.create();
|
||||
|
||||
final alexPhotos = AssetFilter([asset1, asset2]).owned(asset1.ownerId);
|
||||
|
||||
expect(alexPhotos.toList(), [asset1]);
|
||||
});
|
||||
|
||||
test('favorites keeps only favorite assets', () {
|
||||
final asset1 = RemoteAssetFactory.create(isFavorite: true);
|
||||
final asset2 = RemoteAssetFactory.create(ownerId: asset1.ownerId);
|
||||
|
||||
final favorites = AssetFilter([asset1, asset2]).favorite();
|
||||
|
||||
expect(favorites.toList(), [asset1]);
|
||||
});
|
||||
|
||||
test('type keeps only assets of the given type', () {
|
||||
final image = RemoteAssetFactory.create();
|
||||
final video = RemoteAssetFactory.create(ownerId: image.ownerId).copyWith(type: .video);
|
||||
|
||||
final videos = AssetFilter([image, video]).type(.video);
|
||||
|
||||
expect(videos.toList(), [video]);
|
||||
});
|
||||
|
||||
test('visibility keeps only assets with the given visibility', () {
|
||||
final locked = RemoteAssetFactory.create(visibility: AssetVisibility.locked);
|
||||
final onTimeline = RemoteAssetFactory.create(ownerId: locked.ownerId);
|
||||
|
||||
final lockedPhotos = AssetFilter([locked, onTimeline]).visibility(.locked);
|
||||
|
||||
expect(lockedPhotos.toList(), [locked]);
|
||||
});
|
||||
|
||||
test('archived keeps only archived assets', () {
|
||||
final archived = RemoteAssetFactory.create(visibility: AssetVisibility.archive);
|
||||
final onTimeline = RemoteAssetFactory.create(ownerId: archived.ownerId);
|
||||
|
||||
final archivedPhotos = AssetFilter([archived, onTimeline]).archived();
|
||||
|
||||
expect(archivedPhotos.toList(), [archived]);
|
||||
});
|
||||
|
||||
test('stacked keeps only assets belonging to a stack', () {
|
||||
final stacked = RemoteAssetFactory.create(stackId: 'stack');
|
||||
final loose = RemoteAssetFactory.create(ownerId: stacked.ownerId);
|
||||
|
||||
final stackedPhotos = AssetFilter([stacked, loose]).stacked();
|
||||
|
||||
expect(stackedPhotos.toList(), [stacked]);
|
||||
});
|
||||
});
|
||||
|
||||
group('inversion', () {
|
||||
test('notArchived keeps every non-archived visibility', () {
|
||||
final archived = RemoteAssetFactory.create(visibility: AssetVisibility.archive);
|
||||
final onTimeline = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.timeline);
|
||||
final hidden = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.hidden);
|
||||
final locked = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.locked);
|
||||
|
||||
final visiblePhotos = AssetFilter([archived, onTimeline, hidden, locked]).archived(isArchived: false);
|
||||
|
||||
expect(visiblePhotos.toSet(), {onTimeline, hidden, locked});
|
||||
});
|
||||
|
||||
test('notVisibility keeps every asset not at the target visibility', () {
|
||||
final archived = RemoteAssetFactory.create(visibility: AssetVisibility.archive);
|
||||
final onTimeline = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.timeline);
|
||||
final locked = RemoteAssetFactory.create(ownerId: archived.ownerId, visibility: AssetVisibility.locked);
|
||||
|
||||
final toArchive = AssetFilter([archived, onTimeline, locked]).notVisibility(.archive);
|
||||
|
||||
expect(toArchive.toSet(), {onTimeline, locked});
|
||||
});
|
||||
|
||||
test('notStacked keeps only assets without a stack', () {
|
||||
final stacked = RemoteAssetFactory.create(stackId: 'stack');
|
||||
final loose = RemoteAssetFactory.create(ownerId: stacked.ownerId);
|
||||
|
||||
final loosePhotos = AssetFilter([stacked, loose]).stacked(isStacked: false);
|
||||
|
||||
expect(loosePhotos.toList(), [loose]);
|
||||
});
|
||||
|
||||
test('whereNot inverts an arbitrary predicate', () {
|
||||
final favorite = RemoteAssetFactory.create(isFavorite: true);
|
||||
final regular = RemoteAssetFactory.create(ownerId: favorite.ownerId);
|
||||
|
||||
final nonFavorites = AssetFilter([favorite, regular]).whereNot((asset) => asset.isFavorite);
|
||||
|
||||
expect(nonFavorites.toList(), [regular]);
|
||||
});
|
||||
|
||||
test('notFavorites keeps only non-favorite assets', () {
|
||||
final favorite = RemoteAssetFactory.create(isFavorite: true);
|
||||
final regular = RemoteAssetFactory.create(ownerId: favorite.ownerId);
|
||||
|
||||
final nonFavorites = AssetFilter([favorite, regular]).favorite(isFavorite: false);
|
||||
|
||||
expect(nonFavorites.toList(), [regular]);
|
||||
});
|
||||
});
|
||||
|
||||
group('chaining', () {
|
||||
test('combines predicates across owner, visibility and stack', () {
|
||||
final asset = RemoteAssetFactory.create();
|
||||
final wrongOwner = RemoteAssetFactory.create();
|
||||
final archived = RemoteAssetFactory.create(ownerId: asset.ownerId, visibility: AssetVisibility.archive);
|
||||
final stacked = RemoteAssetFactory.create(ownerId: asset.ownerId, stackId: 'stack-1');
|
||||
final localPhoto = LocalAssetFactory.create();
|
||||
|
||||
final result = AssetFilter(<BaseAsset>[
|
||||
asset,
|
||||
wrongOwner,
|
||||
archived,
|
||||
stacked,
|
||||
localPhoto,
|
||||
]).owned(asset.ownerId).archived(isArchived: false).stacked(isStacked: false);
|
||||
|
||||
expect(result.toList(), [asset]);
|
||||
});
|
||||
|
||||
test('a base filter after a promotion retains the promoted type', () {
|
||||
final favorite = RemoteAssetFactory.create(isFavorite: true);
|
||||
final regular = RemoteAssetFactory.create(ownerId: favorite.ownerId);
|
||||
|
||||
final AssetFilter<RemoteAsset> result = AssetFilter([favorite, regular]).owned(favorite.ownerId).favorite();
|
||||
|
||||
expect(result.toList(), [favorite]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
OPENAPI_GENERATOR_VERSION=v7.22.0
|
||||
OPENAPI_GENERATOR_VERSION=v7.23.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -23,7 +23,6 @@ patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./patch/
|
||||
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api.dart <./patch/api.dart.patch
|
||||
patch --no-backup-if-mismatch -u ../mobile/openapi/pubspec.yaml <./patch/pubspec_immich_mobile.yaml.patch
|
||||
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/model/asset_edit_action_item_dto.dart <./patch/asset_edit_action_item_dto.dart.patch
|
||||
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/model/time_bucket_asset_response_dto.dart <./patch/time_bucket_asset_response_dto.dart.patch
|
||||
# Don't include analysis_options.yaml for the generated openapi files
|
||||
# so that language servers can properly exclude the mobile/openapi directory
|
||||
rm ../mobile/openapi/analysis_options.yaml
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||
"spaces": 2,
|
||||
"generator-cli": {
|
||||
"version": "7.22.0"
|
||||
"version": "7.23.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +1,30 @@
|
||||
@@ -13,7 +13,7 @@
|
||||
class ApiClient {
|
||||
ApiClient({this.basePath = '/api', this.authentication,});
|
||||
|
||||
|
||||
- final String basePath;
|
||||
+ String basePath;
|
||||
final Authentication? authentication;
|
||||
|
||||
|
||||
var _client = Client();
|
||||
@@ -44,8 +44,9 @@
|
||||
Object? body,
|
||||
Map<String, String> headerParams,
|
||||
Map<String, String> formParams,
|
||||
- String? contentType,
|
||||
- ) async {
|
||||
+ String? contentType, {
|
||||
+ Future<void>? abortTrigger,
|
||||
+ }) async {
|
||||
await authentication?.applyToParams(queryParams, headerParams);
|
||||
|
||||
headerParams.addAll(_defaultHeaderMap);
|
||||
@@ -63,7 +64,7 @@
|
||||
body is MultipartFile && (contentType == null ||
|
||||
!contentType.toLowerCase().startsWith('multipart/form-data'))
|
||||
) {
|
||||
- final request = StreamedRequest(method, uri);
|
||||
+ final request = AbortableStreamedRequest(method, uri, abortTrigger: abortTrigger);
|
||||
request.headers.addAll(headerParams);
|
||||
request.contentLength = body.length;
|
||||
body.finalize().listen(
|
||||
@@ -78,7 +79,7 @@
|
||||
}
|
||||
|
||||
if (body is MultipartRequest) {
|
||||
- final request = MultipartRequest(method, uri);
|
||||
+ final request = AbortableMultipartRequest(method, uri, abortTrigger: abortTrigger);
|
||||
request.fields.addAll(body.fields);
|
||||
request.files.addAll(body.files);
|
||||
request.headers.addAll(body.headers);
|
||||
@@ -92,14 +93,19 @@
|
||||
: await serializeAsync(body);
|
||||
final nullableHeaderParams = headerParams.isEmpty ? null : headerParams;
|
||||
|
||||
- switch(method) {
|
||||
- case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,);
|
||||
- case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,);
|
||||
- case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,);
|
||||
- case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,);
|
||||
- case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,);
|
||||
- case 'GET': return await _client.get(uri, headers: nullableHeaderParams,);
|
||||
+ final request = AbortableRequest(method, uri, abortTrigger: abortTrigger);
|
||||
+ if (nullableHeaderParams != null) {
|
||||
+ request.headers.addAll(nullableHeaderParams);
|
||||
}
|
||||
+ if (msgBody is String) {
|
||||
+ request.body = msgBody;
|
||||
+ } else if (msgBody is List<int>) {
|
||||
+ request.bodyBytes = msgBody;
|
||||
+ } else if (msgBody is Map<String, String>) {
|
||||
+ request.bodyFields = msgBody;
|
||||
+ }
|
||||
+ final response = await _client.send(request);
|
||||
+ return Response.fromStream(response);
|
||||
} on SocketException catch (error, trace) {
|
||||
throw ApiException.withInner(
|
||||
HttpStatus.badRequest,
|
||||
@@ -136,26 +146,21 @@
|
||||
trace,
|
||||
);
|
||||
}
|
||||
-
|
||||
- throw ApiException(
|
||||
- HttpStatus.badRequest,
|
||||
- 'Invalid HTTP operation: $method $path',
|
||||
- );
|
||||
@@ -143,19 +143,19 @@
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
- Future<dynamic> deserializeAsync(String value, String targetType, {bool growable = false,}) async =>
|
||||
+ Future<dynamic> deserializeAsync(String value, String targetType, {bool growable = false,}) =>
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
deserialize(value, targetType, growable: growable);
|
||||
|
||||
|
||||
@Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.')
|
||||
- dynamic deserialize(String value, String targetType, {bool growable = false,}) {
|
||||
+ Future<dynamic> deserialize(String value, String targetType, {bool growable = false,}) async {
|
||||
// Remove all spaces. Necessary for regular expressions as well.
|
||||
targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments
|
||||
|
||||
|
||||
// If the expected target type is String, nothing to do...
|
||||
return targetType == 'String'
|
||||
? value
|
||||
- : fromJson(json.decode(value), targetType, growable: growable);
|
||||
+ : fromJson(await compute((String j) => json.decode(j), value), targetType, growable: growable);
|
||||
}
|
||||
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
@@ -83,7 +83,7 @@
|
||||
List<num> ratio;
|
||||
|
||||
/// Array of stack information as [stackId, assetCount] tuples (null for non-stacked assets)
|
||||
- Optional<List<List<String>>?> stack;
|
||||
+ Optional<List<List<String>?>?> stack;
|
||||
|
||||
/// Array of BlurHash strings for generating asset previews (base64 encoded)
|
||||
List<String> thumbhash;
|
||||
@@ -1,11 +1,8 @@
|
||||
--- api.mustache
|
||||
+++ api.mustache.modified
|
||||
@@ -49,9 +49,9 @@
|
||||
///
|
||||
{{/-last}}
|
||||
@@ -51,7 +51,7 @@
|
||||
{{/allParams}}
|
||||
- Future<Response> {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async {
|
||||
+ Future<Response> {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future<void>? abortTrigger, }) async {
|
||||
Future<Response> {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async {
|
||||
// ignore: prefer_const_declarations
|
||||
- final path = r'{{{path}}}'{{#pathParams}}
|
||||
+ final apiPath = r'{{{path}}}'{{#pathParams}}
|
||||
@@ -21,7 +18,7 @@
|
||||
{{#formParams}}
|
||||
{{^isFile}}
|
||||
if ({{{paramName}}} != null) {
|
||||
@@ -121,13 +121,14 @@
|
||||
@@ -121,7 +121,7 @@
|
||||
{{/isMultipart}}
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
@@ -30,21 +27,3 @@
|
||||
'{{{httpMethod}}}',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
+ abortTrigger: abortTrigger,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,8 +162,8 @@
|
||||
///
|
||||
{{/-last}}
|
||||
{{/allParams}}
|
||||
- Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async {
|
||||
- final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}} {{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} {{/hasOptionalParams}});
|
||||
+ Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future<void>? abortTrigger, }) async {
|
||||
+ final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}}, {{/required}}{{/allParams}}{{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}}, {{/required}}{{/allParams}}abortTrigger: abortTrigger,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
@@ -26,155 +26,3 @@
|
||||
return {{{classname}}}(
|
||||
{{#vars}}
|
||||
{{#isDateTime}}
|
||||
@@ -195,48 +181,98 @@
|
||||
{{#complexType}}
|
||||
{{#isArray}}
|
||||
{{#items.isArray}}
|
||||
+ {{#vendorExtensions.x-is-optional}}
|
||||
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] is List
|
||||
+ ? (json[r'{{{baseName}}}'] as List).map((e) =>
|
||||
+ {{#items.complexType}}
|
||||
+ e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.complexType}}>[]{{/items.isNullable}} : {{items.complexType}}.listFromJson(e){{#uniqueItems}}.toSet(){{/uniqueItems}}
|
||||
+ {{/items.complexType}}
|
||||
+ {{^items.complexType}}
|
||||
+ e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (e as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false)
|
||||
+ {{/items.complexType}}
|
||||
+ ).toList()
|
||||
+ : {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}}) : const Optional.absent(),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
+ {{^vendorExtensions.x-is-optional}}
|
||||
{{{name}}}: json[r'{{{baseName}}}'] is List
|
||||
? (json[r'{{{baseName}}}'] as List).map((e) =>
|
||||
{{#items.complexType}}
|
||||
- {{items.complexType}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}
|
||||
+ e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.complexType}}>[]{{/items.isNullable}} : {{items.complexType}}.listFromJson(e){{#uniqueItems}}.toSet(){{/uniqueItems}}
|
||||
{{/items.complexType}}
|
||||
{{^items.complexType}}
|
||||
- e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}>[]{{/items.isNullable}} : (e as List).cast<{{items.items.dataType}}>()
|
||||
+ e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (e as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false)
|
||||
{{/items.complexType}}
|
||||
).toList()
|
||||
: {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}},
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
{{/items.isArray}}
|
||||
{{^items.isArray}}
|
||||
+ {{#vendorExtensions.x-is-optional}}
|
||||
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}) : const Optional.absent(),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
+ {{^vendorExtensions.x-is-optional}}
|
||||
{{{name}}}: {{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}},
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
{{/items.isArray}}
|
||||
{{/isArray}}
|
||||
{{^isArray}}
|
||||
{{#isMap}}
|
||||
{{#items.isArray}}
|
||||
+ {{#vendorExtensions.x-is-optional}}
|
||||
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] == null ? null
|
||||
+ {{#items.complexType}}
|
||||
+ : {{items.complexType}}.mapListFromJson(json[r'{{{baseName}}}'])) : const Optional.absent(),
|
||||
+ {{/items.complexType}}
|
||||
+ {{^items.complexType}}
|
||||
+ : (json[r'{{{baseName}}}'] as Map<String, dynamic>).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (v as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false)))) : const Optional.absent(),
|
||||
+ {{/items.complexType}}
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
+ {{^vendorExtensions.x-is-optional}}
|
||||
{{{name}}}: json[r'{{{baseName}}}'] == null
|
||||
? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}
|
||||
- {{#items.complexType}}
|
||||
+ {{#items.complexType}}
|
||||
: {{items.complexType}}.mapListFromJson(json[r'{{{baseName}}}']),
|
||||
- {{/items.complexType}}
|
||||
- {{^items.complexType}}
|
||||
- : (json[r'{{{baseName}}}'] as Map<String, dynamic>).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}>[]{{/items.isNullable}} : (v as List).cast<{{items.items.dataType}}>())),
|
||||
- {{/items.complexType}}
|
||||
+ {{/items.complexType}}
|
||||
+ {{^items.complexType}}
|
||||
+ : (json[r'{{{baseName}}}'] as Map<String, dynamic>).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (v as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false))),
|
||||
+ {{/items.complexType}}
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
{{/items.isArray}}
|
||||
{{^items.isArray}}
|
||||
{{#items.isMap}}
|
||||
{{#items.complexType}}
|
||||
+ {{#vendorExtensions.x-is-optional}}
|
||||
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{items.complexType}}.mapFromJson(json[r'{{{baseName}}}'])) : const Optional.absent(),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
+ {{^vendorExtensions.x-is-optional}}
|
||||
{{{name}}}: {{items.complexType}}.mapFromJson(json[r'{{{baseName}}}']),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
{{/items.complexType}}
|
||||
{{^items.complexType}}
|
||||
+ {{#vendorExtensions.x-is-optional}}
|
||||
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapCastOfType<String, dynamic>(json, r'{{{baseName}}}')) : const Optional.absent(),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
+ {{^vendorExtensions.x-is-optional}}
|
||||
{{{name}}}: mapCastOfType<String, dynamic>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
{{/items.complexType}}
|
||||
{{/items.isMap}}
|
||||
{{^items.isMap}}
|
||||
{{#items.complexType}}
|
||||
+ {{#vendorExtensions.x-is-optional}}
|
||||
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{items.complexType}}}.mapFromJson(json[r'{{{baseName}}}'])) : const Optional.absent(),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
+ {{^vendorExtensions.x-is-optional}}
|
||||
{{{name}}}: {{{items.complexType}}}.mapFromJson(json[r'{{{baseName}}}']),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
{{/items.complexType}}
|
||||
{{^items.complexType}}
|
||||
+ {{#vendorExtensions.x-is-optional}}
|
||||
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapCastOfType<String, {{items.dataType}}>(json, r'{{{baseName}}}')) : const Optional.absent(),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
+ {{^vendorExtensions.x-is-optional}}
|
||||
{{{name}}}: mapCastOfType<String, {{items.dataType}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
{{/items.complexType}}
|
||||
{{/items.isMap}}
|
||||
{{/items.isArray}}
|
||||
@@ -259,23 +295,45 @@
|
||||
{{^complexType}}
|
||||
{{#isArray}}
|
||||
{{#isEnum}}
|
||||
+ {{#vendorExtensions.x-is-optional}}
|
||||
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}) : const Optional.absent(),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
+ {{^vendorExtensions.x-is-optional}}
|
||||
{{{name}}}: {{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}},
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
{{/isEnum}}
|
||||
{{^isEnum}}
|
||||
+ {{#vendorExtensions.x-is-optional}}
|
||||
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] is Iterable
|
||||
+ ? (json[r'{{{baseName}}}'] as Iterable).cast<{{{items.datatype}}}>().{{#uniqueItems}}toSet(){{/uniqueItems}}{{^uniqueItems}}toList(growable: false){{/uniqueItems}}
|
||||
+ : {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}) : const Optional.absent(),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
+ {{^vendorExtensions.x-is-optional}}
|
||||
{{{name}}}: json[r'{{{baseName}}}'] is Iterable
|
||||
? (json[r'{{{baseName}}}'] as Iterable).cast<{{{items.datatype}}}>().{{#uniqueItems}}toSet(){{/uniqueItems}}{{^uniqueItems}}toList(growable: false){{/uniqueItems}}
|
||||
: {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}},
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
{{/isEnum}}
|
||||
{{/isArray}}
|
||||
{{^isArray}}
|
||||
{{#isMap}}
|
||||
+ {{#vendorExtensions.x-is-optional}}
|
||||
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapCastOfType<String, {{{items.datatype}}}>(json, r'{{{baseName}}}')) : const Optional.absent(),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
+ {{^vendorExtensions.x-is-optional}}
|
||||
{{{name}}}: mapCastOfType<String, {{{items.datatype}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
{{/isMap}}
|
||||
{{^isMap}}
|
||||
{{#isNumber}}
|
||||
+ {{#vendorExtensions.x-is-optional}}
|
||||
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] == null ? null : num.parse('${json[r'{{{baseName}}}']}')) : const Optional.absent(),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
+ {{^vendorExtensions.x-is-optional}}
|
||||
{{{name}}}: {{#isNullable}}json[r'{{{baseName}}}'] == null
|
||||
? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}
|
||||
: {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'),
|
||||
+ {{/vendorExtensions.x-is-optional}}
|
||||
{{/isNumber}}
|
||||
{{^isNumber}}
|
||||
{{#vendorExtensions.x-original-is-integer}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"format": "prettier --cache --check i18n/",
|
||||
"format:fix": "prettier --cache --write --list-different i18n"
|
||||
},
|
||||
"packageManager": "pnpm@11.6.0",
|
||||
"packageManager": "pnpm@11.11.0",
|
||||
"engines": {
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.13.3",
|
||||
"@vitest/coverage-v8": "^4.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"oidc-provider": "^9.0.0",
|
||||
"tsx": "^4.20.6"
|
||||
},
|
||||
"packageManager": "pnpm@11.6.0"
|
||||
"packageManager": "pnpm@11.11.0"
|
||||
}
|
||||
|
||||
@@ -103,6 +103,12 @@
|
||||
"default": false,
|
||||
"title": "Case sensitive",
|
||||
"description": "Whether matching should be case-sensitive"
|
||||
},
|
||||
"usePath": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"title": "Use path",
|
||||
"description": "Match the full path on the server instead of the original filename"
|
||||
}
|
||||
},
|
||||
"required": ["pattern"]
|
||||
|
||||
@@ -54,11 +54,11 @@ const methods = wrapper<Manifest>({
|
||||
},
|
||||
|
||||
assetFileFilter: ({ data, config }) => {
|
||||
const { pattern, matchType = 'contains', caseSensitive = false } = config;
|
||||
const { pattern, matchType = 'contains', caseSensitive = false, usePath = false } = config;
|
||||
|
||||
const { asset } = data;
|
||||
|
||||
const fileName = asset.originalFileName || '';
|
||||
const fileName = usePath ? asset.originalPath : asset.originalFileName;
|
||||
const searchName = caseSensitive ? fileName : fileName.toLowerCase();
|
||||
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
||||
|
||||
|
||||
@@ -27,11 +27,11 @@
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"packageManager": "pnpm@11.6.0",
|
||||
"packageManager": "pnpm@11.11.0",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.1.1",
|
||||
"@immich/sdk": "workspace:*",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.13.3",
|
||||
"esbuild": "^0.28.0",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^6.0.0"
|
||||
|
||||
@@ -27,10 +27,10 @@
|
||||
"semver": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.13.3",
|
||||
"@types/semver": "^7.7.1",
|
||||
"vite": "^8.0.16",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"packageManager": "pnpm@11.6.0"
|
||||
"packageManager": "pnpm@11.11.0"
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.13.3",
|
||||
"typescript": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
5848
pnpm-lock.yaml
generated
5848
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -49,14 +49,14 @@
|
||||
"@nestjs/websockets": "^11.0.4",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/context-async-hooks": "^2.0.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.219.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.219.0",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.67.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.65.0",
|
||||
"@opentelemetry/instrumentation-pg": "^0.71.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.220.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.220.0",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.68.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.66.0",
|
||||
"@opentelemetry/instrumentation-pg": "^0.72.0",
|
||||
"@opentelemetry/resources": "^2.0.1",
|
||||
"@opentelemetry/sdk-metrics": "^2.0.1",
|
||||
"@opentelemetry/sdk-node": "^0.219.0",
|
||||
"@opentelemetry/sdk-node": "^0.220.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||
"@react-email/components": "^1.0.0",
|
||||
"@react-email/render": "^2.0.0",
|
||||
@@ -138,7 +138,7 @@
|
||||
"@types/luxon": "^3.6.2",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/node": "^24.13.3",
|
||||
"@types/nodemailer": "^8.0.0",
|
||||
"@types/picomatch": "^4.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
|
||||
@@ -190,11 +190,11 @@ type CustomExtensions = {
|
||||
};
|
||||
|
||||
enum ApiState {
|
||||
'Stable' = 'Stable',
|
||||
'Alpha' = 'Alpha',
|
||||
'Beta' = 'Beta',
|
||||
'Internal' = 'Internal',
|
||||
'Deprecated' = 'Deprecated',
|
||||
Stable = 'Stable',
|
||||
Alpha = 'Alpha',
|
||||
Beta = 'Beta',
|
||||
Internal = 'Internal',
|
||||
Deprecated = 'Deprecated',
|
||||
}
|
||||
export class HistoryBuilder {
|
||||
private hasDeprecated = false;
|
||||
|
||||
@@ -25,6 +25,8 @@ select
|
||||
inner join "plugin" on "plugin"."id" = "plugin_method"."pluginId"
|
||||
where
|
||||
"workflow"."id" = "workflow_step"."workflowId"
|
||||
order by
|
||||
"workflow_step"."order" asc
|
||||
) as agg
|
||||
) as "steps"
|
||||
from
|
||||
@@ -57,6 +59,8 @@ select
|
||||
inner join "plugin" on "plugin"."id" = "plugin_method"."pluginId"
|
||||
where
|
||||
"workflow"."id" = "workflow_step"."workflowId"
|
||||
order by
|
||||
"workflow_step"."order" asc
|
||||
) as agg
|
||||
) as "steps"
|
||||
from
|
||||
|
||||
@@ -154,8 +154,7 @@ export class IntegrityRepository {
|
||||
.select(['allPaths.path as path', 'allPaths.assetId', 'allPaths.fileAssetId', 'integrity_report.id as reportId'])
|
||||
.stream() as AsyncIterableIterator<
|
||||
{ path: string; reportId: string | null } & (
|
||||
| { assetId: string; fileAssetId: null }
|
||||
| { assetId: null; fileAssetId: string }
|
||||
{ assetId: string; fileAssetId: null } | { assetId: null; fileAssetId: string }
|
||||
)
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ export class WorkflowRepository {
|
||||
'plugin_method.name as methodName',
|
||||
'workflow_step.config',
|
||||
'workflow_step.enabled',
|
||||
]),
|
||||
])
|
||||
.orderBy('workflow_step.order', 'asc'),
|
||||
).as('steps'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -336,8 +336,7 @@ export interface IIntegrityUntrackedFilesJob {
|
||||
|
||||
export interface IIntegrityMissingFilesJob {
|
||||
items: ({ path: string; reportId: string | null } & (
|
||||
| { assetId: string; fileAssetId: null }
|
||||
| { assetId: null; fileAssetId: string }
|
||||
{ assetId: string; fileAssetId: null } | { assetId: null; fileAssetId: string }
|
||||
))[];
|
||||
}
|
||||
|
||||
@@ -553,8 +552,7 @@ export interface MemoryData {
|
||||
export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string };
|
||||
export type SystemFlags = { mountChecks: Record<StorageFolder, boolean> };
|
||||
export type MaintenanceModeState =
|
||||
| { isMaintenanceMode: true; secret: string; action?: SetMaintenanceModeDto }
|
||||
| { isMaintenanceMode: false };
|
||||
{ isMaintenanceMode: true; secret: string; action?: SetMaintenanceModeDto } | { isMaintenanceMode: false };
|
||||
export type MemoriesState = {
|
||||
/** memories have already been created through this date */
|
||||
lastOnThisDayDate: string;
|
||||
|
||||
@@ -76,9 +76,9 @@
|
||||
"@koddsson/eslint-plugin-tscompat": "^0.2.0",
|
||||
"@socket.io/component-emitter": "^3.1.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/enhanced-img": "^0.10.4",
|
||||
"@sveltejs/enhanced-img": "^0.11.0",
|
||||
"@sveltejs/kit": "^2.56.1",
|
||||
"@sveltejs/vite-plugin-svelte": "7.1.2",
|
||||
"@sveltejs/vite-plugin-svelte": "7.2.0",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/svelte": "^5.2.8",
|
||||
@@ -105,7 +105,7 @@
|
||||
"prettier-plugin-sort-json": "^4.1.1",
|
||||
"prettier-plugin-svelte": "^4.0.0",
|
||||
"rollup-plugin-visualizer": "^7.0.0",
|
||||
"svelte": "5.56.3",
|
||||
"svelte": "5.56.4",
|
||||
"svelte-check": "^4.4.6",
|
||||
"svelte-eslint-parser": "^1.3.3",
|
||||
"tailwindcss": "^4.2.4",
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<div
|
||||
bind:this={menuScrollView}
|
||||
class={[
|
||||
'fixed z-1 w-max max-w-75 min-w-50 immich-scrollbar rounded-lg bg-slate-100 shadow-lg duration-250 ease-in-out',
|
||||
'fixed z-70 w-max max-w-75 min-w-50 immich-scrollbar rounded-lg bg-slate-100 shadow-lg duration-250 ease-in-out',
|
||||
position.needScrollBar ? 'overflow-auto' : 'overflow-hidden',
|
||||
]}
|
||||
style:left="{position.left}px"
|
||||
|
||||
@@ -8,17 +8,7 @@ import { normalizeTransformEdits } from '$lib/utils/editor';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
|
||||
export type CropAspectRatio =
|
||||
| '1:1'
|
||||
| '16:9'
|
||||
| '4:3'
|
||||
| '3:2'
|
||||
| '7:5'
|
||||
| '9:16'
|
||||
| '3:4'
|
||||
| '2:3'
|
||||
| '5:7'
|
||||
| 'free'
|
||||
| 'reset';
|
||||
'1:1' | '16:9' | '4:3' | '3:2' | '7:5' | '9:16' | '3:4' | '2:3' | '5:7' | 'free' | 'reset';
|
||||
|
||||
type Region = {
|
||||
x: number;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export const enum ByteUnit {
|
||||
'B' = 'B',
|
||||
'KiB' = 'KiB',
|
||||
'MiB' = 'MiB',
|
||||
'GiB' = 'GiB',
|
||||
'TiB' = 'TiB',
|
||||
'PiB' = 'PiB',
|
||||
'EiB' = 'EiB',
|
||||
B = 'B',
|
||||
KiB = 'KiB',
|
||||
MiB = 'MiB',
|
||||
GiB = 'GiB',
|
||||
TiB = 'TiB',
|
||||
PiB = 'PiB',
|
||||
EiB = 'EiB',
|
||||
}
|
||||
|
||||
const byteUnits = [ByteUnit.B, ByteUnit.KiB, ByteUnit.MiB, ByteUnit.GiB, ByteUnit.TiB, ByteUnit.PiB, ByteUnit.EiB];
|
||||
|
||||
@@ -63,6 +63,8 @@ export const openFilePicker = async (options: FilePickerParam = {}) => {
|
||||
fileSelector.addEventListener(
|
||||
'change',
|
||||
(e: Event) => {
|
||||
fileSelector.remove();
|
||||
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (!target.files) {
|
||||
return;
|
||||
@@ -74,6 +76,11 @@ export const openFilePicker = async (options: FilePickerParam = {}) => {
|
||||
{ passive: true },
|
||||
);
|
||||
|
||||
fileSelector.addEventListener('cancel', () => fileSelector.remove(), { passive: true });
|
||||
|
||||
// Safari requires the file selector to be mounted
|
||||
fileSelector.hidden = true;
|
||||
document.body.append(fileSelector);
|
||||
fileSelector.click();
|
||||
} catch (error) {
|
||||
console.log('Error selecting file', error);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ServiceWorkerMessenger } from './sw-messenger';
|
||||
|
||||
const hasServiceWorker = globalThis.isSecureContext && 'serviceWorker' in navigator;
|
||||
// eslint-disable-next-line compat/compat
|
||||
const messenger = hasServiceWorker ? new ServiceWorkerMessenger(navigator.serviceWorker) : undefined;
|
||||
|
||||
export function cancelImageUrl(url: string | undefined | null) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto, invalidateAll } from '$app/navigation';
|
||||
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import UserPageLayout, { headerId } from '$lib/components/layouts/UserPageLayout.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/ButtonContextMenu.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/GalleryViewer.svelte';
|
||||
@@ -92,6 +93,8 @@
|
||||
</Sidebar>
|
||||
{/snippet}
|
||||
|
||||
<OnEvents onAssetsDelete={invalidateAll} />
|
||||
|
||||
<Breadcrumbs node={data.tree} icon={mdiFolderHome} title={$t('folders')} getLink={getLinkForPath} />
|
||||
|
||||
<section class="mt-2 h-[calc(100%-(--spacing(25)))] immich-scrollbar overflow-auto">
|
||||
|
||||
@@ -63,6 +63,5 @@
|
||||
{required}
|
||||
{value}
|
||||
oninput={handleInput}
|
||||
{disabled}
|
||||
></textarea>
|
||||
{disabled}></textarea>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user