Compare commits

..

3 Commits

Author SHA1 Message Date
Luis Nachtigall
b5401eb120 fix(mobile): update album creation to use user-defined name from dialog (#30002)
fix: update album creation to use user-defined name from dialog
2026-07-17 23:45:20 +05:30
Ben Beckford
12fc8bac18 feat: workflow filter assets by upload path (#30000)
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2026-07-17 09:46:48 +00:00
Ben Beckford
f2c00c107d fix(server): return workflow steps in ascending order (#29999) 2026-07-17 11:30:40 +02:00
12 changed files with 83 additions and 115 deletions

View File

@@ -129,7 +129,6 @@ class AppConfig {
.themeDynamic => theme.dynamicTheme,
.themeColorfulInterface => theme.colorfulInterface,
.imagePreferRemote => image.preferRemote,
.imageLoadPreview => image.loadPreview,
.imageLoadOriginal => image.loadOriginal,
.viewerLoopVideo => viewer.loopVideo,
.viewerLoadOriginalVideo => viewer.loadOriginalVideo,
@@ -182,7 +181,6 @@ class AppConfig {
.themeDynamic => copyWith(theme: theme.copyWith(dynamicTheme: value as bool)),
.themeColorfulInterface => copyWith(theme: theme.copyWith(colorfulInterface: value as bool)),
.imagePreferRemote => copyWith(image: image.copyWith(preferRemote: value as bool)),
.imageLoadPreview => copyWith(image: image.copyWith(loadPreview: value as bool)),
.imageLoadOriginal => copyWith(image: image.copyWith(loadOriginal: value as bool)),
.viewerLoopVideo => copyWith(viewer: viewer.copyWith(loopVideo: value as bool)),
.viewerLoadOriginalVideo => copyWith(viewer: viewer.copyWith(loadOriginalVideo: value as bool)),

View File

@@ -1,28 +1,20 @@
class ImageConfig {
final bool preferRemote;
final bool loadPreview;
final bool loadOriginal;
const ImageConfig({this.preferRemote = false, this.loadPreview = true, this.loadOriginal = false});
const ImageConfig({this.preferRemote = false, this.loadOriginal = false});
ImageConfig copyWith({bool? preferRemote, bool? loadPreview, bool? loadOriginal}) => ImageConfig(
preferRemote: preferRemote ?? this.preferRemote,
loadPreview: loadPreview ?? this.loadPreview,
loadOriginal: loadOriginal ?? this.loadOriginal,
);
ImageConfig copyWith({bool? preferRemote, bool? loadOriginal}) =>
ImageConfig(preferRemote: preferRemote ?? this.preferRemote, loadOriginal: loadOriginal ?? this.loadOriginal);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is ImageConfig &&
other.preferRemote == preferRemote &&
other.loadPreview == loadPreview &&
other.loadOriginal == loadOriginal);
(other is ImageConfig && other.preferRemote == preferRemote && other.loadOriginal == loadOriginal);
@override
int get hashCode => Object.hash(preferRemote, loadPreview, loadOriginal);
int get hashCode => Object.hash(preferRemote, loadOriginal);
@override
String toString() =>
'ImageConfig(preferRemoteImage: $preferRemote, loadPreview: $loadPreview, loadOriginal: $loadOriginal)';
String toString() => 'ImageConfig(preferRemoteImage: $preferRemote, loadOriginal: $loadOriginal)';
}

View File

@@ -17,7 +17,6 @@ enum SettingsKey<T> {
// Image
imagePreferRemote<bool>(),
imageLoadPreview<bool>(),
imageLoadOriginal<bool>(),
// Viewer

View File

@@ -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());

View File

@@ -45,6 +45,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
completer.operation.valueOrCancellation().whenComplete(() {
cachedStream.removeListener(listener);
cachedOperation = null;
});
cachedOperation = completer.operation;
return null;
@@ -94,9 +95,6 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
isFinished = isFinal;
return codec;
} catch (e) {
if (isCancelled) {
return null;
}
if (isFinal) {
isFinished = true;
PaintingBinding.instance.imageCache.evict(this);
@@ -108,33 +106,18 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
}
}
Stream<ImageInfo> initialImageStream({required bool isFinal}) async* {
Stream<ImageInfo> initialImageStream() async* {
final cachedOperation = this.cachedOperation;
if (isCancelled) {
return;
}
if (cachedOperation == null) {
// image resolved synchronously
isFinished = isFinal;
return;
}
try {
final cachedImage = await cachedOperation.valueOrCancellation();
if (isCancelled || cachedImage == null) {
return;
if (cachedImage != null && !isCancelled) {
yield cachedImage;
}
isFinished = isFinal;
yield cachedImage;
} catch (e, stack) {
if (isCancelled) {
return;
}
if (isFinal) {
isFinished = true;
PaintingBinding.instance.imageCache.evict(this);
rethrow;
}
_log.severe('Error loading initial image', e, stack);
} finally {
this.cachedOperation = null;

View File

@@ -98,55 +98,51 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
}
Stream<ImageInfo> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
final loadOriginal = SettingsRepository.instance.appConfig.image.loadOriginal;
final loadPreview = SettingsRepository.instance.appConfig.image.loadPreview;
yield* initialImageStream(isFinal: !loadOriginal && !loadPreview);
yield* initialImageStream();
if (isCancelled) {
return;
}
if (loadPreview) {
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
final previewRequest = request = LocalImageRequest(
localId: key.id,
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
assetType: key.assetType,
);
yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
if (isCancelled) {
return;
}
}
final loadOriginal = SettingsRepository.instance.appConfig.image.loadOriginal;
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
var request = this.request = LocalImageRequest(
localId: key.id,
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
assetType: key.assetType,
);
yield* loadRequest(request, decode, isFinal: !loadOriginal);
if (!loadOriginal) {
return;
}
final originalRequest = request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero);
yield* loadRequest(originalRequest, decode, isFinal: true);
if (isCancelled) {
return;
}
request = this.request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero);
yield* loadRequest(request, decode, isFinal: true);
}
Stream<Object> _animatedCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
yield* initialImageStream(isFinal: false);
yield* initialImageStream();
if (isCancelled) {
return;
}
if (SettingsRepository.instance.appConfig.image.loadPreview) {
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
final previewRequest = request = LocalImageRequest(
localId: key.id,
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
assetType: key.assetType,
);
yield* loadRequest(previewRequest, decode, isFinal: false);
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
final previewRequest = request = LocalImageRequest(
localId: key.id,
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
assetType: key.assetType,
);
yield* loadRequest(previewRequest, decode, isFinal: false);
if (isCancelled) {
return;
}
if (isCancelled) {
return;
}
// always try original for animated, since previews don't support animation

View File

@@ -108,35 +108,31 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
}
Stream<ImageInfo> _codec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* {
final isImage = assetType == AssetType.image;
final loadOriginal = isImage && SettingsRepository.instance.appConfig.image.loadOriginal;
final loadPreview = isImage && SettingsRepository.instance.appConfig.image.loadPreview;
yield* initialImageStream(isFinal: !loadOriginal && !loadPreview);
yield* initialImageStream();
if (isCancelled) {
return;
}
if (loadPreview) {
final previewRequest = request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(
key.assetId,
type: AssetMediaSize.preview,
thumbhash: key.thumbhash,
edited: key.edited,
),
);
yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
if (isCancelled) {
return;
}
}
final previewRequest = request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(
key.assetId,
type: AssetMediaSize.preview,
thumbhash: key.thumbhash,
edited: key.edited,
),
);
final loadOriginal = assetType == AssetType.image && SettingsRepository.instance.appConfig.image.loadOriginal;
yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
if (!loadOriginal) {
return;
}
if (isCancelled) {
return;
}
final originalRequest = request = RemoteImageRequest(
uri: getOriginalUrlForRemoteId(key.assetId, edited: key.edited),
);
@@ -144,26 +140,24 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
}
Stream<Object> _animatedCodec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* {
yield* initialImageStream(isFinal: false);
yield* initialImageStream();
if (isCancelled) {
return;
}
if (SettingsRepository.instance.appConfig.image.loadPreview) {
final previewRequest = request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(
key.assetId,
type: AssetMediaSize.preview,
thumbhash: key.thumbhash,
edited: key.edited,
),
);
yield* loadRequest(previewRequest, decode, isFinal: false);
final previewRequest = request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(
key.assetId,
type: AssetMediaSize.preview,
thumbhash: key.thumbhash,
edited: key.edited,
),
);
yield* loadRequest(previewRequest, decode, isFinal: false);
if (isCancelled) {
return;
}
if (isCancelled) {
return;
}
// always try original for animated, since previews don't support animation

View File

@@ -12,11 +12,7 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPreview = useState(ref.read(appConfigProvider).image.loadPreview);
final isOriginal = useState(ref.read(appConfigProvider).image.loadOriginal);
useValueChanged<bool, void>(isPreview.value, (_, __) {
ref.read(settingsProvider).write(.imageLoadPreview, isPreview.value);
});
useValueChanged<bool, void>(isOriginal.value, (_, __) {
ref.read(settingsProvider).write(.imageLoadOriginal, isOriginal.value);
});
@@ -29,12 +25,6 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
icon: Icons.image_outlined,
subtitle: "setting_image_viewer_help".t(context: context),
),
SettingsSwitchListTile(
valueNotifier: isPreview,
title: "setting_image_viewer_preview_title".t(context: context),
subtitle: "setting_image_viewer_preview_subtitle".t(context: context),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
SettingsSwitchListTile(
valueNotifier: isOriginal,
title: "setting_image_viewer_original_title".t(context: context),

View File

@@ -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"]

View File

@@ -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();

View File

@@ -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

View File

@@ -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'),
]);
}