chore: bump dart sdk to 3.8 (#20355)

* chore: bump dart sdk to 3.8

* chore: make build

* make pigeon

* chore: format files

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong
2025-07-29 00:34:03 +05:30
committed by GitHub
parent 9b3718120b
commit e52b9d15b5
643 changed files with 32561 additions and 35292 deletions

View File

@@ -34,23 +34,15 @@ final _features = [
final assets = await ref.read(remoteAssetRepositoryProvider).getSome(user.id);
final selectedAssets = await ctx.pushRoute<Set<BaseAsset>>(
DriftAssetSelectionTimelineRoute(
lockedSelectionAssets: assets.toSet(),
),
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: assets.toSet()),
);
DLog.log(
"Selected ${selectedAssets?.length ?? 0} assets",
);
DLog.log("Selected ${selectedAssets?.length ?? 0} assets");
return Future.value();
},
),
_Feature(
name: '',
icon: Icons.vertical_align_center_sharp,
onTap: (_, __) => Future.value(),
),
_Feature(name: '', icon: Icons.vertical_align_center_sharp, onTap: (_, __) => Future.value()),
_Feature(
name: 'Sync Local',
icon: Icons.photo_album_rounded,
@@ -76,11 +68,7 @@ final _features = [
icon: Icons.save_rounded,
onTap: (_, ref) => ref.read(driftProvider).customStatement("pragma wal_checkpoint(truncate)"),
),
_Feature(
name: '',
icon: Icons.vertical_align_center_sharp,
onTap: (_, __) => Future.value(),
),
_Feature(name: '', icon: Icons.vertical_align_center_sharp, onTap: (_, __) => Future.value()),
_Feature(
name: 'Clear Delta Checkpoint',
icon: Icons.delete_rounded,
@@ -88,10 +76,7 @@ final _features = [
),
_Feature(
name: 'Clear Local Data',
style: const TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold,
),
style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
icon: Icons.delete_forever_rounded,
onTap: (_, ref) async {
final db = ref.read(driftProvider);
@@ -102,10 +87,7 @@ final _features = [
),
_Feature(
name: 'Clear Remote Data',
style: const TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold,
),
style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
icon: Icons.delete_sweep_rounded,
onTap: (_, ref) async {
final db = ref.read(driftProvider);
@@ -123,29 +105,20 @@ final _features = [
),
_Feature(
name: 'Local Media Summary',
style: const TextStyle(
color: Colors.indigo,
fontWeight: FontWeight.bold,
),
style: const TextStyle(color: Colors.indigo, fontWeight: FontWeight.bold),
icon: Icons.table_chart_rounded,
onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()),
),
_Feature(
name: 'Remote Media Summary',
style: const TextStyle(
color: Colors.indigo,
fontWeight: FontWeight.bold,
),
style: const TextStyle(color: Colors.indigo, fontWeight: FontWeight.bold),
icon: Icons.summarize_rounded,
onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()),
),
_Feature(
name: 'Reset Sqlite',
icon: Icons.table_view_rounded,
style: const TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
onTap: (_, ref) async {
final drift = ref.read(driftProvider);
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
@@ -165,10 +138,7 @@ class FeatInDevPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Features in Development'),
centerTitle: true,
),
appBar: AppBar(title: const Text('Features in Development'), centerTitle: true),
body: Column(
children: [
Flexible(
@@ -178,10 +148,7 @@ class FeatInDevPage extends StatelessWidget {
final feat = _features[index];
return Consumer(
builder: (ctx, ref, _) => ListTile(
title: Text(
feat.name,
style: feat.style,
),
title: Text(feat.name, style: feat.style),
trailing: Icon(feat.icon),
visualDensity: VisualDensity.compact,
onTap: () => unawaited(feat.onTap(ctx, ref)),
@@ -200,12 +167,7 @@ class FeatInDevPage extends StatelessWidget {
}
class _Feature {
const _Feature({
required this.name,
required this.icon,
required this.onTap,
this.style,
});
const _Feature({required this.name, required this.icon, required this.onTap, this.style});
final String name;
final IconData icon;
@@ -244,18 +206,11 @@ class _DevLogs extends StatelessWidget {
return ListTile(
title: Text(
logMessage.message,
style: TextStyle(
color: ctx.colorScheme.onSurface,
fontSize: 14.0,
overflow: TextOverflow.ellipsis,
),
style: TextStyle(color: ctx.colorScheme.onSurface, fontSize: 14.0, overflow: TextOverflow.ellipsis),
),
subtitle: Text(
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}",
style: TextStyle(
color: ctx.colorScheme.onSurfaceSecondary,
fontSize: 12.0,
),
style: TextStyle(color: ctx.colorScheme.onSurfaceSecondary, fontSize: 12.0),
),
dense: true,
visualDensity: VisualDensity.compact,

View File

@@ -21,12 +21,7 @@ class _Summary extends StatelessWidget {
final Future<int> countFuture;
final void Function()? onTap;
const _Summary({
required this.name,
required this.countFuture,
this.leading,
this.onTap,
});
const _Summary({required this.name, required this.countFuture, this.leading, this.onTap});
@override
Widget build(BuildContext context) {
@@ -40,31 +35,17 @@ class _Summary extends StatelessWidget {
} else if (snapshot.hasError) {
subtitle = const Icon(Icons.error_rounded);
} else {
subtitle = Text(
'${snapshot.data ?? 0}',
style: ctx.textTheme.bodyLarge,
);
subtitle = Text('${snapshot.data ?? 0}', style: ctx.textTheme.bodyLarge);
}
return ListTile(
leading: leading,
title: Text(name),
trailing: subtitle,
onTap: onTap,
);
return ListTile(leading: leading, title: Text(name), trailing: subtitle, onTap: onTap);
},
);
}
}
final _localStats = [
_Stat(
name: 'Local Assets',
load: (db) => db.managers.localAssetEntity.count(),
),
_Stat(
name: 'Local Albums',
load: (db) => db.managers.localAlbumEntity.count(),
),
_Stat(name: 'Local Assets', load: (db) => db.managers.localAssetEntity.count()),
_Stat(name: 'Local Albums', load: (db) => db.managers.localAlbumEntity.count()),
];
@RoutePage()
@@ -97,10 +78,7 @@ class LocalMediaSummaryPage extends StatelessWidget {
const Divider(),
Padding(
padding: const EdgeInsets.only(left: 15),
child: Text(
"Album summary",
style: ctx.textTheme.titleMedium,
),
child: Text("Album summary", style: ctx.textTheme.titleMedium),
),
],
),
@@ -117,15 +95,14 @@ class LocalMediaSummaryPage extends StatelessWidget {
return SliverList.builder(
itemBuilder: (_, index) {
final album = albums[index];
final countFuture =
db.managers.localAlbumAssetEntity.filter((f) => f.albumId.id.equals(album.id)).count();
final countFuture = db.managers.localAlbumAssetEntity
.filter((f) => f.albumId.id.equals(album.id))
.count();
return _Summary(
leading: const Icon(Icons.photo_album_rounded),
name: album.name,
countFuture: countFuture,
onTap: () => context.router.push(
LocalTimelineRoute(album: album),
),
onTap: () => context.router.push(LocalTimelineRoute(album: album)),
);
},
itemCount: albums.length,
@@ -141,38 +118,14 @@ class LocalMediaSummaryPage extends StatelessWidget {
}
final _remoteStats = [
_Stat(
name: 'Remote Assets',
load: (db) => db.managers.remoteAssetEntity.count(),
),
_Stat(
name: 'Exif Entities',
load: (db) => db.managers.remoteExifEntity.count(),
),
_Stat(
name: 'Remote Albums',
load: (db) => db.managers.remoteAlbumEntity.count(),
),
_Stat(
name: 'Memories',
load: (db) => db.managers.memoryEntity.count(),
),
_Stat(
name: 'Memories Assets',
load: (db) => db.managers.memoryAssetEntity.count(),
),
_Stat(
name: 'Stacks',
load: (db) => db.managers.stackEntity.count(),
),
_Stat(
name: 'People',
load: (db) => db.managers.personEntity.count(),
),
_Stat(
name: 'AssetFaces',
load: (db) => db.managers.assetFaceEntity.count(),
),
_Stat(name: 'Remote Assets', load: (db) => db.managers.remoteAssetEntity.count()),
_Stat(name: 'Exif Entities', load: (db) => db.managers.remoteExifEntity.count()),
_Stat(name: 'Remote Albums', load: (db) => db.managers.remoteAlbumEntity.count()),
_Stat(name: 'Memories', load: (db) => db.managers.memoryEntity.count()),
_Stat(name: 'Memories Assets', load: (db) => db.managers.memoryAssetEntity.count()),
_Stat(name: 'Stacks', load: (db) => db.managers.stackEntity.count()),
_Stat(name: 'People', load: (db) => db.managers.personEntity.count()),
_Stat(name: 'AssetFaces', load: (db) => db.managers.assetFaceEntity.count()),
];
@RoutePage()
@@ -205,10 +158,7 @@ class RemoteMediaSummaryPage extends StatelessWidget {
const Divider(),
Padding(
padding: const EdgeInsets.only(left: 15),
child: Text(
"Album summary",
style: ctx.textTheme.titleMedium,
),
child: Text("Album summary", style: ctx.textTheme.titleMedium),
),
],
),
@@ -225,15 +175,14 @@ class RemoteMediaSummaryPage extends StatelessWidget {
return SliverList.builder(
itemBuilder: (_, index) {
final album = albums[index];
final countFuture =
db.managers.remoteAlbumAssetEntity.filter((f) => f.albumId.id.equals(album.id)).count();
final countFuture = db.managers.remoteAlbumAssetEntity
.filter((f) => f.albumId.id.equals(album.id))
.count();
return _Summary(
leading: const Icon(Icons.photo_album_rounded),
name: album.name,
countFuture: countFuture,
onTap: () => context.router.push(
RemoteAlbumRoute(album: album),
),
onTap: () => context.router.push(RemoteAlbumRoute(album: album)),
);
},
itemCount: albums.length,

View File

@@ -35,13 +35,8 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
pinned: true,
actions: [
IconButton(
icon: const Icon(
Icons.add_rounded,
size: 28,
),
onPressed: () => context.pushRoute(
const DriftCreateAlbumRoute(),
),
icon: const Icon(Icons.add_rounded, size: 28),
onPressed: () => context.pushRoute(const DriftCreateAlbumRoute()),
),
],
showUploadButton: false,
@@ -49,9 +44,7 @@ class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
AlbumSelector(
onAlbumSelected: (album) {
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
context.router.push(
RemoteAlbumRoute(album: album),
);
context.router.push(RemoteAlbumRoute(album: album));
},
),
],

View File

@@ -16,18 +16,16 @@ class DriftArchivePage extends StatelessWidget {
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access archive');
}
timelineServiceProvider.overrideWith((ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access archive');
}
final timelineService = ref.watch(timelineFactoryProvider).archive(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
final timelineService = ref.watch(timelineFactoryProvider).archive(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
appBar: MesmerizingSliverAppBar(

View File

@@ -10,10 +10,7 @@ import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftAssetSelectionTimelinePage extends ConsumerWidget {
final Set<BaseAsset> lockedSelectionAssets;
const DriftAssetSelectionTimelinePage({
super.key,
this.lockedSelectionAssets = const {},
});
const DriftAssetSelectionTimelinePage({super.key, this.lockedSelectionAssets = const {}});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -21,27 +18,19 @@ class DriftAssetSelectionTimelinePage extends ConsumerWidget {
overrides: [
multiSelectProvider.overrideWith(
() => MultiSelectNotifier(
MultiSelectState(
selectedAssets: {},
lockedSelectionAssets: lockedSelectionAssets,
forceEnable: true,
),
MultiSelectState(selectedAssets: {}, lockedSelectionAssets: lockedSelectionAssets, forceEnable: true),
),
),
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception(
'User must be logged in to access asset selection timeline',
);
}
timelineServiceProvider.overrideWith((ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access asset selection timeline');
}
final timelineService = ref.watch(timelineFactoryProvider).remoteAssets(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
final timelineService = ref.watch(timelineFactoryProvider).remoteAssets(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: const Timeline(),
);

View File

@@ -70,12 +70,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
Widget _buildContent() {
if (selectedAssets.isEmpty) {
return SliverList(
delegate: SliverChildListDelegate([
_buildEmptyState(),
_buildSelectPhotosButton(),
]),
);
return SliverList(delegate: SliverChildListDelegate([_buildEmptyState(), _buildSelectPhotosButton()]));
} else {
return _buildSelectedImageGrid();
}
@@ -84,10 +79,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
Widget _buildEmptyState() {
return Padding(
padding: const EdgeInsets.only(top: 0, left: 18),
child: Text(
'create_shared_album_page_share_add_assets',
style: context.textTheme.labelLarge,
).t(),
child: Text('create_shared_album_page_share_add_assets', style: context.textTheme.labelLarge).t(),
);
}
@@ -97,27 +89,17 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
child: FilledButton.icon(
style: FilledButton.styleFrom(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(
vertical: 24.0,
horizontal: 16.0,
),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10.0))),
backgroundColor: context.colorScheme.surfaceContainerHigh,
),
onPressed: onSelectPhotos,
icon: Icon(Icons.add_rounded, color: context.primaryColor),
label: Padding(
padding: const EdgeInsets.only(
left: 8.0,
),
padding: const EdgeInsets.only(left: 8.0),
child: Text(
'create_shared_album_page_share_select_photos',
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor),
).t(),
),
),
@@ -133,16 +115,13 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
crossAxisSpacing: 1.0,
mainAxisSpacing: 1.0,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
final asset = selectedAssets.elementAt(index);
return GestureDetector(
onTap: onBackgroundTapped,
child: Thumbnail(asset: asset),
);
},
childCount: selectedAssets.length,
),
delegate: SliverChildBuilderDelegate((context, index) {
final asset = selectedAssets.elementAt(index);
return GestureDetector(
onTap: onBackgroundTapped,
child: Thumbnail(asset: asset),
);
}, childCount: selectedAssets.length),
),
);
}
@@ -162,9 +141,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
Future<void> onSelectPhotos() async {
final assets = await context.pushRoute<Set<BaseAsset>>(
DriftAssetSelectionTimelineRoute(
lockedSelectionAssets: selectedAssets,
),
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: selectedAssets),
);
if (assets == null || assets.isEmpty) {
@@ -183,16 +160,15 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
if (title.isEmpty) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('create_album_title_required'.t()),
backgroundColor: context.colorScheme.error,
),
SnackBar(content: Text('create_album_title_required'.t()), backgroundColor: context.colorScheme.error),
);
}
return;
}
final album = await ref.watch(remoteAlbumProvider.notifier).createAlbum(
final album = await ref
.watch(remoteAlbumProvider.notifier)
.createAlbum(
title: title,
description: albumDescriptionController.text.trim(),
assetIds: selectedAssets.map((asset) {
@@ -203,18 +179,13 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
if (album != null) {
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
context.replaceRoute(
RemoteAlbumRoute(album: album),
);
context.replaceRoute(RemoteAlbumRoute(album: album));
}
}
Widget buildTitleInputField() {
return Padding(
padding: const EdgeInsets.only(
right: 10.0,
left: 10.0,
),
padding: const EdgeInsets.only(right: 10.0, left: 10.0),
child: _AlbumTitleTextField(
focusNode: albumTitleTextFieldFocusNode,
textController: albumTitleController,
@@ -230,11 +201,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
Widget buildDescriptionInputField() {
return Padding(
padding: const EdgeInsets.only(
right: 10.0,
left: 10.0,
top: 8,
),
padding: const EdgeInsets.only(right: 10.0, left: 10.0, top: 8),
child: _AlbumViewerEditableDescription(
textController: albumDescriptionController,
focusNode: albumDescriptionTextFieldFocusNode,
@@ -244,11 +211,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
Widget buildControlButton() {
return Padding(
padding: const EdgeInsets.only(
left: 12.0,
top: 8.0,
bottom: 8.0,
),
padding: const EdgeInsets.only(left: 12.0, top: 8.0, bottom: 8.0),
child: SizedBox(
height: 42.0,
child: ListView(
@@ -272,10 +235,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
elevation: 0,
centerTitle: false,
backgroundColor: context.scaffoldBackgroundColor,
leading: IconButton(
onPressed: () => context.maybePop(),
icon: const Icon(Icons.close_rounded),
),
leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.close_rounded)),
title: const Text('create_album').t(),
actions: [
TextButton(
@@ -292,12 +252,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
),
body: GestureDetector(
onTap: onBackgroundTapped,
child: CustomScrollView(
slivers: [
_buildSliverAppBar(),
_buildContent(),
],
),
child: CustomScrollView(slivers: [_buildSliverAppBar(), _buildContent()]),
),
);
}
@@ -341,11 +296,7 @@ class _AlbumTitleTextFieldState extends State<_AlbumTitleTextField> {
Widget build(BuildContext context) {
return TextField(
focusNode: widget.focusNode,
style: TextStyle(
fontSize: 28.0,
color: context.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
style: TextStyle(fontSize: 28.0, color: context.colorScheme.onSurface, fontWeight: FontWeight.bold),
controller: widget.textController,
onTap: () {
if (widget.textController.text == 'create_album_page_untitled'.t(context: context)) {
@@ -353,35 +304,23 @@ class _AlbumTitleTextFieldState extends State<_AlbumTitleTextField> {
}
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 16.0,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
suffixIcon: widget.textController.text.isNotEmpty && widget.isFocus
? IconButton(
onPressed: () {
widget.textController.clear();
},
icon: Icon(
Icons.cancel_rounded,
color: context.primaryColor,
),
icon: Icon(Icons.cancel_rounded, color: context.primaryColor),
splashRadius: 10.0,
)
: null,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(
Radius.circular(16.0),
),
borderRadius: BorderRadius.all(Radius.circular(16.0)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: context.primaryColor.withValues(alpha: 0.3),
),
borderRadius: const BorderRadius.all(
Radius.circular(16.0),
),
borderSide: BorderSide(color: context.primaryColor.withValues(alpha: 0.3)),
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
),
hintText: 'add_a_title'.t(),
hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(
@@ -398,10 +337,7 @@ class _AlbumTitleTextFieldState extends State<_AlbumTitleTextField> {
}
class _AlbumViewerEditableDescription extends StatefulWidget {
const _AlbumViewerEditableDescription({
required this.textController,
required this.focusNode,
});
const _AlbumViewerEditableDescription({required this.textController, required this.focusNode});
final TextEditingController textController;
final FocusNode focusNode;
@@ -448,37 +384,23 @@ class _AlbumViewerEditableDescriptionState extends State<_AlbumViewerEditableDes
minLines: 1,
controller: widget.textController,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 16.0,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
suffixIcon: widget.focusNode.hasFocus && widget.textController.text.isNotEmpty
? IconButton(
onPressed: () {
widget.textController.clear();
},
icon: Icon(
Icons.cancel_rounded,
color: context.primaryColor,
),
icon: Icon(Icons.cancel_rounded, color: context.primaryColor),
splashRadius: 10.0,
)
: null,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: context.colorScheme.outline.withValues(alpha: 0.3),
),
borderRadius: const BorderRadius.all(
Radius.circular(16.0),
),
borderSide: BorderSide(color: context.colorScheme.outline.withValues(alpha: 0.3)),
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: context.primaryColor.withValues(alpha: 0.3),
),
borderRadius: const BorderRadius.all(
Radius.circular(16.0),
),
borderSide: BorderSide(color: context.primaryColor.withValues(alpha: 0.3)),
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
),
hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith(
fontSize: 16.0,

View File

@@ -16,18 +16,16 @@ class DriftFavoritePage extends StatelessWidget {
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access favorite');
}
timelineServiceProvider.overrideWith((ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access favorite');
}
final timelineService = ref.watch(timelineFactoryProvider).favorite(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
final timelineService = ref.watch(timelineFactoryProvider).favorite(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
appBar: MesmerizingSliverAppBar(

View File

@@ -27,12 +27,7 @@ class DriftLibraryPage extends ConsumerWidget {
return const Scaffold(
body: CustomScrollView(
slivers: [
ImmichSliverAppBar(
snap: false,
floating: false,
pinned: true,
showUploadButton: false,
),
ImmichSliverAppBar(snap: false, floating: false, pinned: true, showUploadButton: false),
_ActionButtonGrid(),
_CollectionCards(),
_QuickAccessButtonList(),
@@ -47,9 +42,7 @@ class _ActionButtonGrid extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
return SliverPadding(
padding: const EdgeInsets.only(left: 16, top: 16, right: 16, bottom: 12),
@@ -97,11 +90,7 @@ class _ActionButtonGrid extends ConsumerWidget {
}
class _ActionButton extends StatelessWidget {
const _ActionButton({
required this.icon,
required this.onTap,
required this.label,
});
const _ActionButton({required this.icon, required this.onTap, required this.label});
final IconData icon;
final VoidCallback onTap;
@@ -114,13 +103,7 @@ class _ActionButton extends StatelessWidget {
onPressed: onTap,
label: Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
label,
style: TextStyle(
color: context.colorScheme.onSurface,
fontSize: 15,
),
),
child: Text(label, style: TextStyle(color: context.colorScheme.onSurface, fontSize: 15)),
),
style: FilledButton.styleFrom(
elevation: 0,
@@ -129,16 +112,10 @@ class _ActionButton extends StatelessWidget {
alignment: Alignment.centerLeft,
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(25)),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(10),
width: 1,
),
side: BorderSide(color: context.colorScheme.onSurface.withAlpha(10), width: 1),
),
),
icon: Icon(
icon,
color: context.primaryColor,
),
icon: Icon(icon, color: context.primaryColor),
),
);
}
@@ -155,11 +132,7 @@ class _CollectionCards extends StatelessWidget {
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
_PeopleCollectionCard(),
_PlacesCollectionCard(),
_LocalAlbumsCollectionCard(),
],
children: [_PeopleCollectionCard(), _PlacesCollectionCard(), _LocalAlbumsCollectionCard()],
),
),
);
@@ -188,22 +161,15 @@ class _PeopleCollectionCard extends ConsumerWidget {
height: size,
width: size,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
borderRadius: const BorderRadius.all(Radius.circular(20)),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(30),
context.colorScheme.primary.withAlpha(25),
],
colors: [context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(25)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: people.widgetWhen(
onLoading: () => const Center(
child: CircularProgressIndicator(),
),
onLoading: () => const Center(child: CircularProgressIndicator()),
onData: (people) {
return GridView.count(
crossAxisCount: 2,
@@ -253,9 +219,7 @@ class _PlacesCollectionCard extends StatelessWidget {
final size = context.width * widthFactor - 20.0;
return GestureDetector(
onTap: () => context.pushRoute(
DriftPlaceRoute(currentLocation: null),
),
onTap: () => context.pushRoute(DriftPlaceRoute(currentLocation: null)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -270,10 +234,7 @@ class _PlacesCollectionCard extends StatelessWidget {
child: IgnorePointer(
child: MapThumbnail(
zoom: 8,
centre: const LatLng(
21.44950,
-157.91959,
),
centre: const LatLng(21.44950, -157.91959),
showAttribution: false,
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
),
@@ -323,10 +284,7 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(30),
context.colorScheme.primary.withAlpha(25),
],
colors: [context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(25)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
@@ -340,24 +298,14 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
children: albums.when(
data: (data) {
return data.take(4).map((album) {
return LocalAlbumThumbnail(
albumId: album.id,
);
return LocalAlbumThumbnail(albumId: album.id);
}).toList();
},
error: (error, _) {
return [
Center(
child: Text('Error: $error'),
),
];
return [Center(child: Text('Error: $error'))];
},
loading: () {
return [
const Center(
child: CircularProgressIndicator(),
),
];
return [const Center(child: CircularProgressIndicator())];
},
),
),
@@ -394,13 +342,8 @@ class _QuickAccessButtonList extends ConsumerWidget {
sliver: SliverToBoxAdapter(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: context.colorScheme.onSurface.withAlpha(10),
width: 1,
),
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1),
borderRadius: const BorderRadius.all(Radius.circular(20)),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(10),
@@ -425,41 +368,26 @@ class _QuickAccessButtonList extends ConsumerWidget {
bottomRight: Radius.circular(partners.isEmpty ? 20 : 0),
),
),
leading: const Icon(
Icons.folder_outlined,
size: 26,
),
leading: const Icon(Icons.folder_outlined, size: 26),
title: Text(
'folders'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
),
onTap: () => context.pushRoute(FolderRoute()),
),
ListTile(
leading: const Icon(
Icons.lock_outline_rounded,
size: 26,
),
leading: const Icon(Icons.lock_outline_rounded, size: 26),
title: Text(
'locked_folder'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
),
onTap: () => context.pushRoute(const DriftLockedFolderRoute()),
),
ListTile(
leading: const Icon(
Icons.group_outlined,
size: 26,
),
leading: const Icon(Icons.group_outlined, size: 26),
title: Text(
'partners'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
),
onTap: () => context.pushRoute(const DriftPartnerRoute()),
),
@@ -494,22 +422,13 @@ class _PartnerList extends StatelessWidget {
bottomRight: Radius.circular(isLastItem ? 20 : 0),
),
),
contentPadding: const EdgeInsets.only(
left: 12.0,
right: 18.0,
),
leading: PartnerUserAvatar(
partner: partner,
),
contentPadding: const EdgeInsets.only(left: 12.0, right: 18.0),
leading: PartnerUserAvatar(partner: partner),
title: const Text(
"partner_list_user_photos",
style: TextStyle(
fontWeight: FontWeight.w500,
),
style: TextStyle(fontWeight: FontWeight.w500),
).t(context: context, args: {'user': partner.name}),
onTap: () => context.pushRoute(
DriftPartnerDetailRoute(partner: partner),
),
onTap: () => context.pushRoute(DriftPartnerDetailRoute(partner: partner)),
);
},
);

View File

@@ -16,14 +16,7 @@ class DriftLocalAlbumsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Scaffold(
body: CustomScrollView(
slivers: [
LocalAlbumsSliverAppBar(),
_AlbumList(),
],
),
);
return const Scaffold(body: CustomScrollView(slivers: [LocalAlbumsSliverAppBar(), _AlbumList()]));
}
}
@@ -37,10 +30,7 @@ class _AlbumList extends ConsumerWidget {
return albums.when(
loading: () => const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
child: Padding(padding: EdgeInsets.all(20.0), child: CircularProgressIndicator()),
),
),
error: (error, stack) => SliverToBoxAdapter(
@@ -49,9 +39,7 @@ class _AlbumList extends ConsumerWidget {
padding: const EdgeInsets.all(20.0),
child: Text(
'Error loading albums: $error, stack: $stack',
style: TextStyle(
color: context.colorScheme.error,
),
style: TextStyle(color: context.colorScheme.error),
),
),
),
@@ -60,10 +48,7 @@ class _AlbumList extends ConsumerWidget {
if (albums.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Text('No albums found'),
),
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
),
);
}
@@ -77,30 +62,12 @@ class _AlbumList extends ConsumerWidget {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: LargeLeadingTile(
leadingPadding: const EdgeInsets.only(
right: 16,
),
leading: SizedBox(
width: 80,
height: 80,
child: LocalAlbumThumbnail(
albumId: album.id,
),
),
title: Text(
album.name,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
leadingPadding: const EdgeInsets.only(right: 16),
leading: SizedBox(width: 80, height: 80, child: LocalAlbumThumbnail(albumId: album.id)),
title: Text(album.name, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
subtitle: Text(
'items_count'.t(
context: context,
args: {'count': album.assetCount},
),
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
'items_count'.t(context: context, args: {'count': album.assetCount}),
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
onTap: () => context.pushRoute(LocalTimelineRoute(album: album)),
),

View File

@@ -45,27 +45,23 @@ class _DriftLockedFolderPageState extends ConsumerState<DriftLockedFolderPage> w
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access locked folder');
}
timelineServiceProvider.overrideWith((ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access locked folder');
}
final timelineService = ref.watch(timelineFactoryProvider).lockedFolder(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
final timelineService = ref.watch(timelineFactoryProvider).lockedFolder(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: _showOverlay
? const SizedBox()
: PopScope(
onPopInvokedWithResult: (didPop, _) => didPop ? ref.read(authProvider.notifier).lockPinCode() : null,
child: Timeline(
appBar: MesmerizingSliverAppBar(
title: 'locked_folder'.t(context: context),
),
appBar: MesmerizingSliverAppBar(title: 'locked_folder'.t(context: context)),
bottomSheet: const LockedFolderBottomSheet(),
),
),

View File

@@ -22,20 +22,14 @@ class DriftMemoryPage extends HookConsumerWidget {
final List<DriftMemory> memories;
final int memoryIndex;
const DriftMemoryPage({
required this.memories,
required this.memoryIndex,
super.key,
});
const DriftMemoryPage({required this.memories, required this.memoryIndex, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentMemory = useState(memories[memoryIndex]);
final currentAssetPage = useState(0);
final currentMemoryIndex = useState(memoryIndex);
final assetProgress = useState(
"${currentAssetPage.value + 1}|${currentMemory.value.assets.length}",
);
final assetProgress = useState("${currentAssetPage.value + 1}|${currentMemory.value.assets.length}");
const bgColor = Colors.black;
final currentAsset = useState<RemoteAsset?>(null);
@@ -55,19 +49,13 @@ class DriftMemoryPage extends HookConsumerWidget {
});
toNextMemory() {
memoryPageController.nextPage(
duration: const Duration(milliseconds: 500),
curve: Curves.easeIn,
);
memoryPageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn);
}
void toPreviousMemory() {
if (currentMemoryIndex.value > 0) {
// Move to the previous memory page
memoryPageController.previousPage(
duration: const Duration(milliseconds: 500),
curve: Curves.easeIn,
);
memoryPageController.previousPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn);
// Wait for the next frame to ensure the page is built
SchedulerBinding.instance.addPostFrameCallback((_) {
@@ -94,10 +82,7 @@ class DriftMemoryPage extends HookConsumerWidget {
// Go to the next asset
PageController controller = memoryAssetPageControllers[currentMemoryIndex.value];
controller.nextPage(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 500),
);
controller.nextPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500));
} else {
// Go to the next memory since we are at the end of our assets
toNextMemory();
@@ -109,10 +94,7 @@ class DriftMemoryPage extends HookConsumerWidget {
// Go to the previous asset
PageController controller = memoryAssetPageControllers[currentMemoryIndex.value];
controller.previousPage(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 500),
);
controller.previousPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500));
} else {
// Go to the previous memory since we are at the end of our assets
toPreviousMemory();
@@ -160,14 +142,7 @@ class DriftMemoryPage extends HookConsumerWidget {
// Precache the asset
final size = MediaQuery.sizeOf(context);
await precacheImage(
getFullImageProvider(
asset,
size: Size(size.width, size.height),
),
context,
size: size,
);
await precacheImage(getFullImageProvider(asset, size: Size(size.width, size.height)), context, size: size);
}
// Precache the next page right away if we are on the first page
@@ -219,9 +194,7 @@ class DriftMemoryPage extends HookConsumerWidget {
backgroundColor: bgColor,
body: SafeArea(
child: PageView.builder(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
scrollDirection: Axis.vertical,
controller: memoryPageController,
onPageChanged: (pageNumber) {
@@ -249,23 +222,13 @@ class DriftMemoryPage extends HookConsumerWidget {
}
final yearsAgo = DateTime.now().year - memories[mIndex].data.year;
final title = 'years_ago'.t(
context: context,
args: {
'years': yearsAgo.toString(),
},
);
final title = 'years_ago'.t(context: context, args: {'years': yearsAgo.toString()});
// Build horizontal page
final assetController = memoryAssetPageControllers[mIndex];
return Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 24.0,
right: 24.0,
top: 8.0,
bottom: 2.0,
),
padding: const EdgeInsets.only(left: 24.0, right: 24.0, top: 8.0, bottom: 2.0),
child: AnimatedBuilder(
animation: assetController,
builder: (context, child) {
@@ -285,9 +248,7 @@ class DriftMemoryPage extends HookConsumerWidget {
child: Stack(
children: [
PageView.builder(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
controller: assetController,
onPageChanged: onAssetChanged,
scrollDirection: Axis.horizontal,
@@ -298,11 +259,7 @@ class DriftMemoryPage extends HookConsumerWidget {
children: [
Container(
color: Colors.black,
child: DriftMemoryCard(
asset: asset,
title: title,
showTitle: index == 0,
),
child: DriftMemoryCard(asset: asset, title: title, showTitle: index == 0),
),
Positioned.fill(
child: Row(
@@ -343,35 +300,24 @@ class DriftMemoryPage extends HookConsumerWidget {
// turn off full screen mode here
// https://github.com/Milad-Akarie/auto_route_library/issues/1799
context.maybePop();
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.edgeToEdge,
);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
},
shape: const CircleBorder(),
color: Colors.white.withValues(alpha: 0.2),
elevation: 0,
child: const Icon(
Icons.close_rounded,
color: Colors.white,
),
child: const Icon(Icons.close_rounded, color: Colors.white),
),
),
if (currentAsset.value != null && currentAsset.value!.isVideo)
Positioned(
bottom: 24,
right: 32,
child: Icon(
Icons.videocam_outlined,
color: Colors.grey[200],
),
child: Icon(Icons.videocam_outlined, color: Colors.grey[200]),
),
],
),
),
DriftMemoryBottomInfo(
memory: memories[mIndex],
title: title,
),
DriftMemoryBottomInfo(memory: memories[mIndex], title: title),
],
);
},

View File

@@ -15,28 +15,20 @@ import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
class DriftPartnerDetailPage extends StatelessWidget {
final PartnerUserDto partner;
const DriftPartnerDetailPage({
super.key,
required this.partner,
});
const DriftPartnerDetailPage({super.key, required this.partner});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final timelineService = ref.watch(timelineFactoryProvider).remoteAssets(partner.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
timelineServiceProvider.overrideWith((ref) {
final timelineService = ref.watch(timelineFactoryProvider).remoteAssets(partner.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
appBar: MesmerizingSliverAppBar(
title: partner.name,
icon: Icons.person_outline,
),
appBar: MesmerizingSliverAppBar(title: partner.name, icon: Icons.person_outline),
topSliverWidget: _InfoBox(partner: partner),
topSliverWidgetHeight: 110,
bottomSheet: const PartnerDetailBottomSheet(),
@@ -48,9 +40,7 @@ class DriftPartnerDetailPage extends StatelessWidget {
class _InfoBox extends ConsumerStatefulWidget {
final PartnerUserDto partner;
const _InfoBox({
required this.partner,
});
const _InfoBox({required this.partner});
@override
ConsumerState<_InfoBox> createState() => _InfoBoxState();
@@ -72,10 +62,7 @@ class _InfoBoxState extends ConsumerState<_InfoBox> {
}
try {
await ref.read(partnerUsersProvider.notifier).toggleShowInTimeline(
widget.partner.id,
user.id,
);
await ref.read(partnerUsersProvider.notifier).toggleShowInTimeline(widget.partner.id, user.id);
setState(() {
_inTimeline = !_inTimeline;
@@ -101,18 +88,10 @@ class _InfoBoxState extends ConsumerState<_InfoBox> {
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: context.colorScheme.onSurface.withAlpha(10),
width: 1,
),
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1),
borderRadius: const BorderRadius.all(Radius.circular(20)),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(10),
context.colorScheme.primary.withAlpha(15),
],
colors: [context.colorScheme.primary.withAlpha(10), context.colorScheme.primary.withAlpha(15)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
@@ -122,18 +101,13 @@ class _InfoBoxState extends ConsumerState<_InfoBox> {
child: ListTile(
title: Text(
"Show in timeline",
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.primary,
),
style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.primary),
),
subtitle: Text(
"Show photos and videos from this user in your timeline",
style: context.textTheme.bodyMedium,
),
trailing: Switch(
value: _inTimeline,
onChanged: (_) => _toggleInTimeline(),
),
trailing: Switch(value: _inTimeline, onChanged: (_) => _toggleInTimeline()),
),
),
),

View File

@@ -52,9 +52,7 @@ class _PlaceSliverAppBar extends StatelessWidget {
pinned: true,
snap: false,
backgroundColor: context.colorScheme.surfaceContainer,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
automaticallyImplyLeading: search.value == null,
centerTitle: true,
title: search.value != null
@@ -98,20 +96,14 @@ class _Map extends StatelessWidget {
child: MapThumbnail(
onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)),
zoom: 8,
centre: currentLocation ??
const LatLng(
21.44950,
-157.91959,
),
centre: currentLocation ?? const LatLng(21.44950, -157.91959),
showAttribution: false,
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
),
),
),
)
: const SliverToBoxAdapter(
child: SizedBox.shrink(),
);
: const SliverToBoxAdapter(child: SizedBox.shrink());
}
}
@@ -127,10 +119,7 @@ class _PlaceList extends ConsumerWidget {
return places.when(
loading: () => const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
child: Padding(padding: EdgeInsets.all(20.0), child: CircularProgressIndicator()),
),
),
error: (error, stack) => SliverToBoxAdapter(
@@ -139,9 +128,7 @@ class _PlaceList extends ConsumerWidget {
padding: const EdgeInsets.all(20.0),
child: Text(
'Error loading places: $error, stack: $stack',
style: TextStyle(
color: context.colorScheme.error,
),
style: TextStyle(color: context.colorScheme.error),
),
),
),
@@ -174,21 +161,10 @@ class _PlaceTile extends StatelessWidget {
Widget build(BuildContext context) {
return LargeLeadingTile(
onTap: () => context.pushRoute(DriftPlaceDetailRoute(place: place.$1)),
title: Text(
place.$1,
style: context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
title: Text(place.$1, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)),
leading: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
child: Thumbnail(
size: const Size(80, 80),
fit: BoxFit.cover,
remoteId: place.$2,
),
borderRadius: const BorderRadius.all(Radius.circular(20)),
child: Thumbnail(size: const Size(80, 80), fit: BoxFit.cover, remoteId: place.$2),
),
);
}

View File

@@ -9,28 +9,20 @@ import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
class DriftPlaceDetailPage extends StatelessWidget {
final String place;
const DriftPlaceDetailPage({
super.key,
required this.place,
});
const DriftPlaceDetailPage({super.key, required this.place});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final timelineService = ref.watch(timelineFactoryProvider).place(place);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
timelineServiceProvider.overrideWith((ref) {
final timelineService = ref.watch(timelineFactoryProvider).place(place);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
appBar: MesmerizingSliverAppBar(
title: place,
icon: Icons.location_on,
),
appBar: MesmerizingSliverAppBar(title: place, icon: Icons.location_on),
),
);
}

View File

@@ -15,24 +15,18 @@ class DriftRecentlyTakenPage extends StatelessWidget {
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception(
'User must be logged in to access recently taken',
);
}
timelineServiceProvider.overrideWith((ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access recently taken');
}
final timelineService = ref.watch(timelineFactoryProvider).remoteAssets(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
final timelineService = ref.watch(timelineFactoryProvider).remoteAssets(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
appBar: MesmerizingSliverAppBar(title: 'recently_taken'.t()),
),
child: Timeline(appBar: MesmerizingSliverAppBar(title: 'recently_taken'.t())),
);
}
}

View File

@@ -21,10 +21,7 @@ import 'package:immich_mobile/widgets/common/remote_album_sliver_app_bar.dart';
class RemoteAlbumPage extends ConsumerStatefulWidget {
final RemoteAlbum album;
const RemoteAlbumPage({
super.key,
required this.album,
});
const RemoteAlbumPage({super.key, required this.album});
@override
ConsumerState<RemoteAlbumPage> createState() => _RemoteAlbumPageState();
@@ -40,16 +37,16 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
final albumAssets = await ref.read(remoteAlbumProvider.notifier).getAssets(widget.album.id);
final newAssets = await context.pushRoute<Set<BaseAsset>>(
DriftAssetSelectionTimelineRoute(
lockedSelectionAssets: albumAssets.toSet(),
),
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()),
);
if (newAssets == null || newAssets.isEmpty) {
return;
}
final added = await ref.read(remoteAlbumProvider.notifier).addAssets(
final added = await ref
.read(remoteAlbumProvider.notifier)
.addAssets(
widget.album.id,
newAssets.map((asset) {
final remoteAsset = asset as RemoteAsset;
@@ -60,21 +57,14 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
if (added > 0) {
ImmichToast.show(
context: context,
msg: "assets_added_to_album_count".t(
context: context,
args: {
'count': added.toString(),
},
),
msg: "assets_added_to_album_count".t(context: context, args: {'count': added.toString()}),
toastType: ToastType.success,
);
}
}
Future<void> addUsers(BuildContext context) async {
final newUsers = await context.pushRoute<List<String>>(
DriftUserSelectionRoute(album: widget.album),
);
final newUsers = await context.pushRoute<List<String>>(DriftUserSelectionRoute(album: widget.album));
if (newUsers == null || newUsers.isEmpty) {
return;
@@ -86,12 +76,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
if (newUsers.isNotEmpty) {
ImmichToast.show(
context: context,
msg: "users_added_to_album_count".t(
context: context,
args: {
'count': newUsers.length,
},
),
msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}),
toastType: ToastType.success,
);
}
@@ -107,9 +92,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
}
Future<void> toggleAlbumOrder() async {
await ref.read(remoteAlbumProvider.notifier).toggleAlbumOrder(
widget.album.id,
);
await ref.read(remoteAlbumProvider.notifier).toggleAlbumOrder(widget.album.id);
ref.invalidate(timelineServiceProvider);
}
@@ -123,16 +106,9 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'album_delete_confirmation'.t(
context: context,
args: {'album': widget.album.name},
),
),
Text('album_delete_confirmation'.t(context: context, args: {'album': widget.album.name})),
const SizedBox(height: 8),
Text(
'album_delete_confirmation_description'.t(context: context),
),
Text('album_delete_confirmation_description'.t(context: context)),
],
),
actions: [
@@ -142,9 +118,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error),
child: Text('delete_album'.t(context: context)),
),
],
@@ -230,13 +204,11 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final timelineService = ref.watch(timelineFactoryProvider).remoteAlbum(albumId: widget.album.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
timelineServiceProvider.overrideWith((ref) {
final timelineService = ref.watch(timelineFactoryProvider).remoteAlbum(albumId: widget.album.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
appBar: RemoteAlbumSliverAppBar(
@@ -245,9 +217,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
onToggleAlbumOrder: () => toggleAlbumOrder(),
onEditTitle: () => showEditTitleAndDescription(context),
),
bottomSheet: RemoteAlbumBottomSheet(
album: widget.album,
),
bottomSheet: RemoteAlbumBottomSheet(album: widget.album),
),
);
}
@@ -257,18 +227,13 @@ class _EditAlbumData {
final String name;
final String? description;
const _EditAlbumData({
required this.name,
this.description,
});
const _EditAlbumData({required this.name, this.description});
}
class _EditAlbumDialog extends ConsumerStatefulWidget {
final RemoteAlbum album;
const _EditAlbumDialog({
required this.album,
});
const _EditAlbumDialog({required this.album});
@override
ConsumerState<_EditAlbumDialog> createState() => _EditAlbumDialogState();
@@ -302,19 +267,14 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> {
final newTitle = titleController.text.trim();
final newDescription = descriptionController.text.trim();
await ref.read(remoteAlbumProvider.notifier).updateAlbum(
widget.album.id,
name: newTitle,
description: newDescription.isEmpty ? null : newDescription,
);
await ref
.read(remoteAlbumProvider.notifier)
.updateAlbum(widget.album.id, name: newTitle, description: newDescription.isEmpty ? null : newDescription);
if (mounted) {
Navigator.of(context).pop(
_EditAlbumData(
name: newTitle,
description: newDescription.isEmpty ? null : newDescription,
),
);
Navigator.of(
context,
).pop(_EditAlbumData(name: newTitle, description: newDescription.isEmpty ? null : newDescription));
}
} catch (e) {
if (mounted) {
@@ -331,11 +291,7 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> {
Widget build(BuildContext context) {
return Dialog(
insetPadding: const EdgeInsets.all(24),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(16),
),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
child: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(16),
@@ -348,16 +304,9 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> {
children: [
Row(
children: [
Icon(
Icons.edit_outlined,
color: context.colorScheme.primary,
size: 24,
),
Icon(Icons.edit_outlined, color: context.colorScheme.primary, size: 24),
const SizedBox(width: 12),
Text(
'edit_album'.t(context: context),
style: context.textTheme.titleMedium,
),
Text('edit_album'.t(context: context), style: context.textTheme.titleMedium),
],
),
const SizedBox(height: 24),
@@ -365,9 +314,7 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> {
// Album Name
Text(
'album_name'.t(context: context).toUpperCase(),
style: context.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w600,
),
style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
TextFormField(
@@ -375,9 +322,7 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> {
maxLines: 1,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
filled: true,
fillColor: context.colorScheme.surface,
),
@@ -394,9 +339,7 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> {
// Description
Text(
'description'.t(context: context).toUpperCase(),
style: context.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w600,
),
style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
TextFormField(
@@ -404,11 +347,7 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> {
maxLines: 4,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
filled: true,
fillColor: context.colorScheme.surface,
),

View File

@@ -16,18 +16,16 @@ class DriftTrashPage extends StatelessWidget {
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access trash');
}
timelineServiceProvider.overrideWith((ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access trash');
}
final timelineService = ref.watch(timelineFactoryProvider).trash(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
final timelineService = ref.watch(timelineFactoryProvider).trash(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
showStorageIndicator: true,
@@ -42,18 +40,14 @@ class DriftTrashPage extends StatelessWidget {
topSliverWidgetHeight: 24,
topSliverWidget: Consumer(
builder: (context, ref, child) {
final trashDays = ref.watch(
serverInfoProvider.select((v) => v.serverConfig.trashDays),
);
final trashDays = ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays));
return SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverToBoxAdapter(
child: SizedBox(
height: 24.0,
child: const Text(
"trash_page_info",
).t(context: context, args: {"days": "$trashDays"}),
child: const Text("trash_page_info").t(context: context, args: {"days": "$trashDays"}),
),
),
);

View File

@@ -49,10 +49,7 @@ final driftUsersProvider = FutureProvider.autoDispose<List<UserDto>>((ref) async
class DriftUserSelectionPage extends HookConsumerWidget {
final RemoteAlbum album;
const DriftUserSelectionPage({
super.key,
required this.album,
});
const DriftUserSelectionPage({super.key, required this.album});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -65,17 +62,9 @@ class DriftUserSelectionPage extends HookConsumerWidget {
buildTileIcon(UserDto user) {
if (sharedUsersList.value.contains(user)) {
return CircleAvatar(
backgroundColor: context.primaryColor,
child: const Icon(
Icons.check_rounded,
size: 25,
),
);
return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25));
} else {
return UserCircleAvatar(
user: user,
);
return UserCircleAvatar(user: user);
}
}
@@ -88,31 +77,19 @@ class DriftUserSelectionPage extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Chip(
backgroundColor: context.primaryColor.withValues(alpha: 0.15),
label: Text(
user.name,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
label: Text(user.name, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
),
),
);
}
return ListView(
children: [
Wrap(
children: [...usersChip],
),
Wrap(children: [...usersChip]),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'suggestions'.tr(),
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
fontWeight: FontWeight.bold,
),
style: const TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold),
),
),
ListView.builder(
@@ -122,31 +99,15 @@ class DriftUserSelectionPage extends HookConsumerWidget {
return ListTile(
leading: buildTileIcon(users[index]),
dense: true,
title: Text(
users[index].name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
users[index].email,
style: const TextStyle(
fontSize: 12,
),
),
title: Text(users[index].name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
subtitle: Text(users[index].email, style: const TextStyle(fontSize: 12)),
onTap: () {
if (sharedUsersList.value.contains(users[index])) {
sharedUsersList.value = sharedUsersList.value
.where(
(selectedUser) => selectedUser.id != users[index].id,
)
.where((selectedUser) => selectedUser.id != users[index].id)
.toSet();
} else {
sharedUsersList.value = {
...sharedUsersList.value,
users[index],
};
sharedUsersList.value = {...sharedUsersList.value, users[index]};
}
},
);
@@ -159,9 +120,7 @@ class DriftUserSelectionPage extends HookConsumerWidget {
return Scaffold(
appBar: AppBar(
title: const Text(
'invite_to_album',
).tr(),
title: const Text('invite_to_album').tr(),
elevation: 0,
centerTitle: false,
leading: IconButton(
@@ -173,10 +132,7 @@ class DriftUserSelectionPage extends HookConsumerWidget {
actions: [
TextButton(
onPressed: sharedUsersList.value.isEmpty ? null : addNewUsersHandler,
child: const Text(
"add",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
).tr(),
child: const Text("add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(),
),
],
),

View File

@@ -15,22 +15,18 @@ class DriftVideoPage extends StatelessWidget {
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to video');
}
timelineServiceProvider.overrideWith((ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to video');
}
final timelineService = ref.watch(timelineFactoryProvider).video(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
final timelineService = ref.watch(timelineFactoryProvider).video(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
appBar: MesmerizingSliverAppBar(title: 'videos'.t()),
),
child: Timeline(appBar: MesmerizingSliverAppBar(title: 'videos'.t())),
);
}
}

View File

@@ -17,13 +17,11 @@ class LocalTimelinePage extends StatelessWidget {
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final timelineService = ref.watch(timelineFactoryProvider).localAlbum(albumId: album.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
timelineServiceProvider.overrideWith((ref) {
final timelineService = ref.watch(timelineFactoryProvider).localAlbum(albumId: album.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
appBar: MesmerizingSliverAppBar(title: album.name),

View File

@@ -44,12 +44,7 @@ class DriftSearchPage extends HookConsumerWidget {
location: preFilter?.location ?? SearchLocationFilter(),
camera: preFilter?.camera ?? SearchCameraFilter(),
date: preFilter?.date ?? SearchDateFilter(),
display: preFilter?.display ??
SearchDisplayFilters(
isNotInAlbum: false,
isArchive: false,
isFavorite: false,
),
display: preFilter?.display ?? SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
mediaType: preFilter?.mediaType ?? AssetType.other,
language: "${context.locale.languageCode}-${context.locale.countryCode}",
),
@@ -68,10 +63,7 @@ class DriftSearchPage extends HookConsumerWidget {
SnackBar searchInfoSnackBar(String message) {
return SnackBar(
content: Text(
message,
style: context.textTheme.labelLarge,
),
content: Text(message, style: context.textTheme.labelLarge),
showCloseIcon: true,
behavior: SnackBarBehavior.fixed,
closeIconColor: context.colorScheme.onSurface,
@@ -92,9 +84,7 @@ class DriftSearchPage extends HookConsumerWidget {
final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
if (!hasResult) {
context.showSnackBar(
searchInfoSnackBar('search_no_result'.t(context: context)),
);
context.showSnackBar(searchInfoSnackBar('search_no_result'.t(context: context)));
}
previousFilter.value = filter.value;
@@ -106,9 +96,7 @@ class DriftSearchPage extends HookConsumerWidget {
final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
if (!hasResult) {
context.showSnackBar(
searchInfoSnackBar('search_no_more_result'.t(context: context)),
);
context.showSnackBar(searchInfoSnackBar('search_no_more_result'.t(context: context)));
}
isSearching.value = false;
@@ -116,39 +104,26 @@ class DriftSearchPage extends HookConsumerWidget {
searchPreFilter() {
if (preFilter != null) {
Future.delayed(
Duration.zero,
() {
search();
Future.delayed(Duration.zero, () {
search();
if (preFilter!.location.city != null) {
locationCurrentFilterWidget.value = Text(
preFilter!.location.city!,
style: context.textTheme.labelLarge,
);
}
},
);
if (preFilter!.location.city != null) {
locationCurrentFilterWidget.value = Text(preFilter!.location.city!, style: context.textTheme.labelLarge);
}
});
}
}
useEffect(
() {
Future.microtask(
() => ref.invalidate(paginatedSearchProvider),
);
searchPreFilter();
useEffect(() {
Future.microtask(() => ref.invalidate(paginatedSearchProvider));
searchPreFilter();
return null;
},
[],
);
return null;
}, []);
showPeoplePicker() {
handleOnSelect(Set<PersonDto> value) {
filter.value = filter.value.copyWith(
people: value,
);
filter.value = filter.value.copyWith(people: value);
peopleCurrentFilterWidget.value = Text(
value.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', '),
@@ -157,9 +132,7 @@ class DriftSearchPage extends HookConsumerWidget {
}
handleClear() {
filter.value = filter.value.copyWith(
people: {},
);
filter.value = filter.value.copyWith(people: {});
peopleCurrentFilterWidget.value = null;
search();
@@ -175,10 +148,7 @@ class DriftSearchPage extends HookConsumerWidget {
expanded: true,
onSearch: search,
onClear: handleClear,
child: PeoplePicker(
onSelect: handleOnSelect,
filter: filter.value.people,
),
child: PeoplePicker(onSelect: handleOnSelect, filter: filter.value.people),
),
),
);
@@ -187,11 +157,7 @@ class DriftSearchPage extends HookConsumerWidget {
showLocationPicker() {
handleOnSelect(Map<String, String?> value) {
filter.value = filter.value.copyWith(
location: SearchLocationFilter(
country: value['country'],
city: value['city'],
state: value['state'],
),
location: SearchLocationFilter(country: value['country'], city: value['city'], state: value['state']),
);
final locationText = <String>[];
@@ -207,16 +173,11 @@ class DriftSearchPage extends HookConsumerWidget {
locationText.add(value['city']!);
}
locationCurrentFilterWidget.value = Text(
locationText.join(', '),
style: context.textTheme.labelLarge,
);
locationCurrentFilterWidget.value = Text(locationText.join(', '), style: context.textTheme.labelLarge);
}
handleClear() {
filter.value = filter.value.copyWith(
location: SearchLocationFilter(),
);
filter.value = filter.value.copyWith(location: SearchLocationFilter());
locationCurrentFilterWidget.value = null;
search();
@@ -233,15 +194,10 @@ class DriftSearchPage extends HookConsumerWidget {
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Container(
padding: EdgeInsets.only(
bottom: context.viewInsets.bottom,
),
padding: EdgeInsets.only(bottom: context.viewInsets.bottom),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: LocationPicker(
onSelected: handleOnSelect,
filter: filter.value.location,
),
child: LocationPicker(onSelected: handleOnSelect, filter: filter.value.location),
),
),
),
@@ -252,10 +208,7 @@ class DriftSearchPage extends HookConsumerWidget {
showCameraPicker() {
handleOnSelect(Map<String, String?> value) {
filter.value = filter.value.copyWith(
camera: SearchCameraFilter(
make: value['make'],
model: value['model'],
),
camera: SearchCameraFilter(make: value['make'], model: value['model']),
);
cameraCurrentFilterWidget.value = Text(
@@ -265,9 +218,7 @@ class DriftSearchPage extends HookConsumerWidget {
}
handleClear() {
filter.value = filter.value.copyWith(
camera: SearchCameraFilter(),
);
filter.value = filter.value.copyWith(camera: SearchCameraFilter());
cameraCurrentFilterWidget.value = null;
search();
@@ -283,10 +234,7 @@ class DriftSearchPage extends HookConsumerWidget {
onClear: handleClear,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: CameraPicker(
onSelect: handleOnSelect,
filter: filter.value.camera,
),
child: CameraPicker(onSelect: handleOnSelect, filter: filter.value.camera),
),
),
);
@@ -318,9 +266,7 @@ class DriftSearchPage extends HookConsumerWidget {
);
if (date == null) {
filter.value = filter.value.copyWith(
date: SearchDateFilter(),
);
filter.value = filter.value.copyWith(date: SearchDateFilter());
dateRangeCurrentFilterWidget.value = null;
search();
@@ -330,13 +276,7 @@ class DriftSearchPage extends HookConsumerWidget {
filter.value = filter.value.copyWith(
date: SearchDateFilter(
takenAfter: date.start,
takenBefore: date.end.add(
const Duration(
hours: 23,
minutes: 59,
seconds: 59,
),
),
takenBefore: date.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)),
),
);
@@ -365,24 +305,20 @@ class DriftSearchPage extends HookConsumerWidget {
// MEDIA PICKER
showMediaTypePicker() {
handleOnSelected(AssetType assetType) {
filter.value = filter.value.copyWith(
mediaType: assetType,
);
filter.value = filter.value.copyWith(mediaType: assetType);
mediaTypeCurrentFilterWidget.value = Text(
assetType == AssetType.image
? 'image'.t(context: context)
: assetType == AssetType.video
? 'video'.t(context: context)
: 'all'.t(context: context),
? 'video'.t(context: context)
: 'all'.t(context: context),
style: context.textTheme.labelLarge,
);
}
handleClear() {
filter.value = filter.value.copyWith(
mediaType: AssetType.other,
);
filter.value = filter.value.copyWith(mediaType: AssetType.other);
mediaTypeCurrentFilterWidget.value = null;
search();
@@ -394,10 +330,7 @@ class DriftSearchPage extends HookConsumerWidget {
title: 'search_filter_media_type_title'.t(context: context),
onSearch: search,
onClear: handleClear,
child: MediaTypePicker(
onSelect: handleOnSelected,
filter: filter.value.mediaType,
),
child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType),
),
);
}
@@ -409,33 +342,19 @@ class DriftSearchPage extends HookConsumerWidget {
value.forEach((key, value) {
switch (key) {
case DisplayOption.notInAlbum:
filter.value = filter.value.copyWith(
display: filter.value.display.copyWith(
isNotInAlbum: value,
),
);
filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isNotInAlbum: value));
if (value) {
filterText.add(
'search_filter_display_option_not_in_album'.t(context: context),
);
filterText.add('search_filter_display_option_not_in_album'.t(context: context));
}
break;
case DisplayOption.archive:
filter.value = filter.value.copyWith(
display: filter.value.display.copyWith(
isArchive: value,
),
);
filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isArchive: value));
if (value) {
filterText.add('archive'.t(context: context));
}
break;
case DisplayOption.favorite:
filter.value = filter.value.copyWith(
display: filter.value.display.copyWith(
isFavorite: value,
),
);
filter.value = filter.value.copyWith(display: filter.value.display.copyWith(isFavorite: value));
if (value) {
filterText.add('favorite'.t(context: context));
}
@@ -448,19 +367,12 @@ class DriftSearchPage extends HookConsumerWidget {
return;
}
displayOptionCurrentFilterWidget.value = Text(
filterText.join(', '),
style: context.textTheme.labelLarge,
);
displayOptionCurrentFilterWidget.value = Text(filterText.join(', '), style: context.textTheme.labelLarge);
}
handleClear() {
filter.value = filter.value.copyWith(
display: SearchDisplayFilters(
isNotInAlbum: false,
isArchive: false,
isFavorite: false,
),
display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
);
displayOptionCurrentFilterWidget.value = null;
@@ -473,10 +385,7 @@ class DriftSearchPage extends HookConsumerWidget {
title: 'display_options'.t(context: context),
onSearch: search,
onClear: handleClear,
child: DisplayOptionPicker(
onSelect: handleOnSelect,
filter: filter.value.display,
),
child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display),
),
);
}
@@ -484,27 +393,15 @@ class DriftSearchPage extends HookConsumerWidget {
handleTextSubmitted(String value) {
switch (textSearchType.value) {
case TextSearchType.context:
filter.value = filter.value.copyWith(
filename: '',
context: value,
description: '',
);
filter.value = filter.value.copyWith(filename: '', context: value, description: '');
break;
case TextSearchType.filename:
filter.value = filter.value.copyWith(
filename: value,
context: '',
description: '',
);
filter.value = filter.value.copyWith(filename: value, context: '', description: '');
break;
case TextSearchType.description:
filter.value = filter.value.copyWith(
filename: '',
context: '',
description: value,
);
filter.value = filter.value.copyWith(filename: '', context: '', description: value);
break;
}
@@ -512,10 +409,10 @@ class DriftSearchPage extends HookConsumerWidget {
}
IconData getSearchPrefixIcon() => switch (textSearchType.value) {
TextSearchType.context => Icons.image_search_rounded,
TextSearchType.filename => Icons.abc_rounded,
TextSearchType.description => Icons.text_snippet_outlined,
};
TextSearchType.context => Icons.image_search_rounded,
TextSearchType.filename => Icons.abc_rounded,
TextSearchType.description => Icons.text_snippet_outlined,
};
return Scaffold(
resizeToAvoidBottomInset: false,
@@ -528,21 +425,11 @@ class DriftSearchPage extends HookConsumerWidget {
style: MenuStyle(
elevation: const WidgetStatePropertyAll(1),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
),
padding: const WidgetStatePropertyAll(
EdgeInsets.all(4),
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))),
),
padding: const WidgetStatePropertyAll(EdgeInsets.all(4)),
),
builder: (
BuildContext context,
MenuController controller,
Widget? child,
) {
builder: (BuildContext context, MenuController controller, Widget? child) {
return IconButton(
onPressed: () {
if (controller.isOpen) {
@@ -616,13 +503,8 @@ class DriftSearchPage extends HookConsumerWidget {
],
title: Container(
decoration: BoxDecoration(
border: Border.all(
color: context.colorScheme.onSurface.withAlpha(0),
width: 0,
),
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0),
borderRadius: const BorderRadius.all(Radius.circular(24)),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withValues(alpha: 0.075),
@@ -638,12 +520,7 @@ class DriftSearchPage extends HookConsumerWidget {
key: const Key('search_text_field'),
controller: textSearchController,
contentPadding: preFilter != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8),
prefixIcon: preFilter != null
? null
: Icon(
getSearchPrefixIcon(),
color: context.colorScheme.primary,
),
prefixIcon: preFilter != null ? null : Icon(getSearchPrefixIcon(), color: context.colorScheme.primary),
onSubmitted: handleTextSubmitted,
focusNode: ref.watch(searchInputFocusProvider),
),
@@ -705,10 +582,7 @@ class DriftSearchPage extends HookConsumerWidget {
),
),
if (isSearching.value)
const SliverFillRemaining(
hasScrollBody: false,
child: Center(child: CircularProgressIndicator()),
)
const SliverFillRemaining(hasScrollBody: false, child: Center(child: CircularProgressIndicator()))
else
_SearchResultGrid(onScrollEnd: loadMoreSearchResult),
],
@@ -747,19 +621,13 @@ class _SearchResultGrid extends ConsumerWidget {
child: SliverFillRemaining(
child: ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final timelineService = ref.watch(timelineFactoryProvider).fromAssets(searchResult.assets);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
timelineServiceProvider.overrideWith((ref) {
final timelineService = ref.watch(timelineFactoryProvider).fromAssets(searchResult.assets);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
key: ValueKey(searchResult.totalAssets),
appBar: null,
groupBy: GroupAssetsBy.none,
),
child: Timeline(key: ValueKey(searchResult.totalAssets), appBar: null, groupBy: GroupAssetsBy.none),
),
),
);
@@ -784,16 +652,10 @@ class _SearchEmptyContent extends StatelessWidget {
),
const SizedBox(height: 16),
Center(
child: Text(
'search_page_search_photos_videos'.t(context: context),
style: context.textTheme.labelLarge,
),
child: Text('search_page_search_photos_videos'.t(context: context), style: context.textTheme.labelLarge),
),
const SizedBox(height: 32),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: _QuickLinkList(),
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: _QuickLinkList()),
],
),
);
@@ -807,13 +669,8 @@ class _QuickLinkList extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
border: Border.all(
color: context.colorScheme.outline.withAlpha(10),
width: 1,
),
borderRadius: const BorderRadius.all(Radius.circular(20)),
border: Border.all(color: context.colorScheme.outline.withAlpha(10), width: 1),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withAlpha(10),
@@ -876,19 +733,9 @@ class _QuickLink extends StatelessWidget {
);
return ListTile(
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
leading: Icon(
icon,
size: 26,
),
title: Text(
title,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
shape: RoundedRectangleBorder(borderRadius: borderRadius),
leading: Icon(icon, size: 26),
title: Text(title, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)),
onTap: onTap,
);
}

View File

@@ -24,10 +24,7 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
return false;
}
state = SearchResult(
assets: [...state.assets, ...result.assets],
nextPage: result.nextPage,
);
state = SearchResult(assets: [...state.assets, ...result.assets], nextPage: result.nextPage);
return true;
}

View File

@@ -27,10 +27,7 @@ class ArchiveActionButton extends ConsumerWidget {
EventStream.shared.emit(const ViewerReloadAssetEvent());
}
final successMessage = 'archive_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'archive_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View File

@@ -39,14 +39,10 @@ class BaseActionButton extends StatelessWidget {
}
return ConstrainedBox(
constraints: BoxConstraints(
maxWidth: maxWidth,
),
constraints: BoxConstraints(maxWidth: maxWidth),
child: MaterialButton(
padding: const EdgeInsets.all(10),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))),
textColor: textColor,
onPressed: onPressed,
onLongPress: onLongPressed,
@@ -59,10 +55,7 @@ class BaseActionButton extends StatelessWidget {
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w400,
),
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400),
maxLines: 3,
textAlign: TextAlign.center,
softWrap: true,

View File

@@ -20,10 +20,7 @@ class CastActionButton extends ConsumerWidget {
iconColor: isCasting ? context.primaryColor : null, // null = default color
label: "cast".t(context: context),
onPressed: () {
showDialog(
context: context,
builder: (context) => const CastDialog(),
);
showDialog(context: context, builder: (context) => const CastDialog());
},
menuItem: menuItem,
);

View File

@@ -40,9 +40,7 @@ class DeleteActionButton extends ConsumerWidget {
onPressed: () => Navigator.of(context).pop(true),
child: Text(
'confirm'.t(context: context),
style: TextStyle(
color: context.colorScheme.error,
),
style: TextStyle(color: context.colorScheme.error),
),
),
],
@@ -58,10 +56,7 @@ class DeleteActionButton extends ConsumerWidget {
EventStream.shared.emit(const ViewerReloadAssetEvent());
}
final successMessage = 'delete_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'delete_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View File

@@ -33,10 +33,7 @@ class DeleteLocalActionButton extends ConsumerWidget {
return;
}
final successMessage = 'delete_local_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'delete_local_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View File

@@ -43,17 +43,10 @@ class DeleteTrashActionButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return TextButton.icon(
icon: Icon(
Icons.delete_forever,
color: Colors.red[400],
),
icon: Icon(Icons.delete_forever, color: Colors.red[400]),
label: Text(
"delete".t(context: context),
style: TextStyle(
fontSize: 14,
color: Colors.red[400],
fontWeight: FontWeight.bold,
),
style: TextStyle(fontSize: 14, color: Colors.red[400], fontWeight: FontWeight.bold),
),
onPressed: () => _onTap(context, ref),
);

View File

@@ -25,10 +25,7 @@ class EditLocationActionButton extends ConsumerWidget {
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'edit_location_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'edit_location_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View File

@@ -12,11 +12,7 @@ class FavoriteActionButton extends ConsumerWidget {
final ActionSource source;
final bool menuItem;
const FavoriteActionButton({
super.key,
required this.source,
this.menuItem = false,
});
const FavoriteActionButton({super.key, required this.source, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
@@ -31,10 +27,7 @@ class FavoriteActionButton extends ConsumerWidget {
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'favorite_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'favorite_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View File

@@ -12,11 +12,7 @@ class RemoveFromAlbumActionButton extends ConsumerWidget {
final String albumId;
final ActionSource source;
const RemoveFromAlbumActionButton({
super.key,
required this.albumId,
required this.source,
});
const RemoveFromAlbumActionButton({super.key, required this.albumId, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {

View File

@@ -20,10 +20,7 @@ class RestoreTrashActionButton extends ConsumerWidget {
final result = await ref.read(actionProvider.notifier).restoreTrash(source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'assets_restored_count'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(
@@ -38,16 +35,8 @@ class RestoreTrashActionButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return TextButton.icon(
icon: const Icon(
Icons.history_rounded,
),
label: Text(
'restore'.t(),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
icon: const Icon(Icons.history_rounded),
label: Text('restore'.t(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
onPressed: () => _onTap(context, ref),
);
}

View File

@@ -27,10 +27,7 @@ class StackActionButton extends ConsumerWidget {
final result = await ref.read(actionProvider.notifier).stack(user.id, source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'stack_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'stack_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View File

@@ -30,10 +30,7 @@ class TrashActionButton extends ConsumerWidget {
EventStream.shared.emit(const ViewerReloadAssetEvent());
}
final successMessage = 'trash_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'trash_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View File

@@ -21,10 +21,7 @@ class UnArchiveActionButton extends ConsumerWidget {
final result = await ref.read(actionProvider.notifier).unArchive(source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'unarchive_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'unarchive_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View File

@@ -12,11 +12,7 @@ class UnFavoriteActionButton extends ConsumerWidget {
final ActionSource source;
final bool menuItem;
const UnFavoriteActionButton({
super.key,
required this.source,
this.menuItem = false,
});
const UnFavoriteActionButton({super.key, required this.source, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
@@ -31,10 +27,7 @@ class UnFavoriteActionButton extends ConsumerWidget {
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'unfavorite_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'unfavorite_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View File

@@ -21,10 +21,7 @@ class UnStackActionButton extends ConsumerWidget {
final result = await ref.read(actionProvider.notifier).unStack(source);
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'unstack_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'unstack_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View File

@@ -20,10 +20,7 @@ class UploadActionButton extends ConsumerWidget {
final result = await ref.read(actionProvider.notifier).upload(source);
final successMessage = 'upload_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
final successMessage = 'upload_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
ImmichToast.show(

View File

@@ -27,10 +27,7 @@ typedef AlbumSelectorCallback = void Function(RemoteAlbum album);
class AlbumSelector extends ConsumerStatefulWidget {
final AlbumSelectorCallback onAlbumSelected;
const AlbumSelector({
super.key,
required this.onAlbumSelected,
});
const AlbumSelector({super.key, required this.onAlbumSelected});
@override
ConsumerState<AlbumSelector> createState() => _AlbumSelectorState();
@@ -113,21 +110,10 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
onSearch: onSearch,
searchController: searchController,
),
_QuickSortAndViewMode(
isGrid: isGrid,
onToggleViewMode: toggleViewMode,
),
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode),
isGrid
? _AlbumGrid(
albums: albums,
userId: userId,
onAlbumSelected: widget.onAlbumSelected,
)
: _AlbumList(
albums: albums,
userId: userId,
onAlbumSelected: widget.onAlbumSelected,
),
? _AlbumGrid(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
: _AlbumList(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
],
);
}
@@ -151,18 +137,12 @@ class _SortButtonState extends ConsumerState<_SortButton> {
setState(() {
albumSortIsReverse = !albumSortIsReverse;
});
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
sortMode,
isReverse: albumSortIsReverse,
);
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
} else {
setState(() {
albumSortOption = sortMode;
});
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
sortMode,
isReverse: albumSortIsReverse,
);
ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
}
}
@@ -172,15 +152,9 @@ class _SortButtonState extends ConsumerState<_SortButton> {
style: MenuStyle(
elevation: const WidgetStatePropertyAll(1),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
),
padding: const WidgetStatePropertyAll(
EdgeInsets.all(4),
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))),
),
padding: const WidgetStatePropertyAll(EdgeInsets.all(4)),
),
consumeOutsideTap: true,
menuChildren: RemoteAlbumSortMode.values
@@ -188,33 +162,27 @@ class _SortButtonState extends ConsumerState<_SortButton> {
(sortMode) => MenuItemButton(
leadingIcon: albumSortOption == sortMode
? albumSortIsReverse
? Icon(
Icons.keyboard_arrow_down,
color: albumSortOption == sortMode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
)
: Icon(
Icons.keyboard_arrow_up_rounded,
color: albumSortOption == sortMode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
)
? Icon(
Icons.keyboard_arrow_down,
color: albumSortOption == sortMode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
)
: Icon(
Icons.keyboard_arrow_up_rounded,
color: albumSortOption == sortMode
? context.colorScheme.onPrimary
: context.colorScheme.onSurface,
)
: const Icon(Icons.abc, color: Colors.transparent),
onPressed: () => onMenuTapped(sortMode),
style: ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.fromLTRB(16, 16, 32, 16),
),
padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(16, 16, 32, 16)),
backgroundColor: WidgetStateProperty.all(
albumSortOption == sortMode ? context.colorScheme.primary : Colors.transparent,
),
shape: WidgetStateProperty.all(
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))),
),
),
child: Text(
@@ -243,12 +211,8 @@ class _SortButtonState extends ConsumerState<_SortButton> {
Padding(
padding: const EdgeInsets.only(right: 5),
child: albumSortIsReverse
? const Icon(
Icons.keyboard_arrow_down,
)
: const Icon(
Icons.keyboard_arrow_up_rounded,
),
? const Icon(Icons.keyboard_arrow_down)
: const Icon(Icons.keyboard_arrow_up_rounded),
),
Text(
albumSortOption.key.t(context: context),
@@ -287,13 +251,8 @@ class _SearchBar extends StatelessWidget {
sliver: SliverToBoxAdapter(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: context.colorScheme.onSurface.withAlpha(0),
width: 0,
),
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0),
borderRadius: const BorderRadius.all(Radius.circular(24)),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withValues(alpha: 0.075),
@@ -311,10 +270,7 @@ class _SearchBar extends StatelessWidget {
hintText: 'search_albums'.tr(),
prefixIcon: const Icon(Icons.search_rounded),
suffixIcon: searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear_rounded),
onPressed: onClearSearch,
)
? IconButton(icon: const Icon(Icons.clear_rounded), onPressed: onClearSearch)
: null,
controller: searchController,
onChanged: (_) => onSearch(searchController.text, filterMode),
@@ -362,10 +318,7 @@ class _QuickFilterButtonRow extends StatelessWidget {
isSelected: filterMode == QuickFilterMode.sharedWithMe,
onTap: () {
onChangeFilter(QuickFilterMode.sharedWithMe);
onSearch(
searchController.text,
QuickFilterMode.sharedWithMe,
);
onSearch(searchController.text, QuickFilterMode.sharedWithMe);
},
),
_QuickFilterButton(
@@ -373,10 +326,7 @@ class _QuickFilterButtonRow extends StatelessWidget {
isSelected: filterMode == QuickFilterMode.myAlbums,
onTap: () {
onChangeFilter(QuickFilterMode.myAlbums);
onSearch(
searchController.text,
QuickFilterMode.myAlbums,
);
onSearch(searchController.text, QuickFilterMode.myAlbums);
},
),
],
@@ -387,11 +337,7 @@ class _QuickFilterButtonRow extends StatelessWidget {
}
class _QuickFilterButton extends StatelessWidget {
const _QuickFilterButton({
required this.isSelected,
required this.onTap,
required this.label,
});
const _QuickFilterButton({required this.isSelected, required this.onTap, required this.label});
final bool isSelected;
final VoidCallback onTap;
@@ -402,18 +348,11 @@ class _QuickFilterButton extends StatelessWidget {
return TextButton(
onPressed: onTap,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
isSelected ? context.colorScheme.primary : Colors.transparent,
),
backgroundColor: WidgetStateProperty.all(isSelected ? context.colorScheme.primary : Colors.transparent),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(25),
width: 1,
),
borderRadius: const BorderRadius.all(Radius.circular(20)),
side: BorderSide(color: context.colorScheme.onSurface.withAlpha(25), width: 1),
),
),
),
@@ -429,10 +368,7 @@ class _QuickFilterButton extends StatelessWidget {
}
class _QuickSortAndViewMode extends StatelessWidget {
const _QuickSortAndViewMode({
required this.isGrid,
required this.onToggleViewMode,
});
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode});
final bool isGrid;
final VoidCallback onToggleViewMode;
@@ -447,10 +383,7 @@ class _QuickSortAndViewMode extends StatelessWidget {
children: [
const _SortButton(),
IconButton(
icon: Icon(
isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined,
size: 24,
),
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
onPressed: onToggleViewMode,
),
],
@@ -461,11 +394,7 @@ class _QuickSortAndViewMode extends StatelessWidget {
}
class _AlbumList extends ConsumerWidget {
const _AlbumList({
required this.albums,
required this.userId,
required this.onAlbumSelected,
});
const _AlbumList({required this.albums, required this.userId, required this.onAlbumSelected});
final List<RemoteAlbum> albums;
final String? userId;
@@ -476,10 +405,7 @@ class _AlbumList extends ConsumerWidget {
if (albums.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Text('No albums found'),
),
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
),
);
}
@@ -491,51 +417,25 @@ class _AlbumList extends ConsumerWidget {
final album = albums[index];
return Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
),
padding: const EdgeInsets.only(bottom: 8.0),
child: LargeLeadingTile(
title: Text(
album.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
subtitle: Text(
'${'items_count'.t(
context: context,
args: {
'count': album.assetCount,
},
)} ${album.ownerId != userId ? 'shared_by_user'.t(
context: context,
args: {
'user': album.ownerName,
},
) : 'owned'.t(context: context)}',
'${'items_count'.t(context: context, args: {'count': album.assetCount})} • ${album.ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': album.ownerName}) : 'owned'.t(context: context)}',
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
onTap: () => onAlbumSelected(album),
leadingPadding: const EdgeInsets.only(
right: 16,
),
leadingPadding: const EdgeInsets.only(right: 16),
leading: album.thumbnailAssetId != null
? ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(15),
),
child: SizedBox(
width: 80,
height: 80,
child: Thumbnail(
remoteId: album.thumbnailAssetId,
),
),
borderRadius: const BorderRadius.all(Radius.circular(15)),
child: SizedBox(width: 80, height: 80, child: Thumbnail(remoteId: album.thumbnailAssetId)),
)
: SizedBox(
width: 80,
@@ -544,16 +444,9 @@ class _AlbumList extends ConsumerWidget {
decoration: BoxDecoration(
color: context.colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(Radius.circular(16)),
border: Border.all(
color: context.colorScheme.outline.withAlpha(50),
width: 1,
),
),
child: const Icon(
Icons.photo_album_rounded,
size: 24,
color: Colors.grey,
border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1),
),
child: const Icon(Icons.photo_album_rounded, size: 24, color: Colors.grey),
),
),
),
@@ -566,11 +459,7 @@ class _AlbumList extends ConsumerWidget {
}
class _AlbumGrid extends StatelessWidget {
const _AlbumGrid({
required this.albums,
required this.userId,
required this.onAlbumSelected,
});
const _AlbumGrid({required this.albums, required this.userId, required this.onAlbumSelected});
final List<RemoteAlbum> albums;
final String? userId;
@@ -581,10 +470,7 @@ class _AlbumGrid extends StatelessWidget {
if (albums.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Text('No albums found'),
),
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
),
);
}
@@ -598,28 +484,17 @@ class _AlbumGrid extends StatelessWidget {
crossAxisSpacing: 4,
childAspectRatio: .7,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
final album = albums[index];
return _GridAlbumCard(
album: album,
userId: userId,
onAlbumSelected: onAlbumSelected,
);
},
childCount: albums.length,
),
delegate: SliverChildBuilderDelegate((context, index) {
final album = albums[index];
return _GridAlbumCard(album: album, userId: userId, onAlbumSelected: onAlbumSelected);
}, childCount: albums.length),
),
);
}
}
class _GridAlbumCard extends ConsumerWidget {
const _GridAlbumCard({
required this.album,
required this.userId,
required this.onAlbumSelected,
});
const _GridAlbumCard({required this.album, required this.userId, required this.onAlbumSelected});
final RemoteAlbum album;
final String? userId;
@@ -633,13 +508,8 @@ class _GridAlbumCard extends ConsumerWidget {
elevation: 0,
color: context.colorScheme.surfaceBright,
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(
Radius.circular(16),
),
side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(25),
width: 1,
),
borderRadius: const BorderRadius.all(Radius.circular(16)),
side: BorderSide(color: context.colorScheme.onSurface.withAlpha(25), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -647,22 +517,14 @@ class _GridAlbumCard extends ConsumerWidget {
Expanded(
flex: 2,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(15),
),
borderRadius: const BorderRadius.vertical(top: Radius.circular(15)),
child: SizedBox(
width: double.infinity,
child: album.thumbnailAssetId != null
? Thumbnail(
remoteId: album.thumbnailAssetId,
)
? Thumbnail(remoteId: album.thumbnailAssetId)
: Container(
color: context.colorScheme.surfaceContainerHighest,
child: const Icon(
Icons.photo_album_rounded,
size: 40,
color: Colors.grey,
),
child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey),
),
),
),
@@ -679,27 +541,13 @@ class _GridAlbumCard extends ConsumerWidget {
album.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
Text(
'${'items_count'.t(
context: context,
args: {
'count': album.assetCount,
},
)} ${album.ownerId != userId ? 'shared_by_user'.t(
context: context,
args: {
'user': album.ownerName,
},
) : 'owned'.t(context: context)}',
'${'items_count'.t(context: context, args: {'count': album.assetCount})} • ${album.ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': album.ownerName}) : 'owned'.t(context: context)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.labelMedium?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
],
),
@@ -718,17 +566,15 @@ class AddToAlbumHeader extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<void> onCreateAlbum() async {
final newAlbum = await ref.read(remoteAlbumProvider.notifier).createAlbum(
final newAlbum = await ref
.read(remoteAlbumProvider.notifier)
.createAlbum(
title: "Untitled Album",
assetIds: ref.read(multiSelectProvider).selectedAssets.map((e) => (e as RemoteAsset).id).toList(),
);
if (newAlbum == null) {
ImmichToast.show(
context: context,
toastType: ToastType.error,
msg: 'errors.failed_to_create_album'.tr(),
);
ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr());
return;
}
@@ -736,38 +582,23 @@ class AddToAlbumHeader extends ConsumerWidget {
}
return SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"add_to_album",
style: context.textTheme.titleSmall,
).tr(),
Text("add_to_album", style: context.textTheme.titleSmall).tr(),
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
), // remove internal padding
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // remove internal padding
minimumSize: const Size(0, 0), // allow shrinking
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // remove extra height
),
onPressed: onCreateAlbum,
icon: Icon(
Icons.add,
color: context.primaryColor,
),
icon: Icon(Icons.add, color: context.primaryColor),
label: Text(
"common_create_new_album",
style: TextStyle(
color: context.primaryColor,
fontWeight: FontWeight.bold,
fontSize: 14,
),
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14),
).tr(),
),
],

View File

@@ -13,7 +13,5 @@ class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier<List<RemoteAs
}
}
final stackChildrenNotifier =
AsyncNotifierProvider.autoDispose.family<StackChildrenNotifier, List<RemoteAsset>, BaseAsset?>(
StackChildrenNotifier.new,
);
final stackChildrenNotifier = AsyncNotifierProvider.autoDispose
.family<StackChildrenNotifier, List<RemoteAsset>, BaseAsset?>(StackChildrenNotifier.new);

View File

@@ -11,9 +11,7 @@ class AssetStackRow extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
int opacity = ref.watch(
assetViewerProvider.select((state) => state.backgroundOpacity),
);
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
if (!showControls) {
@@ -27,11 +25,10 @@ class AssetStackRow extends ConsumerWidget {
child: AnimatedOpacity(
opacity: opacity / 255,
duration: Durations.short2,
child: ref.watch(stackChildrenNotifier(asset)).when(
data: (state) => SizedBox.square(
dimension: 80,
child: _StackList(stack: state),
),
child: ref
.watch(stackChildrenNotifier(asset))
.when(
data: (state) => SizedBox.square(dimension: 80, child: _StackList(stack: state)),
error: (_, __) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
@@ -49,11 +46,7 @@ class _StackList extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(
left: 5,
right: 5,
bottom: 30,
),
padding: const EdgeInsets.only(left: 5, right: 5, bottom: 30),
itemCount: stack.length,
itemBuilder: (ctx, index) {
final asset = stack[index];
@@ -71,9 +64,7 @@ class _StackList extends ConsumerWidget {
? const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(6)),
border: Border.fromBorderSide(
BorderSide(color: Colors.white, width: 2),
),
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)),
)
: const BoxDecoration(
color: Colors.white,
@@ -87,10 +78,7 @@ class _StackList extends ConsumerWidget {
children: [
Image(
fit: BoxFit.cover,
image: getThumbnailImageProvider(
remoteId: asset.id,
size: const Size.square(60),
),
image: getThumbnailImageProvider(remoteId: asset.id, size: const Size.square(60)),
),
if (asset.isVideo)
const Icon(
@@ -98,11 +86,7 @@ class _StackList extends ConsumerWidget {
color: Colors.white,
size: 16,
shadows: [
Shadow(
blurRadius: 5.0,
color: Color.fromRGBO(0, 0, 0, 0.6),
offset: Offset(0.0, 0.0),
),
Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0)),
],
),
],

View File

@@ -36,12 +36,7 @@ class AssetViewerPage extends StatelessWidget {
final TimelineService timelineService;
final int? heroOffset;
const AssetViewerPage({
super.key,
required this.initialIndex,
required this.timelineService,
this.heroOffset,
});
const AssetViewerPage({super.key, required this.initialIndex, required this.timelineService, this.heroOffset});
@override
Widget build(BuildContext context) {
@@ -59,12 +54,7 @@ class AssetViewer extends ConsumerStatefulWidget {
final Platform? platform;
final int? heroOffset;
const AssetViewer({
super.key,
required this.initialIndex,
this.platform,
this.heroOffset,
});
const AssetViewer({super.key, required this.initialIndex, this.platform, this.heroOffset});
@override
ConsumerState createState() => _AssetViewerState();
@@ -162,11 +152,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
context,
onError: (_, __) {},
),
precacheImage(
getFullImageProvider(asset, size: screenSize),
context,
onError: (_, __) {},
),
precacheImage(getFullImageProvider(asset, size: screenSize), context, onError: (_, __) {}),
]),
);
}
@@ -222,9 +208,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
duration: const Duration(seconds: 2),
content: Text(
"local_asset_cast_failed".tr(),
style: context.textTheme.bodyLarge?.copyWith(
color: context.primaryColor,
),
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
),
),
);
@@ -262,7 +246,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
viewController = controller;
dragDownPosition = details.localPosition;
initialPhotoViewState = controller.value;
final isZoomed = scaleStateController.scaleState == PhotoViewScaleState.zoomedIn ||
final isZoomed =
scaleStateController.scaleState == PhotoViewScaleState.zoomedIn ||
scaleStateController.scaleState == PhotoViewScaleState.covering;
if (!showingBottomSheet && isZoomed) {
blockGestures = true;
@@ -350,10 +335,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
final backgroundOpacity = (255 * (1.0 - (scaleReduction / dragRatio))).round();
viewController?.updateMultiple(
position: initialPhotoViewState.position + delta,
scale: updatedScale,
);
viewController?.updateMultiple(position: initialPhotoViewState.position + delta, scale: updatedScale);
ref.read(assetViewerProvider.notifier).setOpacity(backgroundOpacity);
}
@@ -450,32 +432,21 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
});
}
void _openBottomSheet(
BuildContext ctx, {
double extent = _kBottomSheetMinimumExtent,
}) {
void _openBottomSheet(BuildContext ctx, {double extent = _kBottomSheetMinimumExtent}) {
ref.read(assetViewerProvider.notifier).setBottomSheet(true);
initialScale = viewController?.scale;
viewController?.updateMultiple(scale: _getScaleForBottomSheet);
previousExtent = _kBottomSheetMinimumExtent;
sheetCloseController = showBottomSheet(
context: ctx,
sheetAnimationStyle: const AnimationStyle(
duration: Durations.short4,
reverseDuration: Durations.short2,
),
sheetAnimationStyle: const AnimationStyle(duration: Durations.short4, reverseDuration: Durations.short2),
constraints: const BoxConstraints(maxWidth: double.infinity),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20.0))),
backgroundColor: ctx.colorScheme.surfaceContainerLowest,
builder: (_) {
return NotificationListener<Notification>(
onNotification: _onNotification,
child: AssetDetailBottomSheet(
controller: bottomSheetController,
initialChildSize: extent,
),
child: AssetDetailBottomSheet(controller: bottomSheetController, initialChildSize: extent),
);
},
);
@@ -496,18 +467,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
return;
}
isSnapping = true;
bottomSheetController.animateTo(
_kBottomSheetSnapExtent,
duration: Durations.short3,
curve: Curves.easeOut,
);
bottomSheetController.animateTo(_kBottomSheetSnapExtent, duration: Durations.short3, curve: Curves.easeOut);
}
Widget _placeholderBuilder(
BuildContext ctx,
ImageChunkEvent? progress,
int index,
) {
Widget _placeholderBuilder(BuildContext ctx, ImageChunkEvent? progress, int index) {
BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index);
final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull;
if (stackChildren != null && stackChildren.isNotEmpty) {
@@ -517,14 +480,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
width: double.infinity,
height: double.infinity,
color: backgroundColor,
child: Thumbnail(
asset: asset,
fit: BoxFit.contain,
size: Size(
ctx.width,
ctx.height,
),
),
child: Thumbnail(asset: asset, fit: BoxFit.contain, size: Size(ctx.width, ctx.height)),
);
}
@@ -574,11 +530,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
width: ctx.width,
height: ctx.height,
color: backgroundColor,
child: Thumbnail(
asset: asset,
fit: BoxFit.contain,
size: size,
),
child: Thumbnail(asset: asset, fit: BoxFit.contain, size: size),
),
);
}
@@ -662,8 +614,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
pageController: pageController,
scrollPhysics: platform.isIOS
? const FastScrollPhysics() // Use bouncing physics for iOS
: const FastClampingScrollPhysics() // Use heavy physics for Android
,
: const FastClampingScrollPhysics(), // Use heavy physics for Android
itemCount: totalAssets,
onPageChanged: _onPageChanged,
onPageBuild: _onPageBuild,
@@ -678,10 +629,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const AssetStackRow(),
if (!isInLockedView) const ViewerBottomBar(),
],
children: [const AssetStackRow(), if (!isInLockedView) const ViewerBottomBar()],
),
),
);

View File

@@ -79,17 +79,11 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
}
void setOpacity(int opacity) {
state = state.copyWith(
backgroundOpacity: opacity,
showingControls: opacity == 255 ? true : state.showingControls,
);
state = state.copyWith(backgroundOpacity: opacity, showingControls: opacity == 255 ? true : state.showingControls);
}
void setBottomSheet(bool showing) {
state = state.copyWith(
showingBottomSheet: showing,
showingControls: showing ? true : state.showingControls,
);
state = state.copyWith(showingBottomSheet: showing, showingControls: showing ? true : state.showingControls);
if (showing) {
ref.read(videoPlayerControlsProvider.notifier).pause();
}

View File

@@ -25,12 +25,8 @@ class ViewerBottomBar extends ConsumerWidget {
final user = ref.watch(currentUserProvider);
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
final isSheetOpen = ref.watch(
assetViewerProvider.select((s) => s.showingBottomSheet),
);
int opacity = ref.watch(
assetViewerProvider.select((state) => state.backgroundOpacity),
);
final isSheetOpen = ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
if (!showControls) {
@@ -42,13 +38,8 @@ class ViewerBottomBar extends ConsumerWidget {
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
if (asset.hasRemote && isOwner) const ArchiveActionButton(source: ActionSource.viewer),
asset.isLocalOnly
? const DeleteLocalActionButton(
source: ActionSource.viewer,
)
: const DeleteActionButton(
source: ActionSource.viewer,
showConfirmation: true,
),
? const DeleteLocalActionButton(source: ActionSource.viewer)
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
];
return IgnorePointer(
@@ -64,9 +55,7 @@ class ViewerBottomBar extends ConsumerWidget {
data: context.themeData.copyWith(
iconTheme: const IconThemeData(size: 22, color: Colors.white),
textTheme: context.themeData.textTheme.copyWith(
labelLarge: context.themeData.textTheme.labelLarge?.copyWith(
color: Colors.white,
),
labelLarge: context.themeData.textTheme.labelLarge?.copyWith(color: Colors.white),
),
),
child: Container(
@@ -77,10 +66,7 @@ class ViewerBottomBar extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (asset.isVideo) const VideoControls(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: actions,
),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
],
),
),

View File

@@ -29,11 +29,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
final DraggableScrollableController? controller;
final double initialChildSize;
const AssetDetailBottomSheet({
this.controller,
this.initialChildSize = 0.35,
super.key,
});
const AssetDetailBottomSheet({this.controller, this.initialChildSize = 0.35, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -42,9 +38,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
return const SizedBox.shrink();
}
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
final isInLockedView = ref.watch(inLockedViewProvider);
@@ -58,9 +52,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
? const TrashActionButton(source: ActionSource.viewer)
: const DeletePermanentActionButton(source: ActionSource.viewer),
const DeleteActionButton(source: ActionSource.viewer),
const MoveToLockFolderActionButton(
source: ActionSource.viewer,
),
const MoveToLockFolderActionButton(source: ActionSource.viewer),
],
if (asset.storage == AssetState.local) ...[
const DeleteLocalActionButton(source: ActionSource.viewer),
@@ -153,9 +145,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
// Asset Date and Time
_SheetTile(
title: _getDateTime(context, asset),
titleStyle: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
),
const SheetLocationDetails(),
// Details header
@@ -185,11 +175,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
_SheetTile(
title: cameraTitle,
titleStyle: context.textTheme.labelLarge,
leading: Icon(
Icons.camera_outlined,
size: 24,
color: context.textTheme.labelLarge?.color,
),
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
subtitle: _getCameraInfoSubtitle(exifInfo),
subtitleStyle: context.textTheme.bodyMedium?.copyWith(
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
@@ -207,13 +193,7 @@ class _SheetTile extends StatelessWidget {
final TextStyle? titleStyle;
final TextStyle? subtitleStyle;
const _SheetTile({
required this.title,
this.titleStyle,
this.leading,
this.subtitle,
this.subtitleStyle,
});
const _SheetTile({required this.title, this.titleStyle, this.leading, this.subtitle, this.subtitleStyle});
@override
Widget build(BuildContext context) {

View File

@@ -38,20 +38,13 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
_mapController = controller;
}
void _onExifChanged(
AsyncValue<ExifInfo?>? previous,
AsyncValue<ExifInfo?> current,
) {
void _onExifChanged(AsyncValue<ExifInfo?>? previous, AsyncValue<ExifInfo?> current) {
asset = ref.read(currentAssetNotifier);
setState(() {
exifInfo = current.valueOrNull;
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
if (exifInfo != null && hasCoordinates) {
_mapController?.moveCamera(
CameraUpdate.newLatLng(
LatLng(exifInfo!.latitude!, exifInfo!.longitude!),
),
);
_mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(exifInfo!.latitude!, exifInfo!.longitude!)));
}
});
}
@@ -59,11 +52,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
@override
void initState() {
super.initState();
ref.listenManual(
currentAssetExifProvider,
_onExifChanged,
fireImmediately: true,
);
ref.listenManual(currentAssetExifProvider, _onExifChanged, fireImmediately: true);
}
@override
@@ -80,10 +69,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
final coordinates = "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}";
return Padding(
padding: EdgeInsets.symmetric(
vertical: 16.0,
horizontal: context.isMobile ? 16.0 : 56.0,
),
padding: EdgeInsets.symmetric(vertical: 16.0, horizontal: context.isMobile ? 16.0 : 56.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -97,25 +83,16 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
),
),
),
ExifMap(
exifInfo: exifInfo!,
markerId: remoteId,
onMapCreated: _onMapCreated,
),
ExifMap(exifInfo: exifInfo!, markerId: remoteId, onMapCreated: _onMapCreated),
const SizedBox(height: 15),
if (locationName != null)
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
locationName,
style: context.textTheme.labelLarge,
),
child: Text(locationName, style: context.textTheme.labelLarge),
),
Text(
coordinates,
style: context.textTheme.labelMedium?.copyWith(
color: context.textTheme.labelMedium?.color?.withAlpha(150),
),
style: context.textTheme.labelMedium?.copyWith(color: context.textTheme.labelMedium?.color?.withAlpha(150)),
),
],
),

View File

@@ -36,25 +36,18 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final showViewInTimelineButton = previousRouteName != TabShellRoute.name && previousRouteName != null;
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
int opacity = ref.watch(
assetViewerProvider.select((state) => state.backgroundOpacity),
);
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
if (!showControls) {
opacity = 0;
}
final isCasting = ref.watch(
castProvider.select((c) => c.isCasting),
);
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
final websocketConnected = ref.watch(websocketProvider.select((c) => c.isConnected));
final actions = <Widget>[
if (isCasting || (asset.hasRemote && websocketConnected))
const CastActionButton(
menuItem: true,
),
if (isCasting || (asset.hasRemote && websocketConnected)) const CastActionButton(menuItem: true),
if (showViewInTimelineButton)
IconButton(
onPressed: () async {
@@ -68,19 +61,13 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
if (asset.hasRemote && isOwner && !asset.isFavorite)
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true),
if (asset.hasRemote && isOwner && asset.isFavorite)
const UnFavoriteActionButton(
source: ActionSource.viewer,
menuItem: true,
),
const UnFavoriteActionButton(source: ActionSource.viewer, menuItem: true),
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
const _KebabMenu(),
];
final lockedViewActions = <Widget>[
if (isCasting || (asset.hasRemote && websocketConnected))
const CastActionButton(
menuItem: true,
),
if (isCasting || (asset.hasRemote && websocketConnected)) const CastActionButton(menuItem: true),
const _KebabMenu(),
];
@@ -98,8 +85,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
actions: isShowingSheet
? null
: isInLockedView
? lockedViewActions
: actions,
? lockedViewActions
: actions,
),
),
);

View File

@@ -27,10 +27,7 @@ import 'package:logging/logging.dart';
import 'package:native_video_player/native_video_player.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
bool _isCurrentAsset(
BaseAsset asset,
BaseAsset? currentAsset,
) {
bool _isCurrentAsset(BaseAsset asset, BaseAsset? currentAsset) {
if (asset is RemoteAsset) {
return switch (currentAsset) {
RemoteAsset remoteAsset => remoteAsset.id == asset.id,
@@ -98,10 +95,7 @@ class NativeVideoViewer extends HookConsumerWidget {
throw Exception('No file found for the video');
}
final source = await VideoSource.init(
path: file.path,
type: VideoSourceType.file,
);
final source = await VideoSource.init(path: file.path, type: VideoSourceType.file);
return source;
}
@@ -122,31 +116,24 @@ class NativeVideoViewer extends HookConsumerWidget {
);
return source;
} catch (error) {
log.severe(
'Error creating video source for asset ${asset.name}: $error',
);
log.severe('Error creating video source for asset ${asset.name}: $error');
return null;
}
}
final videoSource = useMemoized<Future<VideoSource?>>(() => createSource());
final aspectRatio = useState<double?>(null);
useMemoized(
() async {
if (!context.mounted || aspectRatio.value != null) {
return null;
}
useMemoized(() async {
if (!context.mounted || aspectRatio.value != null) {
return null;
}
try {
aspectRatio.value = await ref.read(assetServiceProvider).getAspectRatio(asset);
} catch (error) {
log.severe(
'Error getting aspect ratio for asset ${asset.name}: $error',
);
}
},
[asset.heroTag],
);
try {
aspectRatio.value = await ref.read(assetServiceProvider).getAspectRatio(asset);
} catch (error) {
log.severe('Error getting aspect ratio for asset ${asset.name}: $error');
}
}, [asset.heroTag]);
void checkIfBuffering() {
if (!context.mounted) {
@@ -156,8 +143,9 @@ class NativeVideoViewer extends HookConsumerWidget {
final videoPlayback = ref.read(videoPlaybackValueProvider);
if ((isBuffering.value || videoPlayback.state == VideoPlaybackState.initializing) &&
videoPlayback.state != VideoPlaybackState.buffering) {
ref.read(videoPlaybackValueProvider.notifier).value =
videoPlayback.copyWith(state: VideoPlaybackState.buffering);
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback.copyWith(
state: VideoPlaybackState.buffering,
);
}
}
@@ -345,48 +333,42 @@ class NativeVideoViewer extends HookConsumerWidget {
// This delay seems like a hacky way to resolve underlying bugs in video
// playback, but other resolutions failed thus far
Timer(
Platform.isIOS
? Duration(milliseconds: 300 * playbackDelayFactor)
: imageToVideo
? Duration(milliseconds: 200 * playbackDelayFactor)
: Duration(milliseconds: 400 * playbackDelayFactor), () {
if (!context.mounted) {
return;
}
currentAsset.value = value;
if (currentAsset.value == asset) {
onPlaybackReady();
}
});
});
useEffect(
() {
// If opening a remote video from a hero animation, delay visibility to avoid a stutter
final timer = isVisible.value
? null
: Timer(
const Duration(milliseconds: 300),
() => isVisible.value = true,
);
return () {
timer?.cancel();
final playerController = controller.value;
if (playerController == null) {
Platform.isIOS
? Duration(milliseconds: 300 * playbackDelayFactor)
: imageToVideo
? Duration(milliseconds: 200 * playbackDelayFactor)
: Duration(milliseconds: 400 * playbackDelayFactor),
() {
if (!context.mounted) {
return;
}
removeListeners(playerController);
playerController.stop().catchError((error) {
log.fine('Error stopping video: $error');
});
WakelockPlus.disable();
};
},
const [],
);
currentAsset.value = value;
if (currentAsset.value == asset) {
onPlaybackReady();
}
},
);
});
useEffect(() {
// If opening a remote video from a hero animation, delay visibility to avoid a stutter
final timer = isVisible.value ? null : Timer(const Duration(milliseconds: 300), () => isVisible.value = true);
return () {
timer?.cancel();
final playerController = controller.value;
if (playerController == null) {
return;
}
removeListeners(playerController);
playerController.stop().catchError((error) {
log.fine('Error stopping video: $error');
});
WakelockPlus.disable();
};
}, const []);
useOnAppLifecycleStateChange((_, state) async {
if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) {
@@ -416,12 +398,7 @@ class NativeVideoViewer extends HookConsumerWidget {
child: AspectRatio(
key: ValueKey(asset),
aspectRatio: aspectRatio.value!,
child: isCurrent
? NativeVideoPlayerView(
key: ValueKey(asset),
onViewReady: initController,
)
: null,
child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null,
),
),
),

View File

@@ -13,16 +13,11 @@ import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
class VideoViewerControls extends HookConsumerWidget {
final Duration hideTimerDuration;
const VideoViewerControls({
super.key,
this.hideTimerDuration = const Duration(seconds: 5),
});
const VideoViewerControls({super.key, this.hideTimerDuration = const Duration(seconds: 5)});
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetIsVideo = ref.watch(
currentAssetNotifier.select((asset) => asset != null && asset.isVideo),
);
final assetIsVideo = ref.watch(currentAssetNotifier.select((asset) => asset != null && asset.isVideo));
bool showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
final showBottomSheet = ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
if (showBottomSheet) {
@@ -33,20 +28,17 @@ class VideoViewerControls extends HookConsumerWidget {
final cast = ref.watch(castProvider);
// A timer to hide the controls
final hideTimer = useTimer(
hideTimerDuration,
() {
if (!context.mounted) {
return;
}
final state = ref.read(videoPlaybackValueProvider).state;
final hideTimer = useTimer(hideTimerDuration, () {
if (!context.mounted) {
return;
}
final state = ref.read(videoPlaybackValueProvider).state;
// Do not hide on paused
if (state != VideoPlaybackState.paused && state != VideoPlaybackState.completed && assetIsVideo) {
ref.read(assetViewerProvider.notifier).setControls(false);
}
},
);
// Do not hide on paused
if (state != VideoPlaybackState.paused && state != VideoPlaybackState.completed && assetIsVideo) {
ref.read(assetViewerProvider.notifier).setControls(false);
}
});
final showBuffering = state == VideoPlaybackState.buffering && !cast.isCasting;
/// Shows the controls and starts the timer to hide them
@@ -97,11 +89,7 @@ class VideoViewerControls extends HookConsumerWidget {
child: Stack(
children: [
if (showBuffering)
const Center(
child: DelayedLoadingIndicator(
fadeInDuration: Duration(milliseconds: 400),
),
)
const Center(child: DelayedLoadingIndicator(fadeInDuration: Duration(milliseconds: 400)))
else
GestureDetector(
onTap: () => ref.read(assetViewerProvider.notifier).setControls(false),

View File

@@ -11,11 +11,7 @@ class BackupToggleButton extends ConsumerStatefulWidget {
final VoidCallback onStart;
final VoidCallback onStop;
const BackupToggleButton({
super.key,
required this.onStart,
required this.onStop,
});
const BackupToggleButton({super.key, required this.onStart, required this.onStop});
@override
ConsumerState<BackupToggleButton> createState() => BackupToggleButtonState();
@@ -29,17 +25,12 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(seconds: 8),
vsync: this,
);
_animationController = AnimationController(duration: const Duration(seconds: 8), vsync: this);
_gradientAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
_gradientAnimation = Tween<double>(
begin: 0,
end: 1,
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_isEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
}
@@ -66,21 +57,13 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
@override
Widget build(BuildContext context) {
final enqueueCount = ref.watch(
driftBackupProvider.select((state) => state.enqueueCount),
);
final enqueueCount = ref.watch(driftBackupProvider.select((state) => state.enqueueCount));
final enqueueTotalCount = ref.watch(
driftBackupProvider.select((state) => state.enqueueTotalCount),
);
final enqueueTotalCount = ref.watch(driftBackupProvider.select((state) => state.enqueueTotalCount));
final isCanceling = ref.watch(
driftBackupProvider.select((state) => state.isCanceling),
);
final isCanceling = ref.watch(driftBackupProvider.select((state) => state.isCanceling));
final uploadTasks = ref.watch(
driftBackupProvider.select((state) => state.uploadItems),
);
final uploadTasks = ref.watch(driftBackupProvider.select((state) => state.uploadItems));
final isUploading = uploadTasks.isNotEmpty;
@@ -116,11 +99,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: context.primaryColor.withValues(alpha: 0.1),
blurRadius: 12,
offset: const Offset(0, 2),
),
BoxShadow(color: context.primaryColor.withValues(alpha: 0.1), blurRadius: 12, offset: const Offset(0, 2)),
],
),
child: Container(
@@ -151,18 +130,8 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
),
),
child: isUploading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Icon(
Icons.cloud_upload_outlined,
color: context.primaryColor,
size: 24,
),
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))
: Icon(Icons.cloud_upload_outlined, color: context.primaryColor, size: 24),
),
const SizedBox(width: 16),
Expanded(
@@ -185,10 +154,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
Text(
"queue_status".t(
context: context,
args: {
'count': enqueueCount.toString(),
'total': enqueueTotalCount.toString(),
},
args: {'count': enqueueCount.toString(), 'total': enqueueTotalCount.toString()},
),
style: context.textTheme.labelLarge?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
@@ -197,10 +163,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
if (isCanceling)
Row(
children: [
Text(
"canceling".t(),
style: context.textTheme.labelLarge,
),
Text("canceling".t(), style: context.textTheme.labelLarge),
const SizedBox(width: 4),
SizedBox(
width: 18,
@@ -215,10 +178,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
],
),
),
Switch.adaptive(
value: _isEnabled,
onChanged: (value) => isCanceling ? null : _onToggle(value),
),
Switch.adaptive(value: _isEnabled, onChanged: (value) => isCanceling ? null : _onToggle(value)),
],
),
),

View File

@@ -24,9 +24,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final multiselect = ref.watch(multiSelectProvider);
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
return BaseBottomSheet(
initialChildSize: 0.25,
@@ -41,14 +39,10 @@ class ArchiveBottomSheet extends ConsumerWidget {
const DownloadActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(
source: ActionSource.timeline,
),
: const DeletePermanentActionButton(source: ActionSource.timeline),
const EditDateTimeActionButton(),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(
source: ActionSource.timeline,
),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[

View File

@@ -73,9 +73,7 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
borderOnForeground: false,
clipBehavior: Clip.antiAlias,
elevation: 6.0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(18)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(18))),
margin: const EdgeInsets.symmetric(horizontal: 0),
child: CustomScrollView(
controller: scrollController,
@@ -89,11 +87,7 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
if (widget.actions.isNotEmpty)
SizedBox(
height: 115,
child: ListView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: widget.actions,
),
child: ListView(shrinkWrap: true, scrollDirection: Axis.horizontal, children: widget.actions),
),
if (widget.actions.isNotEmpty) ...[
const Divider(indent: 16, endIndent: 16),

View File

@@ -24,9 +24,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final multiselect = ref.watch(multiSelectProvider);
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
return BaseBottomSheet(
initialChildSize: 0.25,
@@ -41,14 +39,10 @@ class FavoriteBottomSheet extends ConsumerWidget {
const DownloadActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(
source: ActionSource.timeline,
),
: const DeletePermanentActionButton(source: ActionSource.timeline),
const EditDateTimeActionButton(),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(
source: ActionSource.timeline,
),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[

View File

@@ -31,9 +31,7 @@ class GeneralBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final multiselect = ref.watch(multiSelectProvider);
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
final selectedAssets = multiselect.selectedAssets;
@@ -41,24 +39,19 @@ class GeneralBottomSheet extends ConsumerWidget {
return;
}
final addedCount = await ref.read(remoteAlbumProvider.notifier).addAssets(
album.id,
selectedAssets.map((e) => (e as RemoteAsset).id).toList(),
);
final addedCount = await ref
.read(remoteAlbumProvider.notifier)
.addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList());
if (addedCount != selectedAssets.length) {
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_already_exists'.tr(
namedArgs: {"album": album.name},
),
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}),
);
} else {
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_added'.tr(
namedArgs: {"album": album.name},
),
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {"album": album.name}),
);
}
@@ -78,18 +71,14 @@ class GeneralBottomSheet extends ConsumerWidget {
const DownloadActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(
source: ActionSource.timeline,
),
: const DeletePermanentActionButton(source: ActionSource.timeline),
const DeleteActionButton(source: ActionSource.timeline),
if (multiselect.hasLocal || multiselect.hasMerged) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),
],
const EditDateTimeActionButton(),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(
source: ActionSource.timeline,
),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
@@ -99,9 +88,7 @@ class GeneralBottomSheet extends ConsumerWidget {
],
slivers: [
const AddToAlbumHeader(),
AlbumSelector(
onAlbumSelected: addAssetsToAlbum,
),
AlbumSelector(onAlbumSelected: addAssetsToAlbum),
],
);
}

View File

@@ -27,9 +27,7 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final multiselect = ref.watch(multiSelectProvider);
final isTrashEnable = ref.watch(
serverInfoProvider.select((state) => state.serverFeatures.trash),
);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
return BaseBottomSheet(
initialChildSize: 0.25,
@@ -44,24 +42,17 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
const DownloadActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(
source: ActionSource.timeline,
),
: const DeletePermanentActionButton(source: ActionSource.timeline),
const EditDateTimeActionButton(),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(
source: ActionSource.timeline,
),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),
const UploadActionButton(source: ActionSource.timeline),
],
RemoveFromAlbumActionButton(
source: ActionSource.timeline,
albumId: album.id,
),
RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: album.id),
],
);
}

View File

@@ -5,20 +5,12 @@ import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
ImageProvider getFullImageProvider(
BaseAsset asset, {
Size size = const Size(1080, 1920),
}) {
ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) {
// Create new provider and cache it
final ImageProvider provider;
if (_shouldUseLocalAsset(asset)) {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
provider = LocalFullImageProvider(
id: id,
name: asset.name,
size: size,
type: asset.type,
);
provider = LocalFullImageProvider(id: id, name: asset.name, size: size, type: asset.type);
} else {
final String assetId;
if (asset is LocalAsset && asset.hasRemote) {
@@ -34,15 +26,8 @@ ImageProvider getFullImageProvider(
return provider;
}
ImageProvider getThumbnailImageProvider({
BaseAsset? asset,
String? remoteId,
Size size = const Size.square(256),
}) {
assert(
asset != null || remoteId != null,
'Either asset or remoteId must be provided',
);
ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Size size = const Size.square(256)}) {
assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
if (remoteId != null) {
return RemoteThumbProvider(assetId: remoteId);
@@ -50,12 +35,7 @@ ImageProvider getThumbnailImageProvider({
if (_shouldUseLocalAsset(asset!)) {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
return LocalThumbProvider(
id: id,
updatedAt: asset.updatedAt,
name: asset.name,
size: size,
);
return LocalThumbProvider(id: id, updatedAt: asset.updatedAt, name: asset.name, size: size);
}
final String assetId;

View File

@@ -5,10 +5,7 @@ import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
class LocalAlbumThumbnail extends ConsumerWidget {
const LocalAlbumThumbnail({
super.key,
required this.albumId,
});
const LocalAlbumThumbnail({super.key, required this.albumId});
final String albumId;
@override
@@ -21,34 +18,21 @@ class LocalAlbumThumbnail extends ConsumerWidget {
decoration: BoxDecoration(
color: context.colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(Radius.circular(16)),
border: Border.all(
color: context.colorScheme.outline.withAlpha(50),
width: 1,
),
),
child: Icon(
Icons.collections,
size: 24,
color: context.primaryColor,
border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1),
),
child: Icon(Icons.collections, size: 24, color: context.primaryColor),
);
}
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Thumbnail(
asset: data,
),
child: Thumbnail(asset: data),
);
},
error: (error, stack) {
return const Icon(Icons.error, size: 24);
},
loading: () => const SizedBox(
width: 24,
height: 24,
child: Center(child: CircularProgressIndicator()),
),
loading: () => const SizedBox(width: 24, height: 24, child: Center(child: CircularProgressIndicator())),
);
}
}

View File

@@ -39,10 +39,7 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
}
@override
ImageStreamCompleter loadImage(
LocalThumbProvider key,
ImageDecoderCallback decode,
) {
ImageStreamCompleter loadImage(LocalThumbProvider key, ImageDecoderCallback decode) {
final cache = cacheManager ?? ThumbnailImageCacheManager();
return MultiFrameImageStreamCompleter(
codec: _codec(key, cache, decode),
@@ -57,11 +54,7 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
);
}
Future<Codec> _codec(
LocalThumbProvider key,
CacheManager cache,
ImageDecoderCallback decode,
) async {
Future<Codec> _codec(LocalThumbProvider key, CacheManager cache, ImageDecoderCallback decode) async {
final cacheKey = '${key.id}-${key.updatedAt}-${key.size.width}x${key.size.height}';
final fileFromCache = await cache.getFileFromCache(cacheKey);
@@ -75,9 +68,7 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
final thumbnailBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
if (thumbnailBytes == null) {
PaintingBinding.instance.imageCache.evict(key);
throw StateError(
"Loading thumb for local photo ${key.name} failed",
);
throw StateError("Loading thumb for local photo ${key.name} failed");
}
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
@@ -107,12 +98,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
final Size size;
final AssetType type;
const LocalFullImageProvider({
required this.id,
required this.name,
required this.size,
required this.type,
});
const LocalFullImageProvider({required this.id, required this.name, required this.size, required this.type});
@override
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
@@ -120,10 +106,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
}
@override
ImageStreamCompleter loadImage(
LocalFullImageProvider key,
ImageDecoderCallback decode,
) {
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
return MultiImageStreamCompleter(
codec: _codec(key, decode),
scale: 1.0,
@@ -134,10 +117,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
}
// Streams in each stage of the image as we ask for it
Stream<Codec> _codec(
LocalFullImageProvider key,
ImageDecoderCallback decode,
) async* {
Stream<Codec> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
try {
switch (key.type) {
case AssetType.image:
@@ -156,16 +136,11 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
}
} catch (error, stack) {
Logger('ImmichLocalImageProvider').severe('Error loading local image ${key.name}', error, stack);
throw const ImageLoadingException(
'Could not load image from local storage',
);
throw const ImageLoadingException('Could not load image from local storage');
}
}
Future<Codec?> _getThumbnailCodec(
LocalFullImageProvider key,
ImageDecoderCallback decode,
) async {
Future<Codec?> _getThumbnailCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async {
final thumbBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
if (thumbBytes == null) {
return null;
@@ -174,10 +149,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
return decode(buffer);
}
Stream<Codec> _decodeProgressive(
LocalFullImageProvider key,
ImageDecoderCallback decode,
) async* {
Stream<Codec> _decodeProgressive(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
final file = await _storageRepository.getFileForAsset(key.id);
if (file == null) {
throw StateError("Opening file for asset ${key.name} failed");

View File

@@ -15,10 +15,7 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
final String assetId;
final CacheManager? cacheManager;
const RemoteThumbProvider({
required this.assetId,
this.cacheManager,
});
const RemoteThumbProvider({required this.assetId, this.cacheManager});
@override
Future<RemoteThumbProvider> obtainKey(ImageConfiguration configuration) {
@@ -26,10 +23,7 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
}
@override
ImageStreamCompleter loadImage(
RemoteThumbProvider key,
ImageDecoderCallback decode,
) {
ImageStreamCompleter loadImage(RemoteThumbProvider key, ImageDecoderCallback decode) {
final cache = cacheManager ?? RemoteImageCacheManager();
final chunkController = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
@@ -49,9 +43,7 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkController,
) async {
final preview = getThumbnailUrlForRemoteId(
key.assetId,
);
final preview = getThumbnailUrlForRemoteId(key.assetId);
return ImageLoader.loadImageFromCache(
preview,
@@ -79,10 +71,7 @@ class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
final String assetId;
final CacheManager? cacheManager;
const RemoteFullImageProvider({
required this.assetId,
this.cacheManager,
});
const RemoteFullImageProvider({required this.assetId, this.cacheManager});
@override
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
@@ -90,10 +79,7 @@ class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
}
@override
ImageStreamCompleter loadImage(
RemoteFullImageProvider key,
ImageDecoderCallback decode,
) {
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
final cache = cacheManager ?? RemoteImageCacheManager();
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter(

View File

@@ -8,9 +8,7 @@ import 'package:thumbhash/thumbhash.dart';
class ThumbHashProvider extends ImageProvider<ThumbHashProvider> {
final String thumbHash;
const ThumbHashProvider({
required this.thumbHash,
});
const ThumbHashProvider({required this.thumbHash});
@override
Future<ThumbHashProvider> obtainKey(ImageConfiguration configuration) {
@@ -18,20 +16,11 @@ class ThumbHashProvider extends ImageProvider<ThumbHashProvider> {
}
@override
ImageStreamCompleter loadImage(
ThumbHashProvider key,
ImageDecoderCallback decode,
) {
return MultiFrameImageStreamCompleter(
codec: _loadCodec(key, decode),
scale: 1.0,
);
ImageStreamCompleter loadImage(ThumbHashProvider key, ImageDecoderCallback decode) {
return MultiFrameImageStreamCompleter(codec: _loadCodec(key, decode), scale: 1.0);
}
Future<Codec> _loadCodec(
ThumbHashProvider key,
ImageDecoderCallback decode,
) async {
Future<Codec> _loadCodec(ThumbHashProvider key, ImageDecoderCallback decode) async {
final image = thumbHashToRGBA(base64Decode(key.thumbHash));
return decode(await ImmutableBuffer.fromUint8List(rgbaToBmp(image)));
}

View File

@@ -8,16 +8,8 @@ import 'package:logging/logging.dart';
import 'package:octo_image/octo_image.dart';
class Thumbnail extends StatelessWidget {
const Thumbnail({
this.asset,
this.remoteId,
this.size = const Size.square(256),
this.fit = BoxFit.cover,
super.key,
}) : assert(
asset != null || remoteId != null,
'Either asset or remoteId must be provided',
);
const Thumbnail({this.asset, this.remoteId, this.size = const Size.square(256), this.fit = BoxFit.cover, super.key})
: assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
final BaseAsset? asset;
final String? remoteId;
@@ -33,12 +25,7 @@ class Thumbnail extends StatelessWidget {
image: provider,
octoSet: OctoSet(
placeholderBuilder: _blurHashPlaceholderBuilder(thumbHash, fit: fit),
errorBuilder: _blurHashErrorBuilder(
thumbHash,
provider: provider,
fit: fit,
asset: asset,
),
errorBuilder: _blurHashErrorBuilder(thumbHash, provider: provider, fit: fit, asset: asset),
),
fadeOutDuration: const Duration(milliseconds: 100),
fadeInDuration: Duration.zero,
@@ -50,10 +37,7 @@ class Thumbnail extends StatelessWidget {
}
}
OctoPlaceholderBuilder _blurHashPlaceholderBuilder(
String? thumbHash, {
BoxFit? fit,
}) {
OctoPlaceholderBuilder _blurHashPlaceholderBuilder(String? thumbHash, {BoxFit? fit}) {
return (context) => thumbHash == null
? const ThumbnailPlaceholder()
: FadeInPlaceholderImage(
@@ -63,12 +47,7 @@ OctoPlaceholderBuilder _blurHashPlaceholderBuilder(
);
}
OctoErrorBuilder _blurHashErrorBuilder(
String? blurhash, {
BaseAsset? asset,
ImageProvider? provider,
BoxFit? fit,
}) =>
OctoErrorBuilder _blurHashErrorBuilder(String? blurhash, {BaseAsset? asset, ImageProvider? provider, BoxFit? fit}) =>
(context, e, s) {
Logger("ImThumbnail").warning("Error loading thumbnail for ${asset?.name}", e, s);
provider?.evict();
@@ -76,10 +55,7 @@ OctoErrorBuilder _blurHashErrorBuilder(
alignment: Alignment.center,
children: [
_blurHashPlaceholderBuilder(blurhash, fit: fit)(context),
const Opacity(
opacity: 0.75,
child: Icon(Icons.error_outline_rounded),
),
const Opacity(opacity: 0.75, child: Icon(Icons.error_outline_rounded)),
],
);
};

View File

@@ -30,29 +30,25 @@ class ThumbnailTile extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final heroIndex = heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
final assetContainerColor =
context.isDarkTheme ? context.primaryColor.darken(amount: 0.4) : context.primaryColor.lighten(amount: 0.75);
final assetContainerColor = context.isDarkTheme
? context.primaryColor.darken(amount: 0.4)
: context.primaryColor.lighten(amount: 0.75);
final isSelected = ref.watch(
multiSelectProvider.select(
(multiselect) => multiselect.selectedAssets.contains(asset),
),
multiSelectProvider.select((multiselect) => multiselect.selectedAssets.contains(asset)),
);
final borderStyle = lockSelection
? BoxDecoration(
color: context.colorScheme.surfaceContainerHighest,
border: Border.all(
color: context.colorScheme.surfaceContainerHighest,
width: 6,
),
border: Border.all(color: context.colorScheme.surfaceContainerHighest, width: 6),
)
: isSelected
? BoxDecoration(
color: assetContainerColor,
border: Border.all(color: assetContainerColor, width: 6),
)
: const BoxDecoration();
? BoxDecoration(
color: assetContainerColor,
border: Border.all(color: assetContainerColor, width: 6),
)
: const BoxDecoration();
final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
@@ -63,28 +59,22 @@ class ThumbnailTile extends ConsumerWidget {
curve: Curves.decelerate,
decoration: borderStyle,
child: ClipRRect(
borderRadius:
isSelected || lockSelection ? const BorderRadius.all(Radius.circular(15.0)) : BorderRadius.zero,
borderRadius: isSelected || lockSelection
? const BorderRadius.all(Radius.circular(15.0))
: BorderRadius.zero,
child: Stack(
children: [
Positioned.fill(
child: Hero(
tag: '${asset.heroTag}_$heroIndex',
child: Thumbnail(
asset: asset,
fit: fit,
size: size,
),
child: Thumbnail(asset: asset, fit: fit, size: size),
),
),
if (hasStack)
Align(
alignment: Alignment.topRight,
child: Padding(
padding: EdgeInsets.only(
right: 10.0,
top: asset.isVideo ? 24.0 : 6.0,
),
padding: EdgeInsets.only(right: 10.0, top: asset.isVideo ? 24.0 : 6.0),
child: const _TileOverlayIcon(Icons.burst_mode_rounded),
),
),
@@ -99,26 +89,26 @@ class ThumbnailTile extends ConsumerWidget {
if (showStorageIndicator)
switch (asset.storage) {
AssetState.local => const Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_off_outlined),
),
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_off_outlined),
),
),
AssetState.remote => const Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_outlined),
),
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_outlined),
),
),
AssetState.merged => const Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_done_outlined),
),
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.cloud_done_outlined),
),
),
},
if (asset.isFavorite)
const Align(
@@ -154,41 +144,22 @@ class _SelectionIndicator extends StatelessWidget {
final bool isLocked;
final Color? color;
const _SelectionIndicator({
required this.isSelected,
required this.isLocked,
this.color,
});
const _SelectionIndicator({required this.isSelected, required this.isLocked, this.color});
@override
Widget build(BuildContext context) {
if (isLocked) {
return DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
child: const Icon(
Icons.check_circle_rounded,
color: Colors.grey,
),
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
child: const Icon(Icons.check_circle_rounded, color: Colors.grey),
);
} else if (isSelected) {
return DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
child: Icon(
Icons.check_circle_rounded,
color: context.primaryColor,
),
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
child: Icon(Icons.check_circle_rounded, color: context.primaryColor),
);
} else {
return const Icon(
Icons.circle_outlined,
color: Colors.white,
);
return const Icon(Icons.circle_outlined, color: Colors.white);
}
}
}
@@ -212,12 +183,7 @@ class _VideoIndicator extends StatelessWidget {
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
shadows: [
Shadow(
blurRadius: 5.0,
color: Color.fromRGBO(0, 0, 0, 0.6),
),
],
shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6))],
),
),
const _TileOverlayIcon(Icons.play_circle_outline_rounded),
@@ -237,13 +203,7 @@ class _TileOverlayIcon extends StatelessWidget {
icon,
color: Colors.white,
size: 16,
shadows: [
const Shadow(
blurRadius: 5.0,
color: Color.fromRGBO(0, 0, 0, 0.6),
offset: Offset(0.0, 0.0),
),
],
shadows: [const Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
);
}
}

View File

@@ -11,11 +11,7 @@ import 'package:immich_mobile/routing/router.dart';
class DriftMemoryBottomInfo extends StatelessWidget {
final DriftMemory memory;
final String title;
const DriftMemoryBottomInfo({
super.key,
required this.memory,
required this.title,
});
const DriftMemoryBottomInfo({super.key, required this.memory, required this.title});
@override
Widget build(BuildContext context) {
@@ -23,47 +19,39 @@ class DriftMemoryBottomInfo extends StatelessWidget {
final fileCreatedDate = memory.assets.first.createdAt;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
color: Colors.grey[400],
fontSize: 13.0,
fontWeight: FontWeight.w500,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(color: Colors.grey[400], fontSize: 13.0, fontWeight: FontWeight.w500),
),
),
Text(
df.format(fileCreatedDate),
style: const TextStyle(
color: Colors.white,
fontSize: 15.0,
fontWeight: FontWeight.w500,
Text(
df.format(fileCreatedDate),
style: const TextStyle(color: Colors.white, fontSize: 15.0, fontWeight: FontWeight.w500),
),
),
],
),
Tooltip(
message: 'view_in_timeline'.tr(),
child: MaterialButton(
minWidth: 0,
onPressed: () async {
await context.maybePop();
await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
EventStream.shared.emit(ScrollToDateEvent(fileCreatedDate));
},
shape: const CircleBorder(),
color: Colors.white.withValues(alpha: 0.2),
elevation: 0,
child: const Icon(
Icons.open_in_new,
color: Colors.white,
],
),
Tooltip(
message: 'view_in_timeline'.tr(),
child: MaterialButton(
minWidth: 0,
onPressed: () async {
await context.maybePop();
await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
EventStream.shared.emit(ScrollToDateEvent(fileCreatedDate));
},
shape: const CircleBorder(),
color: Colors.white.withValues(alpha: 0.2),
elevation: 0,
child: const Icon(Icons.open_in_new, color: Colors.white),
),
),
),
]),
],
),
);
}
}

View File

@@ -29,17 +29,12 @@ class DriftMemoryCard extends StatelessWidget {
color: Colors.black,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(25.0)),
side: BorderSide(
color: Colors.black,
width: 1.0,
),
side: BorderSide(color: Colors.black, width: 1.0),
),
clipBehavior: Clip.hardEdge,
child: Stack(
children: [
SizedBox.expand(
child: _BlurredBackdrop(asset: asset),
),
SizedBox.expand(child: _BlurredBackdrop(asset: asset)),
LayoutBuilder(
builder: (context, constraints) {
// Determine the fit using the aspect ratio
@@ -55,11 +50,7 @@ class DriftMemoryCard extends StatelessWidget {
}
if (asset.isImage) {
return FullImage(
asset,
fit: fit,
size: const Size(double.infinity, double.infinity),
);
return FullImage(asset, fit: fit, size: const Size(double.infinity, double.infinity));
} else {
return SizedBox(
width: context.width,
@@ -69,11 +60,7 @@ class DriftMemoryCard extends StatelessWidget {
asset: asset,
showControls: false,
playbackDelayFactor: 2,
image: FullImage(
asset,
size: Size(context.width, context.height),
fit: BoxFit.contain,
),
image: FullImage(asset, size: Size(context.width, context.height), fit: BoxFit.contain),
),
);
}
@@ -85,10 +72,7 @@ class DriftMemoryCard extends StatelessWidget {
bottom: 18.0,
child: Text(
title,
style: context.textTheme.headlineMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w500,
),
style: context.textTheme.headlineMedium?.copyWith(color: Colors.white, fontWeight: FontWeight.w500),
),
),
],
@@ -109,16 +93,9 @@ class _BlurredBackdrop extends HookWidget {
// Use a nice cheap blur hash image decoration
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: MemoryImage(
blurhash,
),
fit: BoxFit.cover,
),
),
child: Container(
color: Colors.black.withValues(alpha: 0.2),
image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover),
),
child: Container(color: Colors.black.withValues(alpha: 0.2)),
);
} else {
// Fall back to using a more expensive image filtered
@@ -129,16 +106,11 @@ class _BlurredBackdrop extends HookWidget {
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: getFullImageProvider(
asset,
size: Size(context.width, context.height),
),
image: getFullImageProvider(asset, size: Size(context.width, context.height)),
fit: BoxFit.cover,
),
),
child: Container(
color: Colors.black.withValues(alpha: 0.2),
),
child: Container(color: Colors.black.withValues(alpha: 0.2)),
),
);
}

View File

@@ -17,17 +17,13 @@ class DriftMemoryLane extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 200,
),
constraints: const BoxConstraints(maxHeight: 200),
child: CarouselView(
itemExtent: 145.0,
shrinkExtent: 1.0,
elevation: 2,
backgroundColor: Colors.black,
overlayColor: WidgetStateProperty.all(
Colors.white.withValues(alpha: 0.1),
),
overlayColor: WidgetStateProperty.all(Colors.white.withValues(alpha: 0.1)),
onTap: (index) {
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
@@ -40,12 +36,7 @@ class DriftMemoryLane extends ConsumerWidget {
}
}
context.pushRoute(
DriftMemoryRoute(
memories: memories,
memoryIndex: index,
),
);
context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index));
},
children: memories.map((memory) => DriftMemoryCard(memory: memory)).toList(),
),
@@ -54,53 +45,33 @@ class DriftMemoryLane extends ConsumerWidget {
}
class DriftMemoryCard extends ConsumerWidget {
const DriftMemoryCard({
super.key,
required this.memory,
});
const DriftMemoryCard({super.key, required this.memory});
final DriftMemory memory;
@override
Widget build(BuildContext context, WidgetRef ref) {
final yearsAgo = DateTime.now().year - memory.data.year;
final title = 'years_ago'.t(
context: context,
args: {
'years': yearsAgo.toString(),
},
);
final title = 'years_ago'.t(context: context, args: {'years': yearsAgo.toString()});
return Center(
child: Stack(
children: [
ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withValues(alpha: 0.2),
BlendMode.darken,
),
colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.2), BlendMode.darken),
child: SizedBox(
width: 205,
height: 200,
child: Thumbnail(
remoteId: memory.assets[0].id,
fit: BoxFit.cover,
),
child: Thumbnail(remoteId: memory.assets[0].id, fit: BoxFit.cover),
),
),
Positioned(
bottom: 16,
left: 16,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 114,
),
constraints: const BoxConstraints(maxWidth: 114),
child: Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Colors.white,
fontSize: 15,
),
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 15),
),
),
),

