Compare commits

..

12 Commits

Author SHA1 Message Date
mertalev a338e294a6 update commit 2026-06-30 15:32:26 -04:00
mertalev 25f245dd4f update commit 2026-06-30 15:20:49 -04:00
mertalev 2e3d8a1225 try/catch 2026-06-24 18:26:41 -04:00
mertalev 697818732a debug logging 2026-06-24 18:21:26 -04:00
mertalev df590d276a update revision 2026-06-24 17:11:06 -04:00
mertalev 37fa812904 fix placeholder seam 2026-06-24 17:11:06 -04:00
mertalev d473b664dc refactor 2026-06-24 17:11:05 -04:00
mertalev b2d20edb26 move to notifier 2026-06-24 17:11:05 -04:00
mertalev f243712eac update pubspec 2026-06-24 17:11:05 -04:00
mertalev e654001d17 use hls in video viewer 2026-06-24 17:11:05 -04:00
mertalev b9211c62d0 unused import 2026-06-24 17:11:05 -04:00
mertalev 8321be4745 add playlist hint 2026-06-24 17:11:05 -04:00
15 changed files with 209 additions and 187 deletions
@@ -67,6 +67,9 @@ class URLSessionManager: NSObject {
delegate = URLSessionManagerDelegate()
session = Self.buildSession(delegate: delegate)
super.init()
if #available(iOS 15, *) {
VideoProxyServer.shared.session = session
}
Self.serverUrls = UserDefaults.group.stringArray(forKey: SERVER_URLS_KEY) ?? []
NotificationCenter.default.addObserver(
Self.self,
@@ -78,6 +81,9 @@ class URLSessionManager: NSObject {
func recreateSession() {
session = Self.buildSession(delegate: delegate)
if #available(iOS 15, *) {
VideoProxyServer.shared.session = session
}
}
static func setServerUrls(_ urls: [String]) {
@@ -249,9 +255,6 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
let credential = URLCredential(identity: identity as! SecIdentity,
certificates: nil,
persistence: .forSession)
if #available(iOS 15, *) {
VideoProxyServer.shared.session = session
}
return completion(.useCredential, credential)
}
completion(.performDefaultHandling, nil)
@@ -268,9 +271,6 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
else {
return completion(.performDefaultHandling, nil)
}
if #available(iOS 15, *) {
VideoProxyServer.shared.session = session
}
let credential = URLCredential(user: user, password: password, persistence: .forSession)
completion(.useCredential, credential)
}
@@ -7,6 +7,7 @@ class ServerFeatures {
final bool passwordLogin;
final bool ocr;
final bool smartSearch;
final bool realtimeTranscoding;
const ServerFeatures({
required this.trash,
@@ -15,6 +16,7 @@ class ServerFeatures {
required this.passwordLogin,
this.ocr = false,
this.smartSearch = false,
this.realtimeTranscoding = false,
});
ServerFeatures copyWith({
@@ -24,6 +26,7 @@ class ServerFeatures {
bool? passwordLogin,
bool? ocr,
bool? smartSearch,
bool? realtimeTranscoding,
}) {
return ServerFeatures(
trash: trash ?? this.trash,
@@ -32,12 +35,13 @@ class ServerFeatures {
passwordLogin: passwordLogin ?? this.passwordLogin,
ocr: ocr ?? this.ocr,
smartSearch: smartSearch ?? this.smartSearch,
realtimeTranscoding: realtimeTranscoding ?? this.realtimeTranscoding,
);
}
@override
String toString() {
return 'ServerFeatures(trash: $trash, map: $map, oauthEnabled: $oauthEnabled, passwordLogin: $passwordLogin, ocr: $ocr, smartSearch: $smartSearch)';
return 'ServerFeatures(trash: $trash, map: $map, oauthEnabled: $oauthEnabled, passwordLogin: $passwordLogin, ocr: $ocr, smartSearch: $smartSearch, realtimeTranscoding: $realtimeTranscoding)';
}
ServerFeatures.fromDto(ServerFeaturesDto dto)
@@ -46,7 +50,8 @@ class ServerFeatures {
oauthEnabled = dto.oauth,
passwordLogin = dto.passwordLogin,
ocr = dto.ocr,
smartSearch = dto.smartSearch;
smartSearch = dto.smartSearch,
realtimeTranscoding = dto.realtimeTranscoding;
@override
bool operator ==(covariant ServerFeatures other) {
@@ -59,7 +64,8 @@ class ServerFeatures {
other.oauthEnabled == oauthEnabled &&
other.passwordLogin == passwordLogin &&
other.ocr == ocr &&
other.smartSearch == smartSearch;
other.smartSearch == smartSearch &&
other.realtimeTranscoding == realtimeTranscoding;
}
@override
@@ -69,6 +75,7 @@ class ServerFeatures {
oauthEnabled.hashCode ^
passwordLogin.hashCode ^
ocr.hashCode ^
smartSearch.hashCode;
smartSearch.hashCode ^
realtimeTranscoding.hashCode;
}
}
@@ -12,7 +12,6 @@ import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/memory/memory_bottom_info.widget.dart';
import 'package:immich_mobile/presentation/widgets/memory/memory_card.widget.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/utils/system_ui.utils.dart';
import 'package:immich_mobile/widgets/memories/memory_epilogue.dart';
import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart';
@@ -50,7 +49,7 @@ class DriftMemoryPage extends HookConsumerWidget {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
return () {
// Clean up to normal edge to edge when we are done
restoreEdgeToEdge();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
};
});
@@ -329,7 +328,7 @@ class DriftMemoryPage extends HookConsumerWidget {
// turn off full screen mode here
// https://github.com/Milad-Akarie/auto_route_library/issues/1799
context.maybePop();
restoreEdgeToEdge();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
},
shape: const CircleBorder(),
color: Colors.white.withValues(alpha: 0.2),
@@ -19,7 +19,6 @@ import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/system_ui.utils.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
@@ -77,7 +76,7 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
_pageController.dispose();
_crossfadeController.dispose();
unawaited(WakelockPlus.disable());
unawaited(restoreEdgeToEdge());
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
super.dispose();
}
@@ -256,7 +255,7 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
}
void _onTapUp() async {
await (_showAppBar ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive) : restoreEdgeToEdge());
await SystemChrome.setEnabledSystemUIMode(_showAppBar ? SystemUiMode.immersive : SystemUiMode.edgeToEdge);
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
@@ -378,7 +377,7 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
child: NativeVideoViewer(
asset: asset,
isCurrent: isCurrent,
image: Image(image: imageProvider, fit: BoxFit.contain, alignment: Alignment.center),
imageProvider: imageProvider,
),
);
}
@@ -388,7 +388,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
asset: asset,
localFilePath: localFilePath,
isCurrent: isCurrent,
image: Image(image: imageProvider, fit: BoxFit.contain, alignment: Alignment.center),
imageProvider: imageProvider,
),
);
}
@@ -23,7 +23,6 @@ import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/utils/system_ui.utils.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
@RoutePage()
@@ -129,7 +128,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
_reloadSubscription?.cancel();
_stackChildrenKeepAlive?.close();
unawaited(restoreEdgeToEdge());
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
super.dispose();
}
@@ -252,8 +251,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
}
void _setSystemUIMode(bool controls, bool details) {
final immersive = !controls || (CurrentPlatform.isIOS && details);
unawaited(immersive ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky) : restoreEdgeToEdge());
final mode = !controls || (CurrentPlatform.isIOS && details)
? SystemUiMode.immersiveSticky
: SystemUiMode.edgeToEdge;
unawaited(SystemChrome.setEnabledSystemUIMode(mode));
}
@override
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
@@ -14,22 +15,31 @@ import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:native_video_player/native_video_player.dart';
final _hlsVideoSessionIdRegex = RegExp(
r'/video/stream/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/',
);
// For BC if we add an audio endpoint
final _hlsAudioSessionIdRegex = RegExp(
r'/audio/stream/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/',
);
class NativeVideoViewer extends ConsumerStatefulWidget {
final BaseAsset asset;
final String? localFilePath;
final bool isCurrent;
final bool showControls;
final Widget image;
final ImageProvider imageProvider;
const NativeVideoViewer({
super.key,
required this.asset,
this.localFilePath,
required this.image,
required this.imageProvider,
this.isCurrent = false,
this.showControls = true,
});
@@ -46,6 +56,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
Timer? _loadTimer;
bool _isVideoReady = false;
bool _shouldPlayOnForeground = true;
String? _remoteAssetId;
VideoPlayerNotifier get _notifier => ref.read(videoPlayerProvider(widget.asset.heroTag).notifier);
@@ -67,11 +78,16 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
if (!widget.isCurrent) {
_loadTimer?.cancel();
_notifier.pause();
_notifier.endHlsSession();
return;
}
// Prevent unnecessary loading when swiping between assets.
_loadTimer = Timer(const Duration(milliseconds: 200), _loadVideo);
if (ref.read(serverInfoProvider).serverFeatures.realtimeTranscoding) {
_loadVideo();
} else {
// Prevent unnecessary loading when swiping between assets.
_loadTimer = Timer(const Duration(milliseconds: 200), _loadVideo);
}
}
@override
@@ -141,14 +157,22 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
);
}
final remoteId = (videoAsset as RemoteAsset).id;
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
final String videoUrl = videoAsset.livePhotoVideoId != null
? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl'
: '$serverEndpoint/assets/$remoteId/$postfixUrl';
final realtimeTranscoding = ref.read(serverInfoProvider).serverFeatures.realtimeTranscoding;
// Motion photo clips are short, so spinning up a transcoding session for them is wasteful
final useHls = !isOriginalVideo && videoAsset.livePhotoVideoId == null && realtimeTranscoding;
final remoteId = (videoAsset as RemoteAsset).id;
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final String videoUrl;
if (useHls) {
videoUrl = '$serverEndpoint/assets/$remoteId/video/stream/main.m3u8';
} else {
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
videoUrl = videoAsset.livePhotoVideoId != null
? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl'
: '$serverEndpoint/assets/$remoteId/$postfixUrl';
}
_remoteAssetId = remoteId;
return VideoSource.init(path: videoUrl, type: VideoSourceType.network, headers: ApiService.getRequestHeaders());
} catch (error) {
@@ -209,11 +233,17 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
_notifier.onNativeStatusChanged();
}
void _onSourceResolved() {
final url = _controller?.onPlaybackSourceResolved.value;
_notifier.updateHlsSession(assetId: _remoteAssetId, sessionId: url == null ? null : _extractHlsSessionId(url));
}
void _removeListeners() {
_controller?.onPlaybackPositionChanged.removeListener(_onPlaybackPositionChanged);
_controller?.onPlaybackStatusChanged.removeListener(_onPlaybackStatusChanged);
_controller?.onPlaybackReady.removeListener(_onPlaybackReady);
_controller?.onPlaybackEnded.removeListener(_onPlaybackEnded);
_controller?.onPlaybackSourceResolved.removeListener(_onSourceResolved);
}
void _loadVideo() async {
@@ -244,6 +274,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
nc.onPlaybackStatusChanged.addListener(_onPlaybackStatusChanged);
nc.onPlaybackReady.addListener(_onPlaybackReady);
nc.onPlaybackEnded.addListener(_onPlaybackEnded);
nc.onPlaybackSourceResolved.addListener(_onSourceResolved);
_controller = nc;
@@ -252,30 +283,85 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
}
}
/// Extracts the HLS session id from a resolved playlist or segment URL,
/// e.g. `https://host/api/assets/{id}/video/stream/{sessionId}/0/playlist.m3u8`.
String? _extractHlsSessionId(String url) =>
_hlsVideoSessionIdRegex.firstMatch(url)?.group(1) ?? _hlsAudioSessionIdRegex.firstMatch(url)?.group(1);
@override
Widget build(BuildContext context) {
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
final status = ref.watch(videoPlayerProvider(widget.asset.heroTag).select((v) => v.status));
final image = Image(image: widget.imageProvider, fit: BoxFit.contain, alignment: Alignment.center);
if (ref.watch(castProvider.select((c) => c.isCasting))) {
return IgnorePointer(child: Center(child: image));
}
final status = ref.watch(videoPlayerProvider(widget.asset.heroTag).select((v) => v.status));
return IgnorePointer(
child: Stack(
clipBehavior: Clip.none,
children: [
Center(child: widget.image),
if (!isCasting) ...[
Visibility.maintain(
visible: _isVideoReady,
child: NativeVideoPlayerView(onViewReady: _initController),
),
Center(
child: AnimatedOpacity(
opacity: status == VideoPlaybackStatus.buffering ? 1.0 : 0.0,
duration: const Duration(milliseconds: 400),
child: const CircularProgressIndicator(),
),
),
],
// The engine snaps the video platform view to the device-pixel grid; snap the
// placeholder the same way so it doesn't show a hairline past the video's edge.
_DevicePixelSnap(devicePixelRatio: MediaQuery.devicePixelRatioOf(context), child: image),
Visibility.maintain(
visible: _isVideoReady,
child: NativeVideoPlayerView(onViewReady: _initController),
),
if (status == VideoPlaybackStatus.buffering) const Center(child: CircularProgressIndicator()),
],
),
);
}
}
class _DevicePixelSnap extends SingleChildRenderObjectWidget {
final double devicePixelRatio;
const _DevicePixelSnap({required this.devicePixelRatio, required Widget super.child});
@override
_RenderDevicePixelSnap createRenderObject(BuildContext context) => _RenderDevicePixelSnap(devicePixelRatio);
@override
void updateRenderObject(BuildContext context, _RenderDevicePixelSnap renderObject) {
renderObject.devicePixelRatio = devicePixelRatio;
}
}
class _RenderDevicePixelSnap extends RenderShiftedBox {
_RenderDevicePixelSnap(this._devicePixelRatio) : super(null);
double _devicePixelRatio;
set devicePixelRatio(double value) {
if (_devicePixelRatio == value) {
return;
}
_devicePixelRatio = value;
markNeedsLayout();
}
/// The largest device-pixel-aligned extent that still tucks under the platform view.
double _snap(double extent) {
final scaled = extent * _devicePixelRatio;
final floored = scaled.floorToDouble();
if (floored == scaled) {
return extent;
}
final pixels = floored - 1;
return pixels <= 0 ? extent : pixels / _devicePixelRatio;
}
@override
Size computeDryLayout(BoxConstraints constraints) => constraints.biggest;
@override
void performLayout() {
size = constraints.biggest;
final child = this.child;
if (child == null) {
return;
}
child.layout(BoxConstraints.tight(Size(_snap(size.width), _snap(size.height))));
(child.parentData! as BoxParentData).offset = Offset.zero;
}
}
@@ -66,7 +66,7 @@ class DriftMemoryCard extends StatelessWidget {
asset: asset,
isCurrent: isCurrent,
showControls: false,
image: FullImage(asset, size: context.sizeData, fit: BoxFit.contain),
imageProvider: getFullImageProvider(asset, size: context.sizeData),
),
),
);
@@ -1,8 +1,10 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:logging/logging.dart';
import 'package:native_video_player/native_video_player.dart';
import 'package:openapi/api.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
enum VideoPlaybackStatus { paused, playing, buffering, completed }
@@ -33,21 +35,25 @@ final videoPlayerProvider = StateNotifierProvider.autoDispose.family<VideoPlayer
ref,
name,
) {
return VideoPlayerNotifier();
return VideoPlayerNotifier(ref.read(apiServiceProvider).assetsApi);
});
class VideoPlayerNotifier extends StateNotifier<VideoPlayerState> {
static final _log = Logger('VideoPlayerNotifier');
VideoPlayerNotifier() : super(_defaultState);
VideoPlayerNotifier(this._assetsApi) : super(_defaultState);
final AssetsApi _assetsApi;
NativeVideoPlayerController? _controller;
Timer? _bufferingTimer;
Timer? _seekTimer;
VideoPlaybackStatus? _holdStatus;
String? _hlsAssetId;
String? _hlsSessionId;
@override
void dispose() {
endHlsSession();
_bufferingTimer?.cancel();
_seekTimer?.cancel();
WakelockPlus.disable();
@@ -60,6 +66,29 @@ class VideoPlayerNotifier extends StateNotifier<VideoPlayerState> {
_controller = controller;
}
void updateHlsSession({required String? assetId, required String? sessionId}) {
if (sessionId == null || sessionId == _hlsSessionId) {
return;
}
endHlsSession();
_hlsAssetId = assetId;
_hlsSessionId = sessionId;
}
void endHlsSession() {
final assetId = _hlsAssetId;
final sessionId = _hlsSessionId;
_hlsSessionId = null;
if (assetId == null || sessionId == null) {
return;
}
unawaited(
_assetsApi.endSession(assetId, sessionId).onError((error, stackTrace) {
_log.warning('Failed to end HLS session $sessionId for asset $assetId', error, stackTrace);
}),
);
}
Future<void> load(VideoSource source) async {
_startBufferingTimer();
try {
-14
View File
@@ -1,14 +0,0 @@
import 'dart:async';
import 'package:flutter/services.dart';
/// Restore the system bars and return to edge-to-edge layout.
///
/// On Android 15+/API 36 edge-to-edge is enforced, so calling
/// setEnabledSystemUIMode(edgeToEdge) does NOT re-show bars that an immersive
/// mode (immersive / immersiveSticky) previously hid. Explicitly request all
/// overlays first, then return to edge-to-edge layout.
Future<void> restoreEdgeToEdge() async {
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
+2 -2
View File
@@ -1128,8 +1128,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: cdf621bdb7edaf996e118a58a48f6441187d79c6
resolved-ref: cdf621bdb7edaf996e118a58a48f6441187d79c6
ref: "7b07eb0af23de588a8c937c203bcf157397b2c5c"
resolved-ref: "7b07eb0af23de588a8c937c203bcf157397b2c5c"
url: "https://github.com/immich-app/native_video_player"
source: git
version: "1.3.1"
+1 -1
View File
@@ -48,7 +48,7 @@ dependencies:
native_video_player:
git:
url: https://github.com/immich-app/native_video_player
ref: 'cdf621bdb7edaf996e118a58a48f6441187d79c6'
ref: '8b8184d71f2d1ccc32fe153152b2615c0c718b3e'
network_info_plus: ^6.1.4
octo_image: ^2.1.0
openapi:
-70
View File
@@ -203,76 +203,6 @@
},
"uiHints": ["Filter"]
},
{
"name": "assetExifFilter",
"title": "Filter by EXIF metadata",
"description": "Filter assets by their EXIF properties",
"types": ["AssetV1"],
"schema": {
"type": "object",
"properties": {
"property": {
"title": "Property",
"description": "EXIF property to match",
"type": "string",
"enum": [
"make",
"model",
"exifImageWidth",
"exifImageHeight",
"fileSizeInByte",
"orientation",
"lensModel",
"fNumber",
"focalLength",
"iso",
"description",
"fps",
"exposureTime",
"livePhotoCID",
"timeZone",
"projectionType",
"profileDescription",
"colorspace",
"bitsPerSample",
"rating"
],
"uiHint": {
"order": 1
}
},
"pattern": {
"type": "string",
"title": "Pattern",
"description": "Text or regex pattern to match against property value",
"uiHint": {
"order": 2
}
},
"matchType": {
"type": "string",
"title": "Match type",
"enum": ["contains", "startsWith", "exact", "regex"],
"default": "contains",
"description": "Type of pattern matching to perform",
"uiHint": {
"order": 3
}
},
"caseSensitive": {
"type": "boolean",
"default": false,
"title": "Case sensitive",
"description": "Whether matching should be case-sensitive",
"uiHint": {
"order": 4
}
}
},
"required": ["property", "pattern"]
},
"uiHints": ["Filter"]
},
{
"name": "assetArchive",
"title": "Archive asset",
-1
View File
@@ -15,7 +15,6 @@ declare module 'main' {
export function assetMissingTimeZoneFilter(): I32;
export function assetLocationFilter(): I32;
export function assetTypeFilter(): I32;
export function assetExifFilter(): I32;
// updates
export function assetFavorite(): I32;
+33 -47
View File
@@ -4,44 +4,40 @@ import type manifestType from '../dist/manifest';
const wrapper = getWrapper<manifestType>();
type MatchValueConfig = {
pattern: string;
matchType?: 'contains' | 'exact' | 'regex' | 'startsWith';
caseSensitive?: boolean;
};
const matchValueResult = (value: string, config: MatchValueConfig) => {
const { pattern, matchType = 'contains', caseSensitive = false } = config;
const searchName = caseSensitive ? value : value.toLowerCase();
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
switch (matchType) {
case 'contains': {
return { workflow: { continue: searchName.includes(searchPattern) } };
}
case 'exact': {
return { workflow: { continue: searchName === searchPattern } };
}
case 'startsWith': {
return { workflow: { continue: searchName.startsWith(searchPattern) } };
}
case 'regex': {
const flags = caseSensitive ? '' : 'i';
const regex = new RegExp(searchPattern, flags);
return { workflow: { continue: regex.test(value) } };
}
default: {
return {};
}
}
};
export const assetFileFilter = () => {
return wrapper<'assetFileFilter'>(({ data, config }) => matchValueResult(data.asset.originalFileName || '', config));
return wrapper<'assetFileFilter'>(({ data, config }) => {
const { pattern, matchType = 'contains', caseSensitive = false } = config;
const { asset } = data;
const fileName = asset.originalFileName || '';
const searchName = caseSensitive ? fileName : fileName.toLowerCase();
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
switch (matchType) {
case 'contains': {
return { workflow: { continue: searchName.includes(searchPattern) } };
}
case 'exact': {
return { workflow: { continue: searchName === searchPattern } };
}
case 'startsWith': {
return { workflow: { continue: searchName.startsWith(searchPattern) } };
}
case 'regex': {
const flags = caseSensitive ? '' : 'i';
const regex = new RegExp(searchPattern, flags);
return { workflow: { continue: regex.test(fileName) } };
}
default: {
return {};
}
}
});
};
export const assetMissingTimeZoneFilter = () => {
@@ -91,16 +87,6 @@ export const assetLocationFilter = () => {
});
};
export const assetExifFilter = () => {
return wrapper<'assetExifFilter'>(({ config, data }) => {
if (!data.asset.exifInfo) {
return { workflow: { continue: false } };
}
return matchValueResult(String(data.asset.exifInfo[config.property] || ''), config);
});
};
export const assetTypeFilter = () => {
return wrapper<'assetTypeFilter'>(({ config, data }) => {
return { workflow: { continue: config.allowedTypes.includes(data.asset.type) } };