Compare commits

...

6 Commits

Author SHA1 Message Date
Alex a564d46017 feat: new feature board 2026-06-22 16:59:23 -05:00
Daniel Dietzler c484bd99b6 fix: ignore external libraries for integrity report checksum check (#29248) 2026-06-22 13:56:24 +00:00
Anthony Clerici c0bf5a4c56 fix(server): use VBR for QSV so the max bitrate is respected (#29240)
* fix(server): use VBR for QSV so the max bitrate is respected

* update test
2026-06-22 09:56:20 -04:00
MuySup d9d50d2848 fix: turkish readme translation (#29234)
* Translation completed

3-2-1 rule translated

* Fix formatting of warning message in Turkish README
2026-06-22 09:55:58 -04:00
Daniel Dietzler c7453a67fd fix: detail panel people reactivity and iterator consumption (#29250) 2026-06-22 15:47:09 +02:00
Daniel Dietzler e918e3a313 feat: keyboard seeking for new video player (#29208) 2026-06-22 09:42:59 -04:00
29 changed files with 535 additions and 14 deletions
+17
View File
@@ -1251,6 +1251,22 @@
"favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
"favorites": "Favorites",
"favorites_page_no_favorites": "No favorite assets found",
"feature_message_non_destructive_editing_body": "Edit your photos freely — the original is always kept untouched.",
"feature_message_non_destructive_editing_title": "Non-destructive editing",
"feature_message_ocr_body": "Immich now reads the text inside your photos, so you can search for them by what they say.",
"feature_message_ocr_title": "Search text in your photos",
"feature_message_open_in_immich_body": "Set Immich as your gallery on Android to open photos straight from other apps.",
"feature_message_open_in_immich_title": "Open photos in Immich",
"feature_message_recently_added_body": "Jump straight to everything you've added lately on a dedicated page.",
"feature_message_recently_added_title": "Recently added",
"feature_message_settings_subtitle": "See what's new in version {version}",
"feature_message_share_quality_body": "Press and hold the share button to choose the image quality before you share.",
"feature_message_share_quality_title": "Choose your share quality",
"feature_message_slideshow_body": "Sit back and watch your photos play in a full-screen slideshow.",
"feature_message_slideshow_title": "Slideshow",
"feature_message_upload_to_album_body": "Add photos directly into an album as you upload them.",
"feature_message_upload_to_album_title": "Upload straight to an album",
"feature_message_version": "Version {version}",
"feature_photo_updated": "Feature photo updated",
"features": "Features",
"features_in_development": "Features in Development",
@@ -2530,6 +2546,7 @@
"week": "Week",
"welcome": "Welcome",
"welcome_to_immich": "Welcome to Immich",
"whats_new": "What's new",
"when": "When",
"width": "Width",
"wifi_name": "Wi-Fi Name",
Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

@@ -4,6 +4,7 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/config/album_config.dart';
import 'package:immich_mobile/domain/models/config/backup_config.dart';
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
import 'package:immich_mobile/domain/models/config/feature_message_config.dart';
import 'package:immich_mobile/domain/models/config/image_config.dart';
import 'package:immich_mobile/domain/models/config/map_config.dart';
import 'package:immich_mobile/domain/models/config/network_config.dart';
@@ -32,6 +33,7 @@ class AppConfig {
final BackupConfig backup;
final NetworkConfig network;
final ShareConfig share;
final FeatureMessageConfig featureMessage;
const AppConfig({
this.logLevel = .info,
@@ -46,6 +48,7 @@ class AppConfig {
this.backup = const .new(),
this.network = const .new(),
this.share = const .new(),
this.featureMessage = const .new(),
});
AppConfig copyWith({
@@ -61,6 +64,7 @@ class AppConfig {
BackupConfig? backup,
NetworkConfig? network,
ShareConfig? share,
FeatureMessageConfig? featureMessage,
}) => .new(
logLevel: logLevel ?? this.logLevel,
theme: theme ?? this.theme,
@@ -74,6 +78,7 @@ class AppConfig {
backup: backup ?? this.backup,
network: network ?? this.network,
share: share ?? this.share,
featureMessage: featureMessage ?? this.featureMessage,
);
@override
@@ -91,15 +96,29 @@ class AppConfig {
other.album == album &&
other.backup == backup &&
other.network == network &&
other.share == share);
other.share == share &&
other.featureMessage == featureMessage);
@override
int get hashCode =>
Object.hash(logLevel, theme, cleanup, map, timeline, image, viewer, slideshow, album, backup, network, share);
int get hashCode => Object.hash(
logLevel,
theme,
cleanup,
map,
timeline,
image,
viewer,
slideshow,
album,
backup,
network,
share,
featureMessage,
);
@override
String toString() =>
'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network, share: $share)';
'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network, share: $share, featureMessage: $featureMessage)';
T read<T>(SettingsKey<T> key) =>
(switch (key) {
@@ -146,6 +165,7 @@ class AppConfig {
.slideshowDuration => slideshow.duration,
.slideshowLook => slideshow.look,
.slideshowDirection => slideshow.direction,
.featureMessageSeenVersion => featureMessage.seenVersion,
})
as T;
@@ -199,6 +219,7 @@ class AppConfig {
.slideshowDuration => copyWith(slideshow: slideshow.copyWith(duration: value as int)),
.slideshowLook => copyWith(slideshow: slideshow.copyWith(look: value as SlideshowLook)),
.slideshowDirection => copyWith(slideshow: slideshow.copyWith(direction: value as SlideshowDirection)),
.featureMessageSeenVersion => copyWith(featureMessage: featureMessage.copyWith(seenVersion: value as int)),
};
}
}
@@ -0,0 +1,18 @@
class FeatureMessageConfig {
final int seenVersion;
const FeatureMessageConfig({this.seenVersion = 0});
FeatureMessageConfig copyWith({int? seenVersion}) =>
FeatureMessageConfig(seenVersion: seenVersion ?? this.seenVersion);
@override
bool operator ==(Object other) =>
identical(this, other) || (other is FeatureMessageConfig && other.seenVersion == seenVersion);
@override
int get hashCode => seenVersion.hashCode;
@override
String toString() => 'FeatureMessageConfig(seenVersion: $seenVersion)';
}
@@ -0,0 +1,49 @@
class FeatureHighlight {
final String image;
final String titleKey;
final String bodyKey;
const FeatureHighlight({required this.image, required this.titleKey, required this.bodyKey});
}
const int featureMessageHighlightVersion = 1;
const String featureMessageReleaseLabel = '3.0.0';
const List<FeatureHighlight> featureMessageHighlights = [
FeatureHighlight(
image: 'assets/feature_message/share_quality.webp',
titleKey: 'feature_message_share_quality_title',
bodyKey: 'feature_message_share_quality_body',
),
FeatureHighlight(
image: 'assets/feature_message/slideshow.webp',
titleKey: 'feature_message_slideshow_title',
bodyKey: 'feature_message_slideshow_body',
),
FeatureHighlight(
image: 'assets/feature_message/recently_added.webp',
titleKey: 'feature_message_recently_added_title',
bodyKey: 'feature_message_recently_added_body',
),
FeatureHighlight(
image: 'assets/feature_message/non_destructive_editing.webp',
titleKey: 'feature_message_non_destructive_editing_title',
bodyKey: 'feature_message_non_destructive_editing_body',
),
FeatureHighlight(
image: 'assets/feature_message/ocr.webp',
titleKey: 'feature_message_ocr_title',
bodyKey: 'feature_message_ocr_body',
),
FeatureHighlight(
image: 'assets/feature_message/open_in_immich.webp',
titleKey: 'feature_message_open_in_immich_title',
bodyKey: 'feature_message_open_in_immich_body',
),
FeatureHighlight(
image: 'assets/feature_message/upload_to_album.webp',
titleKey: 'feature_message_upload_to_album_title',
bodyKey: 'feature_message_upload_to_album_body',
),
];
+4 -1
View File
@@ -73,7 +73,10 @@ enum SettingsKey<T> {
slideshowRepeat<bool>(),
slideshowDuration<int>(),
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)),
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values));
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values)),
// Feature message
featureMessageSeenVersion<int>();
final _SettingsCodec<T>? _codecOverride;
@@ -0,0 +1,17 @@
import 'package:immich_mobile/domain/models/feature_message.model.dart';
import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
class FeatureMessageService {
final SettingsRepository _settingsRepository;
const FeatureMessageService(this._settingsRepository);
bool shouldShow() {
final seen = _settingsRepository.appConfig.read(SettingsKey.featureMessageSeenVersion);
return featureMessageHighlights.isNotEmpty && featureMessageHighlightVersion > seen;
}
Future<void> markSeen() =>
_settingsRepository.write(SettingsKey.featureMessageSeenVersion, featureMessageHighlightVersion);
}
@@ -2,7 +2,9 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:immich_mobile/domain/models/feature_message.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/feature_message/feature_message_dialog.widget.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/settings/advanced_settings.dart';
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
@@ -87,6 +89,14 @@ class _MobileLayout extends StatelessWidget {
],
)
.toList();
settings.add(
SettingsCard(
icon: Icons.auto_awesome_outlined,
title: 'whats_new'.tr(),
subtitle: 'feature_message_settings_subtitle'.tr(namedArgs: {'version': featureMessageReleaseLabel}),
onTap: () => showFeatureMessageDialog(context),
),
);
return ListView(padding: const EdgeInsets.only(top: 10.0, bottom: 60), children: [...settings]);
}
}
@@ -116,6 +126,13 @@ class _TabletLayout extends HookWidget {
),
),
),
SliverToBoxAdapter(
child: ListTile(
title: Text('whats_new'.tr()),
leading: const Icon(Icons.auto_awesome_outlined),
onTap: () => showFeatureMessageDialog(context),
),
),
],
),
),
@@ -3,14 +3,37 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/presentation/widgets/feature_message/feature_message_dialog.widget.dart';
import 'package:immich_mobile/providers/feature_message.provider.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
@RoutePage()
class MainTimelinePage extends ConsumerWidget {
class MainTimelinePage extends ConsumerStatefulWidget {
const MainTimelinePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<MainTimelinePage> createState() => _MainTimelinePageState();
}
class _MainTimelinePageState extends ConsumerState<MainTimelinePage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || ref.read(featureMessageCheckedProvider)) {
return;
}
ref.read(featureMessageCheckedProvider.notifier).state = true;
final service = ref.read(featureMessageServiceProvider);
// if (service.shouldShow()) {
showFeatureMessageDialog(context).then((_) => service.markSeen());
// }
});
}
@override
Widget build(BuildContext context) {
final hasMemories = ref.watch(driftMemoryFutureProvider.select((state) => state.value?.isNotEmpty ?? false));
return Timeline(
topSliverWidget: const SliverToBoxAdapter(child: DriftMemoryLane()),
@@ -0,0 +1,201 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/feature_message.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
Future<void> showFeatureMessageDialog(BuildContext context) {
return showGeneralDialog<void>(
context: context,
useRootNavigator: true,
barrierDismissible: true,
barrierLabel: 'whats_new'.tr(),
barrierColor: Colors.black.withValues(alpha: 0.55),
transitionDuration: const Duration(milliseconds: 280),
pageBuilder: (_, __, ___) => const _FeatureMessageDialog(),
transitionBuilder: (_, animation, __, child) {
final curved = CurvedAnimation(parent: animation, curve: Curves.easeOutCubic, reverseCurve: Curves.easeInCubic);
return FadeTransition(
opacity: animation,
child: ScaleTransition(scale: Tween<double>(begin: 0.94, end: 1.0).animate(curved), child: child),
);
},
);
}
class _FeatureMessageDialog extends StatefulWidget {
const _FeatureMessageDialog();
@override
State<_FeatureMessageDialog> createState() => _FeatureMessageDialogState();
}
class _FeatureMessageDialogState extends State<_FeatureMessageDialog> {
final PageController _controller = PageController();
int _index = 0;
bool get _isLast => _index >= featureMessageHighlights.length - 1;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _advance() {
if (_isLast) {
Navigator.of(context).pop();
return;
}
_controller.nextPage(duration: const Duration(milliseconds: 320), curve: Curves.easeOutCubic);
}
@override
Widget build(BuildContext context) {
return Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 64),
clipBehavior: Clip.antiAlias,
backgroundColor: context.colorScheme.surface,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))),
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: context.height * 0.9, maxWidth: 480),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 20, 24, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('whats_new'.tr(), style: context.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700)),
const SizedBox(height: 2),
Text(
'feature_message_version'.tr(namedArgs: {'version': featureMessageReleaseLabel}),
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant),
),
],
),
),
Expanded(
child: PageView.builder(
controller: _controller,
itemCount: featureMessageHighlights.length,
onPageChanged: (i) => setState(() => _index = i),
itemBuilder: (_, index) => _FeaturePage(highlight: featureMessageHighlights[index]),
),
),
const SizedBox(height: 8),
_PageDots(controller: _controller, index: _index, count: featureMessageHighlights.length),
Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: _advance,
style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 14)),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Text(_isLast ? 'ok'.tr() : 'next'.tr(), key: ValueKey(_isLast)),
),
),
),
),
],
),
),
);
}
}
class _FeaturePage extends StatelessWidget {
final FeatureHighlight highlight;
const _FeaturePage({required this.highlight});
@override
Widget build(BuildContext context) {
final scheme = context.colorScheme;
return SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 0),
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: ColoredBox(
color: scheme.surfaceContainerHighest,
child: SizedBox(
width: double.infinity,
height: 300,
child: Image.asset(
highlight.image,
fit: BoxFit.contain,
errorBuilder: (context, _, __) =>
Center(child: Icon(Icons.auto_awesome_outlined, color: scheme.onSurfaceVariant, size: 56)),
),
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 18, 24, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
highlight.titleKey.tr(),
style: context.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
Text(
highlight.bodyKey.tr(),
style: context.textTheme.bodyMedium?.copyWith(color: scheme.onSurfaceVariant, height: 1.4),
),
],
),
),
],
),
);
}
}
class _PageDots extends StatelessWidget {
final PageController controller;
final int index;
final int count;
const _PageDots({required this.controller, required this.index, required this.count});
@override
Widget build(BuildContext context) {
final primary = context.primaryColor;
return AnimatedBuilder(
animation: controller,
builder: (context, _) {
final page = controller.hasClients ? (controller.page ?? index.toDouble()) : index.toDouble();
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(count, (i) {
final activeness = (1 - (page - i).abs()).clamp(0.0, 1.0);
return AnimatedContainer(
duration: const Duration(milliseconds: 150),
margin: const EdgeInsets.symmetric(horizontal: 3),
height: 7,
width: 7 + 16 * activeness,
decoration: BoxDecoration(
color: Color.lerp(context.colorScheme.surfaceContainerHighest, primary, activeness),
borderRadius: BorderRadius.circular(8),
),
);
}),
);
},
);
}
}
@@ -0,0 +1,101 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/feature_message.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
Future<void> showFeatureMessageSheet(BuildContext context) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
useRootNavigator: true,
builder: (_) => const _FeatureMessageSheet(),
);
}
class _FeatureMessageSheet extends StatelessWidget {
const _FeatureMessageSheet();
@override
Widget build(BuildContext context) {
return BaseBottomSheet(
actions: const [],
resizeOnScroll: false,
expand: false,
initialChildSize: 0.6,
minChildSize: 0.4,
maxChildSize: 0.9,
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 4, 24, 16),
child: Text(
'whats_new'.tr(),
style: context.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w600),
),
),
),
SliverList.separated(
itemCount: featureMessageHighlights.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (_, index) => _HighlightCard(highlight: featureMessageHighlights[index]),
),
const SliverToBoxAdapter(child: SizedBox(height: 16)),
],
footer: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 8, 24, 16),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => Navigator.of(context).pop(),
style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 14)),
child: Text('feature_message_got_it'.tr()),
),
),
),
),
);
}
}
class _HighlightCard extends StatelessWidget {
final FeatureHighlight highlight;
const _HighlightCard({required this.highlight});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Image.asset(
highlight.image,
fit: BoxFit.cover,
errorBuilder: (context, _, __) => ColoredBox(
color: context.colorScheme.surfaceContainerHighest,
child: Icon(Icons.image_outlined, color: context.colorScheme.onSurfaceVariant, size: 48),
),
),
),
),
const SizedBox(height: 12),
Text(highlight.titleKey.tr(), style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text(
highlight.bodyKey.tr(),
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant),
),
],
),
);
}
}
@@ -0,0 +1,9 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/feature_message.service.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
final featureMessageServiceProvider = Provider<FeatureMessageService>(
(ref) => FeatureMessageService(ref.read(settingsProvider)),
);
final featureMessageCheckedProvider = StateProvider<bool>((ref) => false);
@@ -18,6 +18,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/feature_message.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/oauth.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -262,6 +263,7 @@ class LoginForm extends HookConsumerWidget {
}
unawaited(handleSyncFlow());
ref.read(websocketProvider.notifier).connect();
unawaited(ref.read(featureMessageServiceProvider).markSeen());
unawaited(context.router.replaceAll([const TabShellRoute()]));
return;
}
@@ -349,6 +351,7 @@ class LoginForm extends HookConsumerWidget {
await getManageMediaPermission();
}
unawaited(handleSyncFlow());
unawaited(ref.read(featureMessageServiceProvider).markSeen());
unawaited(context.router.replaceAll([const TabShellRoute()]));
return;
}
@@ -8,13 +8,15 @@ class SettingsCard extends StatelessWidget {
required this.icon,
required this.title,
required this.subtitle,
required this.settingRoute,
this.settingRoute,
this.onTap,
});
final IconData icon;
final String title;
final String subtitle;
final PageRouteInfo settingRoute;
final PageRouteInfo? settingRoute;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
@@ -38,7 +40,7 @@ class SettingsCard extends StatelessWidget {
),
title: Text(title, style: context.textTheme.titleMedium!.copyWith(color: context.primaryColor)),
subtitle: Text(subtitle, style: context.textTheme.bodyMedium),
onTap: () => context.pushRoute(settingRoute),
onTap: onTap ?? (settingRoute != null ? () => context.pushRoute(settingRoute!) : null),
),
),
);
+1
View File
@@ -119,6 +119,7 @@ flutter:
uses-material-design: true
assets:
- assets/
- assets/feature_message/
fonts:
- family: GoogleSans
fonts:
+2 -2
View File
@@ -38,8 +38,8 @@
</p>
> [!WARNING]
> ⚠️ Always follow [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan for your precious photos and videos!
>
> ⚠️ Değerli fotoğraflarınız ve videolarınız için daima [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) yedekleme planını uygulayın!
>
> [!NOTE]
> Kurulum dahil olmak üzere resmi belgeleri https://immich.app/ adresinde bulabilirsiniz.
@@ -129,6 +129,7 @@ from
and "integrity_report"."type" = $1
where
"asset"."deletedAt" is null
and "asset"."isExternal" = false
and "integrity_report"."createdAt" >= $2
and "integrity_report"."createdAt" <= $3
order by
@@ -177,6 +177,7 @@ export class IntegrityRepository {
'asset.id as assetId',
'integrity_report.id as reportId',
])
.where('asset.isExternal', '=', sql.lit(false))
.$if(startMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '>=', startMarker!))
.$if(endMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '<=', endMarker!))
.orderBy('integrity_report.createdAt', 'asc')
@@ -2939,6 +2939,8 @@ describe(MediaService.name, () => {
'7',
'-global_quality:v',
'23',
'-b:v',
'6897k',
'-maxrate',
'10000k',
'-bufsize',
+6
View File
@@ -788,6 +788,12 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
const options = [`-${this.useCQP() ? 'q:v' : 'global_quality:v'}`, `${this.config.crf}`];
const bitrates = this.getBitrateDistribution();
if (bitrates.max > 0) {
// Workaround for https://github.com/immich-app/immich/issues/29220, to be revisited
// QSV seems to ignore -maxrate without -b:v
// -b:v alongside global_quality uses QVBR
if (!this.useCQP()) {
options.push('-b:v', `${bitrates.target}${bitrates.unit}`);
}
options.push('-maxrate', `${bitrates.max}${bitrates.unit}`, '-bufsize', `${bitrates.max * 2}${bitrates.unit}`);
}
return options;
@@ -686,6 +686,22 @@ describe(IntegrityService.name, () => {
nextCursor: undefined,
});
});
it('should skip external library files', async () => {
const { sut, ctx } = setup();
const job = ctx.getMock(JobRepository);
job.queue.mockResolvedValue(void 0);
const { user } = await ctx.newUser();
await ctx.newAsset({ ownerId: user.id, isExternal: true });
await sut.handleChecksumFiles({ refreshOnly: false });
await expect(
ctx.get(IntegrityRepository).getIntegrityReport({ limit: 100 }, IntegrityReport.ChecksumFail),
).resolves.toEqual({ items: [], nextCursor: undefined });
});
});
describe('handleChecksumRefresh', () => {
@@ -324,6 +324,18 @@
shortcut: { key: ' ' },
onShortcut: () => (videoPlayer?.paused ? videoPlayer?.play() : videoPlayer?.pause()),
},
{
shortcut: { shift: true, key: 'ArrowLeft' },
onShortcut: () =>
videoPlayer ? (videoPlayer.currentTime = Math.max(videoPlayer.currentTime - 0.4, 0)) : undefined,
},
{
shortcut: { shift: true, key: 'ArrowRight' },
onShortcut: () =>
videoPlayer
? (videoPlayer.currentTime = Math.min(videoPlayer.currentTime + 0.4, videoPlayer.duration))
: undefined,
},
]}
/>
+3 -2
View File
@@ -24,7 +24,8 @@ class FaceManager {
});
readonly people = $derived.by(() => {
const people = new SvelteMap<string, PersonResponseDto>();
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const people = new Map<string, PersonResponseDto>();
for (const face of this.data) {
if (face.person) {
@@ -32,7 +33,7 @@ class FaceManager {
}
}
return people.values();
return Array.from(people.values());
});
readonly facesByPersonId = $derived.by(() => {