View File

@@ -25,9 +25,7 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: FontWeight.w600,
);
TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w600);
return SafeArea(
child: Padding(
@@ -38,72 +36,46 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
if (onEditAlbum != null)
ListTile(
leading: const Icon(Icons.edit),
title: Text(
'edit_album'.t(context: context),
style: textStyle,
),
title: Text('edit_album'.t(context: context), style: textStyle),
onTap: onEditAlbum,
),
if (onAddPhotos != null)
ListTile(
leading: const Icon(Icons.add_a_photo),
title: Text(
'add_photos'.t(context: context),
style: textStyle,
),
title: Text('add_photos'.t(context: context), style: textStyle),
onTap: onAddPhotos,
),
if (onAddUsers != null)
ListTile(
leading: const Icon(Icons.group_add),
title: Text(
'album_viewer_page_share_add_users'.t(context: context),
style: textStyle,
),
title: Text('album_viewer_page_share_add_users'.t(context: context), style: textStyle),
onTap: onAddUsers,
),
if (onLeaveAlbum != null)
ListTile(
leading: const Icon(Icons.person_remove_rounded),
title: Text(
'leave_album'.t(context: context),
style: textStyle,
),
title: Text('leave_album'.t(context: context), style: textStyle),
onTap: onLeaveAlbum,
),
if (onToggleAlbumOrder != null)
ListTile(
leading: const Icon(Icons.swap_vert_rounded),
title: Text(
'change_display_order'.t(context: context),
style: textStyle,
),
title: Text('change_display_order'.t(context: context), style: textStyle),
onTap: onToggleAlbumOrder,
),
if (onCreateSharedLink != null)
ListTile(
leading: const Icon(Icons.link),
title: Text(
'create_shared_link'.t(context: context),
style: textStyle,
),
title: Text('create_shared_link'.t(context: context), style: textStyle),
onTap: onCreateSharedLink,
),
if (onDeleteAlbum != null) ...[
const Divider(
indent: 16,
endIndent: 16,
),
const Divider(indent: 16, endIndent: 16),
ListTile(
leading: Icon(
Icons.delete,
color: context.isDarkTheme ? Colors.red[400] : Colors.red[800],
),
leading: Icon(Icons.delete, color: context.isDarkTheme ? Colors.red[400] : Colors.red[800]),
title: Text(
'delete_album'.t(context: context),
style: textStyle.copyWith(
color: context.isDarkTheme ? Colors.red[400] : Colors.red[800],
),
style: textStyle.copyWith(color: context.isDarkTheme ? Colors.red[400] : Colors.red[800]),
),
onTap: onDeleteAlbum,
),

View File

@@ -16,11 +16,7 @@ class FixedTimelineRow extends MultiChildRenderObjectWidget {
@override
RenderObject createRenderObject(BuildContext context) {
return RenderFixedRow(
dimension: dimension,
spacing: spacing,
textDirection: textDirection,
);
return RenderFixedRow(dimension: dimension, spacing: spacing, textDirection: textDirection);
}
@override
@@ -50,9 +46,9 @@ class RenderFixedRow extends RenderBox
required double dimension,
required double spacing,
required TextDirection textDirection,
}) : _dimension = dimension,
_spacing = spacing,
_textDirection = textDirection {
}) : _dimension = dimension,
_spacing = spacing,
_textDirection = textDirection {
addAll(children);
}

View File

@@ -33,8 +33,8 @@ class FixedSegment extends Segment {
required super.headerExtent,
required super.spacing,
required super.header,
}) : assert(tileHeight != 0),
mainAxisExtend = tileHeight + spacing;
}) : assert(tileHeight != 0),
mainAxisExtend = tileHeight + spacing;
@override
double indexToLayoutOffset(int index) {
@@ -64,12 +64,7 @@ class FixedSegment extends Segment {
final numberOfAssets = math.min(columnCount, assetCount - assetIndex);
if (index == firstIndex) {
return TimelineHeader(
bucket: bucket,
header: header,
height: headerExtent,
assetOffset: firstAssetIndex,
);
return TimelineHeader(bucket: bucket, header: header, height: headerExtent, assetOffset: firstAssetIndex);
}
return _FixedSegmentRow(
@@ -104,10 +99,7 @@ class _FixedSegmentRow extends ConsumerWidget {
}
if (timelineService.hasRange(assetIndex, assetCount)) {
return _buildAssetRow(
context,
timelineService.getAssets(assetIndex, assetCount),
);
return _buildAssetRow(context, timelineService.getAssets(assetIndex, assetCount));
}
return FutureBuilder<List<BaseAsset>>(
@@ -122,12 +114,7 @@ class _FixedSegmentRow extends ConsumerWidget {
}
Widget _buildPlaceholder(BuildContext context) {
return SegmentBuilder.buildPlaceholder(
context,
assetCount,
size: Size.square(tileHeight),
spacing: spacing,
);
return SegmentBuilder.buildPlaceholder(context, assetCount, size: Size.square(tileHeight), spacing: spacing);
}
Widget _buildAssetRow(BuildContext context, List<BaseAsset> assets) {
@@ -137,11 +124,7 @@ class _FixedSegmentRow extends ConsumerWidget {
textDirection: Directionality.of(context),
children: [
for (int i = 0; i < assets.length; i++)
_AssetTileWidget(
key: ValueKey(assets[i].heroTag),
asset: assets[i],
assetIndex: assetIndex + i,
),
_AssetTileWidget(key: ValueKey(assets[i].heroTag), asset: assets[i], assetIndex: assetIndex + i),
],
);
}
@@ -151,19 +134,9 @@ class _AssetTileWidget extends ConsumerWidget {
final BaseAsset asset;
final int assetIndex;
const _AssetTileWidget({
super.key,
required this.asset,
required this.assetIndex,
});
const _AssetTileWidget({super.key, required this.asset, required this.assetIndex});
Future _handleOnTap(
BuildContext ctx,
WidgetRef ref,
int assetIndex,
BaseAsset asset,
int? heroOffset,
) async {
Future _handleOnTap(BuildContext ctx, WidgetRef ref, int assetIndex, BaseAsset asset, int? heroOffset) async {
final multiSelectState = ref.read(multiSelectProvider);
if (multiSelectState.forceEnable || multiSelectState.isEnabled) {
@@ -192,11 +165,7 @@ class _AssetTileWidget extends ConsumerWidget {
}
bool _getLockSelectionStatus(WidgetRef ref) {
final lockSelectionAssets = ref.read(
multiSelectProvider.select(
(state) => state.lockedSelectionAssets,
),
);
final lockSelectionAssets = ref.read(multiSelectProvider.select((state) => state.lockedSelectionAssets));
if (lockSelectionAssets.isEmpty) {
return false;
@@ -210,9 +179,7 @@ class _AssetTileWidget extends ConsumerWidget {
final heroOffset = TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
final lockSelection = _getLockSelectionStatus(ref);
final showStorageIndicator = ref.watch(
timelineArgsProvider.select((args) => args.showStorageIndicator),
);
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
return RepaintBoundary(
child: GestureDetector(

View File

@@ -35,8 +35,7 @@ class FixedSegmentBuilder extends SegmentBuilder {
final timelineHeader = switch (groupBy) {
GroupAssetsBy.month => HeaderType.month,
GroupAssetsBy.day ||
GroupAssetsBy.auto =>
GroupAssetsBy.day || GroupAssetsBy.auto =>
bucket is TimeBucket && bucket.date.month != previousDate?.month ? HeaderType.monthAndDay : HeaderType.day,
GroupAssetsBy.none => HeaderType.none,
};

View File

@@ -47,11 +47,7 @@ class TimelineHeader extends StatelessWidget {
final isDayHeader = header == HeaderType.day || header == HeaderType.monthAndDay;
return Padding(
padding: EdgeInsets.only(
top: isMonthHeader ? 8.0 : 0.0,
left: 12.0,
right: 12.0,
),
padding: EdgeInsets.only(top: isMonthHeader ? 8.0 : 0.0, left: 12.0, right: 12.0),
child: SizedBox(
height: height,
child: Column(
@@ -61,32 +57,17 @@ class TimelineHeader extends StatelessWidget {
if (isMonthHeader)
Row(
children: [
Text(
_formatMonth(context, date),
style: context.textTheme.labelLarge?.copyWith(fontSize: 24),
),
Text(_formatMonth(context, date), style: context.textTheme.labelLarge?.copyWith(fontSize: 24)),
const Spacer(),
if (header != HeaderType.monthAndDay)
_BulkSelectIconButton(
bucket: bucket,
assetOffset: assetOffset,
),
if (header != HeaderType.monthAndDay) _BulkSelectIconButton(bucket: bucket, assetOffset: assetOffset),
],
),
if (isDayHeader)
Row(
children: [
Text(
_formatDay(context, date),
style: context.textTheme.labelLarge?.copyWith(
fontSize: 15,
),
),
Text(_formatDay(context, date), style: context.textTheme.labelLarge?.copyWith(fontSize: 15)),
const Spacer(),
_BulkSelectIconButton(
bucket: bucket,
assetOffset: assetOffset,
),
_BulkSelectIconButton(bucket: bucket, assetOffset: assetOffset),
],
),
],
@@ -100,10 +81,7 @@ class _BulkSelectIconButton extends ConsumerWidget {
final Bucket bucket;
final int assetOffset;
const _BulkSelectIconButton({
required this.bucket,
required this.assetOffset,
});
const _BulkSelectIconButton({required this.bucket, required this.assetOffset});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -118,23 +96,12 @@ class _BulkSelectIconButton extends ConsumerWidget {
return IconButton(
onPressed: () {
ref.read(multiSelectProvider.notifier).toggleBucketSelection(
assetOffset,
bucket.assetCount,
);
ref.read(multiSelectProvider.notifier).toggleBucketSelection(assetOffset, bucket.assetCount);
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
},
icon: isAllSelected
? Icon(
Icons.check_circle_rounded,
size: 26,
color: context.primaryColor,
)
: Icon(
Icons.check_circle_outline_rounded,
size: 26,
color: context.colorScheme.onSurfaceSecondary,
),
? Icon(Icons.check_circle_rounded, size: 26, color: context.primaryColor)
: Icon(Icons.check_circle_outline_rounded, size: 26, color: context.colorScheme.onSurfaceSecondary),
);
}
}

View File

@@ -43,10 +43,7 @@ class Scrubber extends ConsumerStatefulWidget {
ConsumerState createState() => ScrubberState();
}
List<_Segment> _buildSegments({
required List<Segment> layoutSegments,
required double timelineHeight,
}) {
List<_Segment> _buildSegments({required List<Segment> layoutSegments, required double timelineHeight}) {
const double offsetThreshold = 20.0;
final segments = <_Segment>[];
@@ -66,14 +63,7 @@ List<_Segment> _buildSegments({
final showSegment = lastOffset + offsetThreshold <= startOffset && (lastDate == null || date.year != lastDate.year);
segments.add(
_Segment(
date: date,
startOffset: startOffset,
scrollLabel: label,
showSegment: showSegment,
),
);
segments.add(_Segment(date: date, startOffset: startOffset, scrollLabel: label, showSegment: showSegment));
lastDate = date;
if (showSegment) {
lastOffset = startOffset;
@@ -109,27 +99,12 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
void initState() {
super.initState();
_isDragging = false;
_segments = _buildSegments(
layoutSegments: widget.layoutSegments,
timelineHeight: _scrubberHeight,
);
_thumbAnimationController = AnimationController(
vsync: this,
duration: kTimelineScrubberFadeInDuration,
);
_thumbAnimation = CurvedAnimation(
parent: _thumbAnimationController,
curve: Curves.fastEaseInToSlowEaseOut,
);
_labelAnimationController = AnimationController(
vsync: this,
duration: kTimelineScrubberFadeInDuration,
);
_segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight);
_thumbAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
_thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastEaseInToSlowEaseOut);
_labelAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
_labelAnimation = CurvedAnimation(
parent: _labelAnimationController,
curve: Curves.fastOutSlowIn,
);
_labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn);
}
@override
@@ -143,10 +118,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
super.didUpdateWidget(oldWidget);
if (oldWidget.layoutSegments.lastOrNull?.endOffset != widget.layoutSegments.lastOrNull?.endOffset) {
_segments = _buildSegments(
layoutSegments: widget.layoutSegments,
timelineHeight: _scrubberHeight,
);
_segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight);
}
}
@@ -276,12 +248,10 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
}
int _findLayoutSegmentIndex(_Segment segment) {
return widget.layoutSegments.indexWhere(
(layoutSegment) {
final bucket = layoutSegment.bucket as TimeBucket;
return bucket.date.year == segment.date.year && bucket.date.month == segment.date.month;
},
);
return widget.layoutSegments.indexWhere((layoutSegment) {
final bucket = layoutSegment.bucket as TimeBucket;
return bucket.date.year == segment.date.year && bucket.date.month == segment.date.month;
});
}
void _scrollToLayoutSegment(int layoutSegmentIndex) {
@@ -311,19 +281,13 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
if (_scrollController.hasClients == true) {
// Cache to avoid multiple calls to [_currentOffset]
final scrollOffset = _currentOffset;
final labelText = _segments
.lastWhereOrNull(
(segment) => segment.startOffset <= scrollOffset,
)
?.scrollLabel ??
final labelText =
_segments.lastWhereOrNull((segment) => segment.startOffset <= scrollOffset)?.scrollLabel ??
_segments.firstOrNull?.scrollLabel;
label = labelText != null
? Text(
labelText,
style: ctx.textTheme.bodyLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
style: ctx.textTheme.bodyLarge?.copyWith(color: Colors.white, fontWeight: FontWeight.bold),
)
: null;
}
@@ -351,11 +315,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
onVerticalDragStart: _onDragStart,
onVerticalDragUpdate: _onDragUpdate,
onVerticalDragEnd: _onDragEnd,
child: _Scrubber(
thumbAnimation: _thumbAnimation,
labelAnimation: _labelAnimation,
label: label,
),
child: _Scrubber(thumbAnimation: _thumbAnimation, labelAnimation: _labelAnimation, label: label),
),
),
),
@@ -370,12 +330,7 @@ class _SegmentsLayer extends StatelessWidget {
final double topPadding;
final bool isDragging;
const _SegmentsLayer({
super.key,
required this.segments,
required this.topPadding,
required this.isDragging,
});
const _SegmentsLayer({super.key, required this.segments, required this.topPadding, required this.isDragging});
@override
Widget build(BuildContext context) {
@@ -389,9 +344,7 @@ class _SegmentsLayer extends StatelessWidget {
key: ValueKey('segment_${segment.date.millisecondsSinceEpoch}'),
top: topPadding + segment.startOffset,
end: 100,
child: RepaintBoundary(
child: _SegmentWidget(segment),
),
child: RepaintBoundary(child: _SegmentWidget(segment)),
),
)
.toList(),
@@ -419,10 +372,7 @@ class _SegmentWidget extends StatelessWidget {
alignment: Alignment.center,
child: Text(
_segment.date.year.toString(),
style: context.textTheme.labelMedium?.copyWith(
fontFamily: "OverpassMono",
fontWeight: FontWeight.w600,
),
style: context.textTheme.labelMedium?.copyWith(fontFamily: "OverpassMono", fontWeight: FontWeight.w600),
),
),
),
@@ -436,11 +386,7 @@ class _ScrollLabel extends StatelessWidget {
final Color backgroundColor;
final Animation<double> animation;
const _ScrollLabel({
required this.label,
required this.backgroundColor,
required this.animation,
});
const _ScrollLabel({required this.label, required this.backgroundColor, required this.animation});
@override
Widget build(BuildContext context) {
@@ -471,16 +417,13 @@ class _Scrubber extends StatelessWidget {
final Animation<double> thumbAnimation;
final Animation<double> labelAnimation;
const _Scrubber({
this.label,
required this.thumbAnimation,
required this.labelAnimation,
});
const _Scrubber({this.label, required this.thumbAnimation, required this.labelAnimation});
@override
Widget build(BuildContext context) {
final backgroundColor =
context.isDarkTheme ? context.colorScheme.primary.darken(amount: .5) : context.colorScheme.primary;
final backgroundColor = context.isDarkTheme
? context.colorScheme.primary.darken(amount: .5)
: context.colorScheme.primary;
return _SlideFadeTransition(
animation: thumbAnimation,
@@ -488,12 +431,7 @@ class _Scrubber extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (label != null)
_ScrollLabel(
label: label!,
backgroundColor: backgroundColor,
animation: labelAnimation,
),
if (label != null) _ScrollLabel(label: label!, backgroundColor: backgroundColor, animation: labelAnimation),
_CircularThumb(backgroundColor),
],
),
@@ -519,9 +457,7 @@ class _CircularThumb extends StatelessWidget {
topRight: Radius.circular(4.0),
bottomRight: Radius.circular(4.0),
),
child: Container(
constraints: BoxConstraints.tight(const Size(48.0 * 0.6, 48.0)),
),
child: Container(constraints: BoxConstraints.tight(const Size(48.0 * 0.6, 48.0))),
),
);
}
@@ -543,14 +479,8 @@ class _ArrowPainter extends CustomPainter {
final baseX = size.width / 2;
final baseY = size.height / 2;
canvas.drawPath(
_trianglePath(Offset(baseX, baseY - 2.0), width, height, true),
paint,
);
canvas.drawPath(
_trianglePath(Offset(baseX, baseY + 2.0), width, height, false),
paint,
);
canvas.drawPath(_trianglePath(Offset(baseX, baseY - 2.0), width, height, true), paint);
canvas.drawPath(_trianglePath(Offset(baseX, baseY + 2.0), width, height, false), paint);
}
static Path _trianglePath(Offset o, double width, double height, bool isUp) {
@@ -566,11 +496,9 @@ class _SlideFadeTransition extends StatelessWidget {
final Animation<double> _animation;
final Widget _child;
const _SlideFadeTransition({
required Animation<double> animation,
required Widget child,
}) : _animation = animation,
_child = child;
const _SlideFadeTransition({required Animation<double> animation, required Widget child})
: _animation = animation,
_child = child;
@override
Widget build(BuildContext context) {
@@ -578,14 +506,8 @@ class _SlideFadeTransition extends StatelessWidget {
animation: _animation,
builder: (context, child) => _animation.value == 0.0 ? const SizedBox() : child!,
child: SlideTransition(
position: Tween(
begin: const Offset(0.3, 0.0),
end: const Offset(0.0, 0.0),
).animate(_animation),
child: FadeTransition(
opacity: _animation,
child: _child,
),
position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(_animation),
child: FadeTransition(opacity: _animation, child: _child),
),
);
}
@@ -597,19 +519,9 @@ class _Segment {
final String scrollLabel;
final bool showSegment;
const _Segment({
required this.date,
required this.startOffset,
required this.scrollLabel,
this.showSegment = false,
});
const _Segment({required this.date, required this.startOffset, required this.scrollLabel, this.showSegment = false});
_Segment copyWith({
DateTime? date,
double? startOffset,
String? scrollLabel,
bool? showSegment,
}) {
_Segment copyWith({DateTime? date, double? startOffset, String? scrollLabel, bool? showSegment}) {
return _Segment(
date: date ?? this.date,
startOffset: startOffset ?? this.startOffset,

View File

@@ -37,8 +37,8 @@ abstract class Segment {
required this.headerExtent,
required this.spacing,
required this.header,
}) : gridIndex = firstIndex + 1,
gridOffset = startOffset + headerExtent + spacing;
}) : gridIndex = firstIndex + 1,
gridOffset = startOffset + headerExtent + spacing;
bool containsIndex(int index) => firstIndex <= index && index <= lastIndex;

View File

@@ -9,34 +9,26 @@ abstract class SegmentBuilder {
final double spacing;
final GroupAssetsBy groupBy;
const SegmentBuilder({
required this.buckets,
this.spacing = kTimelineSpacing,
this.groupBy = GroupAssetsBy.day,
});
const SegmentBuilder({required this.buckets, this.spacing = kTimelineSpacing, this.groupBy = GroupAssetsBy.day});
static double headerExtent(HeaderType header) => switch (header) {
HeaderType.month => kTimelineHeaderExtent,
HeaderType.day => kTimelineHeaderExtent * 0.90,
HeaderType.monthAndDay => kTimelineHeaderExtent * 1.6,
HeaderType.none => 0.0,
};
HeaderType.month => kTimelineHeaderExtent,
HeaderType.day => kTimelineHeaderExtent * 0.90,
HeaderType.monthAndDay => kTimelineHeaderExtent * 1.6,
HeaderType.none => 0.0,
};
static Widget buildPlaceholder(
BuildContext context,
int count, {
Size size = const Size.square(kTimelineFixedTileExtent),
double spacing = kTimelineSpacing,
}) =>
RepaintBoundary(
child: FixedTimelineRow(
dimension: size.height,
spacing: spacing,
textDirection: Directionality.of(context),
children: List.generate(
count,
(_) => ThumbnailPlaceholder(width: size.width, height: size.height),
),
),
);
}) => RepaintBoundary(
child: FixedTimelineRow(
dimension: size.height,
spacing: spacing,
textDirection: Directionality.of(context),
children: List.generate(count, (_) => ThumbnailPlaceholder(width: size.width, height: size.height)),
),
);
}

View File

@@ -54,10 +54,7 @@ class TimelineState {
final bool isScrubbing;
final bool isScrolling;
const TimelineState({
this.isScrubbing = false,
this.isScrolling = false,
});
const TimelineState({this.isScrubbing = false, this.isScrolling = false});
bool get isInteracting => isScrubbing || isScrolling;
@@ -70,10 +67,7 @@ class TimelineState {
int get hashCode => isScrubbing.hashCode ^ isScrolling.hashCode;
TimelineState copyWith({bool? isScrubbing, bool? isScrolling}) {
return TimelineState(
isScrubbing: isScrubbing ?? this.isScrubbing,
isScrolling: isScrolling ?? this.isScrolling,
);
return TimelineState(isScrubbing: isScrubbing ?? this.isScrubbing, isScrolling: isScrolling ?? this.isScrolling);
}
}
@@ -89,38 +83,30 @@ class TimelineStateNotifier extends Notifier<TimelineState> {
}
@override
TimelineState build() => const TimelineState(
isScrubbing: false,
isScrolling: false,
);
TimelineState build() => const TimelineState(isScrubbing: false, isScrolling: false);
}
// This provider watches the buckets from the timeline service & args and serves the segments.
// It should be used only after the timeline service and timeline args provider is overridden
final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>(
(ref) async* {
final args = ref.watch(timelineArgsProvider);
final columnCount = args.columnCount;
final spacing = args.spacing;
final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1));
final tileExtent = math.max(0, availableTileWidth) / columnCount;
final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>((ref) async* {
final args = ref.watch(timelineArgsProvider);
final columnCount = args.columnCount;
final spacing = args.spacing;
final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1));
final tileExtent = math.max(0, availableTileWidth) / columnCount;
final groupBy = args.groupBy ?? GroupAssetsBy.values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)];
final groupBy = args.groupBy ?? GroupAssetsBy.values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)];
final timelineService = ref.watch(timelineServiceProvider);
yield* timelineService.watchBuckets().map((buckets) {
return FixedSegmentBuilder(
buckets: buckets,
tileHeight: tileExtent,
columnCount: columnCount,
spacing: spacing,
groupBy: groupBy,
).generate();
});
},
dependencies: [timelineServiceProvider, timelineArgsProvider],
);
final timelineService = ref.watch(timelineServiceProvider);
yield* timelineService.watchBuckets().map((buckets) {
return FixedSegmentBuilder(
buckets: buckets,
tileHeight: tileExtent,
columnCount: columnCount,
spacing: spacing,
groupBy: groupBy,
).generate();
});
}, dependencies: [timelineServiceProvider, timelineArgsProvider]);
final timelineStateProvider = NotifierProvider<TimelineStateNotifier, TimelineState>(
TimelineStateNotifier.new,
);
final timelineStateProvider = NotifierProvider<TimelineStateNotifier, TimelineState>(TimelineStateNotifier.new);

View File

@@ -29,11 +29,7 @@ class Timeline extends StatelessWidget {
this.topSliverWidgetHeight,
this.showStorageIndicator = false,
this.withStack = false,
this.appBar = const ImmichSliverAppBar(
floating: true,
pinned: false,
snap: false,
),
this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false),
this.bottomSheet = const GeneralBottomSheet(),
this.groupBy,
});
@@ -57,9 +53,7 @@ class Timeline extends StatelessWidget {
(ref) => TimelineArgs(
maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight,
columnCount: ref.watch(
settingsProvider.select((s) => s.get(Setting.tilesPerRow)),
),
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
showStorageIndicator: showStorageIndicator,
withStack: withStack,
groupBy: groupBy,
@@ -79,12 +73,7 @@ class Timeline extends StatelessWidget {
}
class _SliverTimeline extends ConsumerStatefulWidget {
const _SliverTimeline({
this.topSliverWidget,
this.topSliverWidgetHeight,
this.appBar,
this.bottomSheet,
});
const _SliverTimeline({this.topSliverWidget, this.topSliverWidgetHeight, this.appBar, this.bottomSheet});
final Widget? topSliverWidget;
final double? topSliverWidgetHeight;
@@ -108,11 +97,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
void _onEvent(Event event) {
switch (event) {
case ScrollToTopEvent():
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
_scrollController.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut);
case ScrollToDateEvent scrollToDateEvent:
_scrollToDate(scrollToDateEvent.date);
case TimelineReloadEvent():
@@ -143,7 +128,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
});
// If exact date not found, try to find the closest month
final fallbackSegment = targetSegment ??
final fallbackSegment =
targetSegment ??
segments.firstWhereOrNull((segment) {
if (segment.bucket is TimeBucket) {
final segmentDate = (segment.bucket as TimeBucket).date;
@@ -168,9 +154,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
Widget build(BuildContext _) {
final asyncSegments = ref.watch(timelineSegmentProvider);
final maxHeight = ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
final isSelectionMode = ref.watch(
multiSelectProvider.select((s) => s.forceEnable),
);
final isSelectionMode = ref.watch(multiSelectProvider.select((s) => s.forceEnable));
return asyncSegments.widgetWhen(
onData: (segments) {
@@ -211,42 +195,26 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
addRepaintBoundaries: false,
),
),
const SliverPadding(
padding: EdgeInsets.only(
bottom: scrubberBottomPadding,
),
),
const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)),
],
),
),
if (!isSelectionMode) ...[
Consumer(
builder: (_, consumerRef, child) {
final isMultiSelectEnabled = consumerRef.watch(
multiSelectProvider.select(
(s) => s.isEnabled,
),
);
final isMultiSelectEnabled = consumerRef.watch(multiSelectProvider.select((s) => s.isEnabled));
if (isMultiSelectEnabled) {
return child!;
}
return const SizedBox.shrink();
},
child: const Positioned(
top: 60,
left: 25,
child: _MultiSelectStatusButton(),
),
child: const Positioned(top: 60, left: 25, child: _MultiSelectStatusButton()),
),
if (widget.bottomSheet != null)
Consumer(
builder: (_, consumerRef, child) {
final isMultiSelectEnabled = consumerRef.watch(
multiSelectProvider.select(
(s) => s.isEnabled,
),
);
final isMultiSelectEnabled = consumerRef.watch(multiSelectProvider.select((s) => s.isEnabled));
if (isMultiSelectEnabled) {
return child!;
@@ -267,22 +235,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
class _SliverSegmentedList extends SliverMultiBoxAdaptorWidget {
final List<Segment> _segments;
const _SliverSegmentedList({
required List<Segment> segments,
required super.delegate,
}) : _segments = segments;
const _SliverSegmentedList({required List<Segment> segments, required super.delegate}) : _segments = segments;
@override
_RenderSliverTimelineBoxAdaptor createRenderObject(BuildContext context) => _RenderSliverTimelineBoxAdaptor(
childManager: context as SliverMultiBoxAdaptorElement,
segments: _segments,
);
_RenderSliverTimelineBoxAdaptor createRenderObject(BuildContext context) =>
_RenderSliverTimelineBoxAdaptor(childManager: context as SliverMultiBoxAdaptorElement, segments: _segments);
@override
void updateRenderObject(
BuildContext context,
_RenderSliverTimelineBoxAdaptor renderObject,
) {
void updateRenderObject(BuildContext context, _RenderSliverTimelineBoxAdaptor renderObject) {
renderObject.segments = _segments;
}
}
@@ -299,10 +259,8 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
markNeedsLayout();
}
_RenderSliverTimelineBoxAdaptor({
required super.childManager,
required List<Segment> segments,
}) : _segments = segments;
_RenderSliverTimelineBoxAdaptor({required super.childManager, required List<Segment> segments})
: _segments = segments;
int getMinChildIndexForScrollOffset(double offset) =>
_segments.findByOffset(offset)?.getMinChildIndexForScrollOffset(offset) ?? 0;
@@ -335,16 +293,18 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
final int firstRequiredChildIndex = getMinChildIndexForScrollOffset(scrollOffset);
// Find the index of the last child that should be visible or in the trailing cache area.
final int? lastRequiredChildIndex =
targetScrollOffset.isFinite ? getMaxChildIndexForScrollOffset(targetScrollOffset) : null;
final int? lastRequiredChildIndex = targetScrollOffset.isFinite
? getMaxChildIndexForScrollOffset(targetScrollOffset)
: null;
// Remove children that are no longer visible or within the cache area.
if (firstChild == null) {
collectGarbage(0, 0);
} else {
final int leadingChildrenToRemove = calculateLeadingGarbage(firstIndex: firstRequiredChildIndex);
final int trailingChildrenToRemove =
lastRequiredChildIndex == null ? 0 : calculateTrailingGarbage(lastIndex: lastRequiredChildIndex);
final int trailingChildrenToRemove = lastRequiredChildIndex == null
? 0
: calculateTrailingGarbage(lastIndex: lastRequiredChildIndex);
collectGarbage(leadingChildrenToRemove, trailingChildrenToRemove);
}
@@ -352,10 +312,7 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
// try to add the first child needed for the current scroll offset.
if (firstChild == null) {
final double firstChildLayoutOffset = indexToLayoutOffset(firstRequiredChildIndex);
final bool childAdded = addInitialChild(
index: firstRequiredChildIndex,
layoutOffset: firstChildLayoutOffset,
);
final bool childAdded = addInitialChild(index: firstRequiredChildIndex, layoutOffset: firstChildLayoutOffset);
if (!childAdded) {
// There are either no children, or we are past the end of all our children.
@@ -408,16 +365,15 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
// until we reach the [lastRequiredChildIndex] or run out of children.
double calculatedMaxScrollOffset = double.infinity;
for (int currentIndex = indexOf(mostRecentlyLaidOutChild!) + 1;
lastRequiredChildIndex == null || currentIndex <= lastRequiredChildIndex;
++currentIndex) {
for (
int currentIndex = indexOf(mostRecentlyLaidOutChild!) + 1;
lastRequiredChildIndex == null || currentIndex <= lastRequiredChildIndex;
++currentIndex
) {
RenderBox? child = childAfter(mostRecentlyLaidOutChild!);
if (child == null || indexOf(child) != currentIndex) {
child = insertAndLayoutChild(
childConstraints,
after: mostRecentlyLaidOutChild,
);
child = insertAndLayoutChild(childConstraints, after: mostRecentlyLaidOutChild);
if (child == null) {
final Segment? segment = _segments.findByIndex(currentIndex) ?? _segments.lastOrNull;
calculatedMaxScrollOffset = segment?.indexToLayoutOffset(currentIndex) ?? computeMaxScrollOffset();
@@ -443,30 +399,18 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
);
assert(debugAssertChildListIsNonEmptyAndContiguous());
assert(indexOf(firstChild!) == firstRequiredChildIndex);
assert(
lastRequiredChildIndex == null || lastLaidOutChildIndex <= lastRequiredChildIndex,
);
assert(lastRequiredChildIndex == null || lastLaidOutChildIndex <= lastRequiredChildIndex);
calculatedMaxScrollOffset = math.min(
calculatedMaxScrollOffset,
estimateMaxScrollOffset(),
);
calculatedMaxScrollOffset = math.min(calculatedMaxScrollOffset, estimateMaxScrollOffset());
final double paintExtent = calculatePaintOffset(
constraints,
from: leadingScrollOffset,
to: trailingScrollOffset,
);
final double paintExtent = calculatePaintOffset(constraints, from: leadingScrollOffset, to: trailingScrollOffset);
final double cacheExtent = calculateCacheOffset(
constraints,
from: leadingScrollOffset,
to: trailingScrollOffset,
);
final double cacheExtent = calculateCacheOffset(constraints, from: leadingScrollOffset, to: trailingScrollOffset);
final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent;
final int? targetLastIndexForPaint =
targetEndScrollOffsetForPaint.isFinite ? getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint) : null;
final int? targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite
? getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint)
: null;
final maxPaintExtent = math.max(paintExtent, calculatedMaxScrollOffset);
@@ -477,7 +421,8 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
// Indicates if there's content scrolled off-screen.
// This is true if the last child needed for painting is actually laid out,
// or if the first child is partially visible.
hasVisualOverflow: (targetLastIndexForPaint != null && lastLaidOutChildIndex >= targetLastIndexForPaint) ||
hasVisualOverflow:
(targetLastIndexForPaint != null && lastLaidOutChildIndex >= targetLastIndexForPaint) ||
constraints.scrollOffset > 0.0,
cacheExtent: cacheExtent,
);
@@ -500,16 +445,10 @@ class _MultiSelectStatusButton extends ConsumerWidget {
final selectCount = ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length));
return ElevatedButton.icon(
onPressed: () => ref.read(multiSelectProvider.notifier).reset(),
icon: Icon(
Icons.close_rounded,
color: context.colorScheme.onPrimary,
),
icon: Icon(Icons.close_rounded, color: context.colorScheme.onPrimary),
label: Text(
selectCount.toString(),
style: context.textTheme.titleMedium?.copyWith(
height: 2.5,
color: context.colorScheme.onPrimary,
),
style: context.textTheme.titleMedium?.copyWith(height: 2.5, color: context.colorScheme.onPrimary),
),
);
}