mirror of
https://github.com/immich-app/immich.git
synced 2025-01-12 15:32:36 +02:00
feat(mobile): Explore favorites, recently added, videos, and motion photos (#2076)
* Added placeholder for search explore * refactor immich asset grid to use ref and provider * all videos page * got favorites, recently added, videos, and motion videos all using the immich grid * Fixed issue with hero animations * theming * localization * delete empty file * style text * Styling icons * more styling --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
d2600e0ddd
commit
501b96baf7
@ -249,5 +249,14 @@
|
|||||||
"album_thumbnail_owned": "Owned",
|
"album_thumbnail_owned": "Owned",
|
||||||
"curated_object_page_title": "Things",
|
"curated_object_page_title": "Things",
|
||||||
"curated_location_page_title": "Places",
|
"curated_location_page_title": "Places",
|
||||||
"search_page_view_all_button": "View All"
|
"search_page_view_all_button": "View all",
|
||||||
|
"search_page_your_activity": "Your activity",
|
||||||
|
"search_page_favorites": "Favorites",
|
||||||
|
"search_page_videos": "Videos",
|
||||||
|
"all_videos_page_title": "Videos",
|
||||||
|
"recently_added_page_title": "Recently Added",
|
||||||
|
"motion_photos_page_title": "Motion Photos",
|
||||||
|
"search_page_motion_photos": "Motion Photos",
|
||||||
|
"search_page_recently_added": "Recently added",
|
||||||
|
"search_page_categories": "Categories"
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
|
final renderListProvider = FutureProvider.family<RenderList, List<Asset>>((ref, assets) {
|
||||||
|
var settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
|
||||||
|
final layout = AssetGridLayoutParameters(
|
||||||
|
settings.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
|
settings.getSetting(AppSettingsEnum.dynamicLayout),
|
||||||
|
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)],
|
||||||
|
);
|
||||||
|
|
||||||
|
return RenderList.fromAssets(assets, layout);
|
||||||
|
});
|
@ -3,9 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
||||||
import 'package:immich_mobile/modules/favorite/ui/favorite_image.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
|
||||||
|
|
||||||
class FavoritesPage extends HookConsumerWidget {
|
class FavoritesPage extends HookConsumerWidget {
|
||||||
const FavoritesPage({Key? key}) : super(key: key);
|
const FavoritesPage({Key? key}) : super(key: key);
|
||||||
@ -22,46 +20,14 @@ class FavoritesPage extends HookConsumerWidget {
|
|||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'favorites_page_title',
|
'favorites_page_title',
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
||||||
).tr(),
|
).tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildImageGrid() {
|
|
||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
|
||||||
|
|
||||||
if (ref.watch(favoriteAssetProvider).isNotEmpty) {
|
|
||||||
return SliverPadding(
|
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
|
||||||
sliver: SliverGrid(
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount:
|
|
||||||
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
|
|
||||||
crossAxisSpacing: 5.0,
|
|
||||||
mainAxisSpacing: 5,
|
|
||||||
),
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(
|
|
||||||
BuildContext context,
|
|
||||||
int index,
|
|
||||||
) {
|
|
||||||
return FavoriteImage(
|
|
||||||
ref.watch(favoriteAssetProvider)[index],
|
|
||||||
ref.watch(favoriteAssetProvider),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: ref.watch(favoriteAssetProvider).length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return const SliverToBoxAdapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: buildAppBar(),
|
appBar: buildAppBar(),
|
||||||
body: CustomScrollView(
|
body: ImmichAssetGrid(
|
||||||
slivers: [buildImageGrid()],
|
assets: ref.watch(favoriteAssetProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,300 +1,104 @@
|
|||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid_view.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'asset_grid_data_structure.dart';
|
|
||||||
import 'group_divider_title.dart';
|
|
||||||
import 'disable_multi_select_button.dart';
|
|
||||||
import 'draggable_scrollbar_custom.dart';
|
|
||||||
|
|
||||||
typedef ImmichAssetGridSelectionListener = void Function(
|
class ImmichAssetGrid extends HookConsumerWidget {
|
||||||
bool,
|
final int? assetsPerRow;
|
||||||
Set<Asset>,
|
|
||||||
);
|
|
||||||
|
|
||||||
class ImmichAssetGridState extends State<ImmichAssetGrid> {
|
|
||||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
|
||||||
final ItemPositionsListener _itemPositionsListener =
|
|
||||||
ItemPositionsListener.create();
|
|
||||||
|
|
||||||
bool _scrolling = false;
|
|
||||||
final Set<int> _selectedAssets = HashSet();
|
|
||||||
|
|
||||||
Set<Asset> _getSelectedAssets() {
|
|
||||||
return _selectedAssets
|
|
||||||
.map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
|
|
||||||
.whereNotNull()
|
|
||||||
.toSet();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _callSelectionListener(bool selectionActive) {
|
|
||||||
widget.listener?.call(selectionActive, _getSelectedAssets());
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectAssets(List<Asset> assets) {
|
|
||||||
setState(() {
|
|
||||||
for (var e in assets) {
|
|
||||||
_selectedAssets.add(e.id);
|
|
||||||
}
|
|
||||||
_callSelectionListener(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deselectAssets(List<Asset> assets) {
|
|
||||||
setState(() {
|
|
||||||
for (var e in assets) {
|
|
||||||
_selectedAssets.remove(e.id);
|
|
||||||
}
|
|
||||||
_callSelectionListener(_selectedAssets.isNotEmpty);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deselectAll() {
|
|
||||||
setState(() {
|
|
||||||
_selectedAssets.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
_callSelectionListener(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _allAssetsSelected(List<Asset> assets) {
|
|
||||||
return widget.selectionActive &&
|
|
||||||
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildThumbnailOrPlaceholder(
|
|
||||||
Asset asset,
|
|
||||||
bool placeholder,
|
|
||||||
) {
|
|
||||||
if (placeholder) {
|
|
||||||
return const DecoratedBox(
|
|
||||||
decoration: BoxDecoration(color: Colors.grey),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ThumbnailImage(
|
|
||||||
asset: asset,
|
|
||||||
assetList: widget.allAssets,
|
|
||||||
multiselectEnabled: widget.selectionActive,
|
|
||||||
isSelected: widget.selectionActive && _selectedAssets.contains(asset.id),
|
|
||||||
onSelect: () => _selectAssets([asset]),
|
|
||||||
onDeselect: () => _deselectAssets([asset]),
|
|
||||||
useGrayBoxPlaceholder: true,
|
|
||||||
showStorageIndicator: widget.showStorageIndicator,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAssetRow(
|
|
||||||
BuildContext context,
|
|
||||||
RenderAssetGridRow row,
|
|
||||||
bool scrolling,
|
|
||||||
) {
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
final size = constraints.maxWidth / widget.assetsPerRow -
|
|
||||||
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
|
|
||||||
return Row(
|
|
||||||
key: Key("asset-row-${row.assets.first.id}"),
|
|
||||||
children: row.assets.mapIndexed((int index, Asset asset) {
|
|
||||||
bool last = asset.id == row.assets.last.id;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
key: Key("asset-${asset.id}"),
|
|
||||||
width: size * row.widthDistribution[index],
|
|
||||||
height: size,
|
|
||||||
margin: EdgeInsets.only(
|
|
||||||
top: widget.margin,
|
|
||||||
right: last ? 0.0 : widget.margin,
|
|
||||||
),
|
|
||||||
child: _buildThumbnailOrPlaceholder(asset, scrolling),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTitle(
|
|
||||||
BuildContext context,
|
|
||||||
String title,
|
|
||||||
List<Asset> assets,
|
|
||||||
) {
|
|
||||||
return GroupDividerTitle(
|
|
||||||
text: title,
|
|
||||||
multiselectEnabled: widget.selectionActive,
|
|
||||||
onSelect: () => _selectAssets(assets),
|
|
||||||
onDeselect: () => _deselectAssets(assets),
|
|
||||||
selected: _allAssetsSelected(assets),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMonthTitle(BuildContext context, String title) {
|
|
||||||
return Padding(
|
|
||||||
key: Key("month-$title"),
|
|
||||||
padding: const EdgeInsets.only(left: 12.0, top: 32),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 26,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Theme.of(context).textTheme.displayLarge?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _itemBuilder(BuildContext c, int position) {
|
|
||||||
final item = widget.renderList.elements[position];
|
|
||||||
|
|
||||||
if (item.type == RenderAssetGridElementType.groupDividerTitle) {
|
|
||||||
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
|
||||||
} else if (item.type == RenderAssetGridElementType.monthTitle) {
|
|
||||||
return _buildMonthTitle(c, item.title!);
|
|
||||||
} else if (item.type == RenderAssetGridElementType.assetRow) {
|
|
||||||
return _buildAssetRow(c, item.assetRow!, _scrolling);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const Text("Invalid widget type!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Text _labelBuilder(int pos) {
|
|
||||||
final date = widget.renderList.elements[pos].date;
|
|
||||||
return Text(
|
|
||||||
DateFormat.yMMMM().format(date),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMultiSelectIndicator() {
|
|
||||||
return DisableMultiSelectButton(
|
|
||||||
onPressed: () => _deselectAll(),
|
|
||||||
selectedItemCount: _selectedAssets.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAssetGrid() {
|
|
||||||
final useDragScrolling = widget.allAssets.length >= 20;
|
|
||||||
|
|
||||||
void dragScrolling(bool active) {
|
|
||||||
setState(() {
|
|
||||||
_scrolling = active;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final listWidget = ScrollablePositionedList.builder(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
bottom: 220,
|
|
||||||
),
|
|
||||||
itemBuilder: _itemBuilder,
|
|
||||||
itemPositionsListener: _itemPositionsListener,
|
|
||||||
itemScrollController: _itemScrollController,
|
|
||||||
itemCount: widget.renderList.elements.length,
|
|
||||||
addRepaintBoundaries: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!useDragScrolling) {
|
|
||||||
return listWidget;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DraggableScrollbar.semicircle(
|
|
||||||
scrollStateListener: dragScrolling,
|
|
||||||
itemPositionsListener: _itemPositionsListener,
|
|
||||||
controller: _itemScrollController,
|
|
||||||
backgroundColor: Theme.of(context).hintColor,
|
|
||||||
labelTextBuilder: _labelBuilder,
|
|
||||||
labelConstraints: const BoxConstraints(maxHeight: 28),
|
|
||||||
scrollbarAnimationDuration: const Duration(seconds: 1),
|
|
||||||
scrollbarTimeToFade: const Duration(seconds: 4),
|
|
||||||
child: listWidget,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(ImmichAssetGrid oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (!widget.selectionActive) {
|
|
||||||
setState(() {
|
|
||||||
_selectedAssets.clear();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> onWillPop() async {
|
|
||||||
if (widget.selectionActive && _selectedAssets.isNotEmpty) {
|
|
||||||
_deselectAll();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
scrollToTopNotifierProvider.addListener(_scrollToTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
scrollToTopNotifierProvider.removeListener(_scrollToTop);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _scrollToTop() {
|
|
||||||
// for some reason, this is necessary as well in order
|
|
||||||
// to correctly reposition the drag thumb scroll bar
|
|
||||||
_itemScrollController.jumpTo(
|
|
||||||
index: 0,
|
|
||||||
);
|
|
||||||
_itemScrollController.scrollTo(
|
|
||||||
index: 0,
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return WillPopScope(
|
|
||||||
onWillPop: onWillPop,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
_buildAssetGrid(),
|
|
||||||
if (widget.selectionActive) _buildMultiSelectIndicator(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImmichAssetGrid extends StatefulWidget {
|
|
||||||
final RenderList renderList;
|
|
||||||
final int assetsPerRow;
|
|
||||||
final double margin;
|
final double margin;
|
||||||
final bool showStorageIndicator;
|
final bool? showStorageIndicator;
|
||||||
final ImmichAssetGridSelectionListener? listener;
|
final ImmichAssetGridSelectionListener? listener;
|
||||||
final bool selectionActive;
|
final bool selectionActive;
|
||||||
final List<Asset> allAssets;
|
final List<Asset> assets;
|
||||||
|
final RenderList? renderList;
|
||||||
|
|
||||||
const ImmichAssetGrid({
|
const ImmichAssetGrid({
|
||||||
super.key,
|
super.key,
|
||||||
required this.renderList,
|
required this.assets,
|
||||||
required this.allAssets,
|
this.renderList,
|
||||||
required this.assetsPerRow,
|
this.assetsPerRow,
|
||||||
required this.showStorageIndicator,
|
this.showStorageIndicator,
|
||||||
this.listener,
|
this.listener,
|
||||||
this.margin = 5.0,
|
this.margin = 5.0,
|
||||||
this.selectionActive = false,
|
this.selectionActive = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ImmichAssetGridState();
|
var settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
final renderListFuture = ref.watch(renderListProvider(assets));
|
||||||
|
|
||||||
|
// Needs to suppress hero animations when navigating to this widget
|
||||||
|
final enableHeroAnimations = useState(false);
|
||||||
|
|
||||||
|
// Wait for transition to complete, then re-enable
|
||||||
|
ModalRoute.of(context)?.animation?.addListener(() {
|
||||||
|
// If we've already enabled, we are done
|
||||||
|
if (enableHeroAnimations.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final animation = ModalRoute.of(context)?.animation;
|
||||||
|
if (animation != null) {
|
||||||
|
// When the animation is complete, re-enable hero animations
|
||||||
|
enableHeroAnimations.value = animation.isCompleted;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<bool> onWillPop() async {
|
||||||
|
enableHeroAnimations.value = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderList != null) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: onWillPop,
|
||||||
|
child: HeroMode(
|
||||||
|
enabled: enableHeroAnimations.value,
|
||||||
|
child: ImmichAssetGridView(
|
||||||
|
allAssets: assets,
|
||||||
|
assetsPerRow: assetsPerRow
|
||||||
|
?? settings.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
|
listener: listener,
|
||||||
|
showStorageIndicator: showStorageIndicator
|
||||||
|
?? settings.getSetting(AppSettingsEnum.storageIndicator),
|
||||||
|
renderList: renderList!,
|
||||||
|
margin: margin,
|
||||||
|
selectionActive: selectionActive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderListFuture.when(
|
||||||
|
data: (renderList) =>
|
||||||
|
WillPopScope(
|
||||||
|
onWillPop: onWillPop,
|
||||||
|
child: HeroMode(
|
||||||
|
enabled: enableHeroAnimations.value,
|
||||||
|
child: ImmichAssetGridView(
|
||||||
|
allAssets: assets,
|
||||||
|
assetsPerRow: assetsPerRow
|
||||||
|
?? settings.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
|
listener: listener,
|
||||||
|
showStorageIndicator: showStorageIndicator
|
||||||
|
?? settings.getSetting(AppSettingsEnum.storageIndicator),
|
||||||
|
renderList: renderList,
|
||||||
|
margin: margin,
|
||||||
|
selectionActive: selectionActive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (err, stack) =>
|
||||||
|
Center(child: Text("$err")),
|
||||||
|
loading: () => const Center(
|
||||||
|
child: ImmichLoadingIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,300 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
import 'asset_grid_data_structure.dart';
|
||||||
|
import 'group_divider_title.dart';
|
||||||
|
import 'disable_multi_select_button.dart';
|
||||||
|
import 'draggable_scrollbar_custom.dart';
|
||||||
|
|
||||||
|
typedef ImmichAssetGridSelectionListener = void Function(
|
||||||
|
bool,
|
||||||
|
Set<Asset>,
|
||||||
|
);
|
||||||
|
|
||||||
|
class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||||
|
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||||
|
final ItemPositionsListener _itemPositionsListener =
|
||||||
|
ItemPositionsListener.create();
|
||||||
|
|
||||||
|
bool _scrolling = false;
|
||||||
|
final Set<int> _selectedAssets = HashSet();
|
||||||
|
|
||||||
|
Set<Asset> _getSelectedAssets() {
|
||||||
|
return _selectedAssets
|
||||||
|
.map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
|
||||||
|
.whereNotNull()
|
||||||
|
.toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _callSelectionListener(bool selectionActive) {
|
||||||
|
widget.listener?.call(selectionActive, _getSelectedAssets());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectAssets(List<Asset> assets) {
|
||||||
|
setState(() {
|
||||||
|
for (var e in assets) {
|
||||||
|
_selectedAssets.add(e.id);
|
||||||
|
}
|
||||||
|
_callSelectionListener(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _deselectAssets(List<Asset> assets) {
|
||||||
|
setState(() {
|
||||||
|
for (var e in assets) {
|
||||||
|
_selectedAssets.remove(e.id);
|
||||||
|
}
|
||||||
|
_callSelectionListener(_selectedAssets.isNotEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _deselectAll() {
|
||||||
|
setState(() {
|
||||||
|
_selectedAssets.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
_callSelectionListener(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _allAssetsSelected(List<Asset> assets) {
|
||||||
|
return widget.selectionActive &&
|
||||||
|
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildThumbnailOrPlaceholder(
|
||||||
|
Asset asset,
|
||||||
|
bool placeholder,
|
||||||
|
) {
|
||||||
|
if (placeholder) {
|
||||||
|
return const DecoratedBox(
|
||||||
|
decoration: BoxDecoration(color: Colors.grey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ThumbnailImage(
|
||||||
|
asset: asset,
|
||||||
|
assetList: widget.allAssets,
|
||||||
|
multiselectEnabled: widget.selectionActive,
|
||||||
|
isSelected: widget.selectionActive && _selectedAssets.contains(asset.id),
|
||||||
|
onSelect: () => _selectAssets([asset]),
|
||||||
|
onDeselect: () => _deselectAssets([asset]),
|
||||||
|
useGrayBoxPlaceholder: true,
|
||||||
|
showStorageIndicator: widget.showStorageIndicator,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAssetRow(
|
||||||
|
BuildContext context,
|
||||||
|
RenderAssetGridRow row,
|
||||||
|
bool scrolling,
|
||||||
|
) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final size = constraints.maxWidth / widget.assetsPerRow -
|
||||||
|
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
|
||||||
|
return Row(
|
||||||
|
key: Key("asset-row-${row.assets.first.id}"),
|
||||||
|
children: row.assets.mapIndexed((int index, Asset asset) {
|
||||||
|
bool last = asset.id == row.assets.last.id;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
key: Key("asset-${asset.id}"),
|
||||||
|
width: size * row.widthDistribution[index],
|
||||||
|
height: size,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
top: widget.margin,
|
||||||
|
right: last ? 0.0 : widget.margin,
|
||||||
|
),
|
||||||
|
child: _buildThumbnailOrPlaceholder(asset, scrolling),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTitle(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
List<Asset> assets,
|
||||||
|
) {
|
||||||
|
return GroupDividerTitle(
|
||||||
|
text: title,
|
||||||
|
multiselectEnabled: widget.selectionActive,
|
||||||
|
onSelect: () => _selectAssets(assets),
|
||||||
|
onDeselect: () => _deselectAssets(assets),
|
||||||
|
selected: _allAssetsSelected(assets),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMonthTitle(BuildContext context, String title) {
|
||||||
|
return Padding(
|
||||||
|
key: Key("month-$title"),
|
||||||
|
padding: const EdgeInsets.only(left: 12.0, top: 32),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).textTheme.displayLarge?.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _itemBuilder(BuildContext c, int position) {
|
||||||
|
final item = widget.renderList.elements[position];
|
||||||
|
|
||||||
|
if (item.type == RenderAssetGridElementType.groupDividerTitle) {
|
||||||
|
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
||||||
|
} else if (item.type == RenderAssetGridElementType.monthTitle) {
|
||||||
|
return _buildMonthTitle(c, item.title!);
|
||||||
|
} else if (item.type == RenderAssetGridElementType.assetRow) {
|
||||||
|
return _buildAssetRow(c, item.assetRow!, _scrolling);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const Text("Invalid widget type!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Text _labelBuilder(int pos) {
|
||||||
|
final date = widget.renderList.elements[pos].date;
|
||||||
|
return Text(
|
||||||
|
DateFormat.yMMMM().format(date),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMultiSelectIndicator() {
|
||||||
|
return DisableMultiSelectButton(
|
||||||
|
onPressed: () => _deselectAll(),
|
||||||
|
selectedItemCount: _selectedAssets.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAssetGrid() {
|
||||||
|
final useDragScrolling = widget.allAssets.length >= 20;
|
||||||
|
|
||||||
|
void dragScrolling(bool active) {
|
||||||
|
setState(() {
|
||||||
|
_scrolling = active;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final listWidget = ScrollablePositionedList.builder(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: 220,
|
||||||
|
),
|
||||||
|
itemBuilder: _itemBuilder,
|
||||||
|
itemPositionsListener: _itemPositionsListener,
|
||||||
|
itemScrollController: _itemScrollController,
|
||||||
|
itemCount: widget.renderList.elements.length,
|
||||||
|
addRepaintBoundaries: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!useDragScrolling) {
|
||||||
|
return listWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DraggableScrollbar.semicircle(
|
||||||
|
scrollStateListener: dragScrolling,
|
||||||
|
itemPositionsListener: _itemPositionsListener,
|
||||||
|
controller: _itemScrollController,
|
||||||
|
backgroundColor: Theme.of(context).hintColor,
|
||||||
|
labelTextBuilder: _labelBuilder,
|
||||||
|
labelConstraints: const BoxConstraints(maxHeight: 28),
|
||||||
|
scrollbarAnimationDuration: const Duration(seconds: 1),
|
||||||
|
scrollbarTimeToFade: const Duration(seconds: 4),
|
||||||
|
child: listWidget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ImmichAssetGridView oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (!widget.selectionActive) {
|
||||||
|
setState(() {
|
||||||
|
_selectedAssets.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> onWillPop() async {
|
||||||
|
if (widget.selectionActive && _selectedAssets.isNotEmpty) {
|
||||||
|
_deselectAll();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
scrollToTopNotifierProvider.addListener(_scrollToTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
scrollToTopNotifierProvider.removeListener(_scrollToTop);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollToTop() {
|
||||||
|
// for some reason, this is necessary as well in order
|
||||||
|
// to correctly reposition the drag thumb scroll bar
|
||||||
|
_itemScrollController.jumpTo(
|
||||||
|
index: 0,
|
||||||
|
);
|
||||||
|
_itemScrollController.scrollTo(
|
||||||
|
index: 0,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: onWillPop,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
_buildAssetGrid(),
|
||||||
|
if (widget.selectionActive) _buildMultiSelectIndicator(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImmichAssetGridView extends StatefulWidget {
|
||||||
|
final RenderList renderList;
|
||||||
|
final int assetsPerRow;
|
||||||
|
final double margin;
|
||||||
|
final bool showStorageIndicator;
|
||||||
|
final ImmichAssetGridSelectionListener? listener;
|
||||||
|
final bool selectionActive;
|
||||||
|
final List<Asset> allAssets;
|
||||||
|
|
||||||
|
const ImmichAssetGridView({
|
||||||
|
super.key,
|
||||||
|
required this.renderList,
|
||||||
|
required this.allAssets,
|
||||||
|
required this.assetsPerRow,
|
||||||
|
required this.showStorageIndicator,
|
||||||
|
this.listener,
|
||||||
|
this.margin = 5.0,
|
||||||
|
this.selectionActive = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return ImmichAssetGridViewState();
|
||||||
|
}
|
||||||
|
}
|
@ -234,7 +234,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
? buildLoadingIndicator()
|
? buildLoadingIndicator()
|
||||||
: ImmichAssetGrid(
|
: ImmichAssetGrid(
|
||||||
renderList: ref.watch(assetProvider).renderList!,
|
renderList: ref.watch(assetProvider).renderList!,
|
||||||
allAssets: ref.watch(assetProvider).allAssets,
|
assets: ref.watch(assetProvider).allAssets,
|
||||||
assetsPerRow: appSettingService
|
assetsPerRow: appSettingService
|
||||||
.getSetting(AppSettingsEnum.tilesPerRow),
|
.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
showStorageIndicator: appSettingService
|
showStorageIndicator: appSettingService
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
|
|
||||||
|
final allMotionPhotosProvider = FutureProvider<List<Asset>>( (ref) async {
|
||||||
|
final search = await ref.watch(apiServiceProvider).searchApi.search(
|
||||||
|
motion: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (search == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.watch(dbProvider)
|
||||||
|
.assets
|
||||||
|
.getAllByRemoteId(
|
||||||
|
search.assets.items.map((e) => e.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/// This works offline, but we use the above
|
||||||
|
/*
|
||||||
|
return ref.watch(dbProvider).assets
|
||||||
|
.filter()
|
||||||
|
.livePhotoVideoIdIsNotNull()
|
||||||
|
.findAll();
|
||||||
|
*/
|
||||||
|
});
|
@ -0,0 +1,28 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
|
|
||||||
|
final allVideoAssetsProvider = FutureProvider<List<Asset>>( (ref) async {
|
||||||
|
final search = await ref.watch(apiServiceProvider).searchApi.search(
|
||||||
|
type: 'VIDEO',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (search == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.watch(dbProvider)
|
||||||
|
.assets
|
||||||
|
.getAllByRemoteId(
|
||||||
|
search.assets.items.map((e) => e.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// This works offline, but we use the above
|
||||||
|
/*
|
||||||
|
return ref.watch(dbProvider).assets
|
||||||
|
.filter()
|
||||||
|
.durationInSecondsGreaterThan(0)
|
||||||
|
.findAll();
|
||||||
|
*/
|
||||||
|
});
|
@ -0,0 +1,20 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
|
|
||||||
|
final recentlyAddedProvider = FutureProvider<List<Asset>>( (ref) async {
|
||||||
|
final search = await ref.watch(apiServiceProvider).searchApi.search(
|
||||||
|
recent: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (search == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.watch(dbProvider)
|
||||||
|
.assets
|
||||||
|
.getAllByRemoteId(
|
||||||
|
search.assets.items.map((e) => e.id),
|
||||||
|
);
|
||||||
|
});
|
@ -1,10 +1,8 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
|
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||||
@ -55,15 +53,6 @@ final searchResultPageProvider =
|
|||||||
});
|
});
|
||||||
|
|
||||||
final searchRenderListProvider = FutureProvider((ref) {
|
final searchRenderListProvider = FutureProvider((ref) {
|
||||||
var settings = ref.watch(appSettingsServiceProvider);
|
|
||||||
|
|
||||||
final assets = ref.watch(searchResultPageProvider).searchResult;
|
final assets = ref.watch(searchResultPageProvider).searchResult;
|
||||||
|
return ref.watch(renderListProvider(assets));
|
||||||
final layout = AssetGridLayoutParameters(
|
|
||||||
settings.getSetting(AppSettingsEnum.tilesPerRow),
|
|
||||||
settings.getSetting(AppSettingsEnum.dynamicLayout),
|
|
||||||
GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)],
|
|
||||||
);
|
|
||||||
|
|
||||||
return RenderList.fromAssets(assets, layout);
|
|
||||||
});
|
});
|
||||||
|
67
mobile/lib/modules/search/ui/curated_row.dart
Normal file
67
mobile/lib/modules/search/ui/curated_row.dart
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/curated_content.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
|
|
||||||
|
class CuratedRow extends StatelessWidget {
|
||||||
|
final List<CuratedContent> content;
|
||||||
|
final double imageSize;
|
||||||
|
|
||||||
|
/// Callback with the content and the index when tapped
|
||||||
|
final Function(CuratedContent, int)? onTap;
|
||||||
|
|
||||||
|
const CuratedRow({
|
||||||
|
super.key,
|
||||||
|
required this.content,
|
||||||
|
this.imageSize = 200,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
// Guard empty [content]
|
||||||
|
if (content.isEmpty) {
|
||||||
|
// Return empty thumbnail
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: imageSize,
|
||||||
|
height: imageSize,
|
||||||
|
child: ThumbnailWithInfo(
|
||||||
|
textInfo: '',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final object = content[index];
|
||||||
|
final thumbnailRequestUrl =
|
||||||
|
'${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${object.id}';
|
||||||
|
return SizedBox(
|
||||||
|
width: imageSize,
|
||||||
|
height: imageSize,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4.0),
|
||||||
|
child: ThumbnailWithInfo(
|
||||||
|
imageUrl: thumbnailRequestUrl,
|
||||||
|
textInfo: object.label,
|
||||||
|
onTap: () => onTap?.call(object, index),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: content.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
35
mobile/lib/modules/search/views/all_motion_videos_page.dart
Normal file
35
mobile/lib/modules/search/views/all_motion_videos_page.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/providers/all_motion_photos.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
|
||||||
|
class AllMotionPhotosPage extends HookConsumerWidget {
|
||||||
|
const AllMotionPhotosPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final motionPhotos = ref.watch(allMotionPhotosProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('motion_photos_page_title').tr(),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () => AutoRouter.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: motionPhotos.when(
|
||||||
|
data: (assets) => ImmichAssetGrid(
|
||||||
|
assets: assets,
|
||||||
|
),
|
||||||
|
error: (e, s) => Text(e.toString()),
|
||||||
|
loading: () => const Center(
|
||||||
|
child: ImmichLoadingIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
35
mobile/lib/modules/search/views/all_videos_page.dart
Normal file
35
mobile/lib/modules/search/views/all_videos_page.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/providers/all_video_assets.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
|
||||||
|
class AllVideosPage extends HookConsumerWidget {
|
||||||
|
const AllVideosPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final videos = ref.watch(allVideoAssetsProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('all_videos_page_title').tr(),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () => AutoRouter.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: videos.when(
|
||||||
|
data: (assets) => ImmichAssetGrid(
|
||||||
|
assets: assets,
|
||||||
|
),
|
||||||
|
error: (e, s) => Text(e.toString()),
|
||||||
|
loading: () => const Center(
|
||||||
|
child: ImmichLoadingIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -25,6 +26,10 @@ class CuratedLocationPage extends HookConsumerWidget {
|
|||||||
fontSize: 16.0,
|
fontSize: 16.0,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () => AutoRouter.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: curatedLocation.when(
|
body: curatedLocation.when(
|
||||||
loading: () => const Center(child: ImmichLoadingIndicator()),
|
loading: () => const Center(child: ImmichLoadingIndicator()),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -28,6 +29,10 @@ class CuratedObjectPage extends HookConsumerWidget {
|
|||||||
fontSize: 16.0,
|
fontSize: 16.0,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () => AutoRouter.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: curatedObjects.when(
|
body: curatedObjects.when(
|
||||||
loading: () => const Center(child: ImmichLoadingIndicator()),
|
loading: () => const Center(child: ImmichLoadingIndicator()),
|
||||||
|
35
mobile/lib/modules/search/views/recently_added_page.dart
Normal file
35
mobile/lib/modules/search/views/recently_added_page.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/providers/recently_added.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
|
||||||
|
class RecentlyAddedPage extends HookConsumerWidget {
|
||||||
|
const RecentlyAddedPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final recents = ref.watch(recentlyAddedProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('recently_added_page_title').tr(),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () => AutoRouter.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: recents.when(
|
||||||
|
data: (searchResponse) => ImmichAssetGrid(
|
||||||
|
assets: searchResponse,
|
||||||
|
),
|
||||||
|
error: (e, s) => Text(e.toString()),
|
||||||
|
loading: () => const Center(
|
||||||
|
child: ImmichLoadingIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,14 +3,13 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/models/curated_content.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/ui/curated_row.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_bar.dart';
|
import 'package:immich_mobile/modules/search/ui/search_bar.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
@ -26,8 +25,14 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
ref.watch(getCuratedLocationProvider);
|
ref.watch(getCuratedLocationProvider);
|
||||||
AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects =
|
AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects =
|
||||||
ref.watch(getCuratedObjectProvider);
|
ref.watch(getCuratedObjectProvider);
|
||||||
|
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
double imageSize = MediaQuery.of(context).size.width / 3;
|
double imageSize = MediaQuery.of(context).size.width / 3;
|
||||||
|
TextStyle categoryTitleStyle = const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
Color categoryIconColor = isDarkTheme ? Colors.white : Colors.black;
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
@ -50,100 +55,55 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
child: curatedLocation.when(
|
child: curatedLocation.when(
|
||||||
loading: () => const Center(child: ImmichLoadingIndicator()),
|
loading: () => const Center(child: ImmichLoadingIndicator()),
|
||||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||||
data: (curatedLocations) => ListView.builder(
|
data: (locations) => CuratedRow(
|
||||||
padding: const EdgeInsets.symmetric(
|
content: locations
|
||||||
horizontal: 16,
|
.map(
|
||||||
),
|
(o) => CuratedContent(
|
||||||
scrollDirection: Axis.horizontal,
|
id: o.id,
|
||||||
itemBuilder: (context, index) {
|
label: o.city,
|
||||||
final locationInfo = curatedLocations[index];
|
|
||||||
final thumbnailRequestUrl =
|
|
||||||
'${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${locationInfo.id}';
|
|
||||||
return SizedBox(
|
|
||||||
width: imageSize,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
|
||||||
child: ThumbnailWithInfo(
|
|
||||||
imageUrl: thumbnailRequestUrl,
|
|
||||||
textInfo: locationInfo.city,
|
|
||||||
onTap: () {
|
|
||||||
AutoRouter.of(context).push(
|
|
||||||
SearchResultRoute(searchTerm: locationInfo.city),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
|
.toList(),
|
||||||
|
imageSize: imageSize,
|
||||||
|
onTap: (content, index) {
|
||||||
|
AutoRouter.of(context).push(
|
||||||
|
SearchResultRoute(searchTerm: content.label),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemCount: curatedLocations.length.clamp(0, 10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildEmptyThumbnail() {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: SizedBox(
|
|
||||||
width: imageSize,
|
|
||||||
height: imageSize,
|
|
||||||
child: ThumbnailWithInfo(
|
|
||||||
textInfo: '',
|
|
||||||
onTap: () {},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildThings() {
|
buildThings() {
|
||||||
return curatedObjects.when(
|
return SizedBox(
|
||||||
loading: () => SizedBox(
|
height: imageSize,
|
||||||
height: imageSize,
|
child: curatedObjects.when(
|
||||||
child: const Center(child: ImmichLoadingIndicator()),
|
loading: () => SizedBox(
|
||||||
),
|
height: imageSize,
|
||||||
error: (err, stack) => SizedBox(
|
child: const Center(child: ImmichLoadingIndicator()),
|
||||||
height: imageSize,
|
),
|
||||||
child: Center(child: Text('Error: $err')),
|
error: (err, stack) => SizedBox(
|
||||||
),
|
height: imageSize,
|
||||||
data: (objects) => objects.isEmpty
|
child: Center(child: Text('Error: $err')),
|
||||||
? buildEmptyThumbnail()
|
),
|
||||||
: SizedBox(
|
data: (objects) => CuratedRow(
|
||||||
height: imageSize,
|
content: objects
|
||||||
child: ListView.builder(
|
.map(
|
||||||
shrinkWrap: true,
|
(o) => CuratedContent(
|
||||||
scrollDirection: Axis.horizontal,
|
id: o.id,
|
||||||
padding: const EdgeInsets.symmetric(
|
label: o.object,
|
||||||
horizontal: 16,
|
|
||||||
),
|
),
|
||||||
itemBuilder: (context, index) {
|
)
|
||||||
final curatedObjectInfo = objects[index];
|
.toList(),
|
||||||
final thumbnailRequestUrl =
|
imageSize: imageSize,
|
||||||
'${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${curatedObjectInfo.id}';
|
onTap: (content, index) {
|
||||||
return SizedBox(
|
AutoRouter.of(context).push(
|
||||||
width: imageSize,
|
SearchResultRoute(searchTerm: content.label),
|
||||||
child: Padding(
|
);
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
},
|
||||||
child: ThumbnailWithInfo(
|
),
|
||||||
imageUrl: thumbnailRequestUrl,
|
),
|
||||||
textInfo: curatedObjectInfo.object,
|
|
||||||
onTap: () {
|
|
||||||
AutoRouter.of(context).push(
|
|
||||||
SearchResultRoute(
|
|
||||||
searchTerm: curatedObjectInfo.object
|
|
||||||
.capitalizeFirstLetter(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: objects.length.clamp(0, 10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,12 +129,9 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
"search_page_places",
|
"search_page_places",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
).tr(),
|
).tr(),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -194,19 +151,18 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
buildPlaces(),
|
buildPlaces(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.only(
|
||||||
horizontal: 16.0,
|
top: 24.0,
|
||||||
vertical: 4.0,
|
bottom: 4.0,
|
||||||
|
left: 16.0,
|
||||||
|
right: 16.0,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
"search_page_things",
|
"search_page_things",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
).tr(),
|
).tr(),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -225,6 +181,85 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
buildThings(),
|
buildThings(),
|
||||||
|
const SizedBox(height: 24.0),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Text(
|
||||||
|
'search_page_your_activity',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.star_outline,
|
||||||
|
color: categoryIconColor,
|
||||||
|
),
|
||||||
|
title:
|
||||||
|
Text('search_page_favorites', style: categoryTitleStyle)
|
||||||
|
.tr(),
|
||||||
|
onTap: () => AutoRouter.of(context).push(
|
||||||
|
const FavoritesRoute(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 72,
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.schedule_outlined,
|
||||||
|
color: categoryIconColor,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'search_page_recently_added',
|
||||||
|
style: categoryTitleStyle,
|
||||||
|
).tr(),
|
||||||
|
onTap: () => AutoRouter.of(context).push(
|
||||||
|
const RecentlyAddedRoute(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24.0),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Text(
|
||||||
|
'search_page_categories',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('search_page_videos', style: categoryTitleStyle)
|
||||||
|
.tr(),
|
||||||
|
leading: Icon(
|
||||||
|
Icons.play_circle_outline,
|
||||||
|
color: categoryIconColor,
|
||||||
|
),
|
||||||
|
onTap: () => AutoRouter.of(context).push(
|
||||||
|
const AllVideosRoute(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 72,
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
'search_page_motion_photos',
|
||||||
|
style: categoryTitleStyle,
|
||||||
|
).tr(),
|
||||||
|
leading: Icon(
|
||||||
|
Icons.motion_photos_on_outlined,
|
||||||
|
color: categoryIconColor,
|
||||||
|
),
|
||||||
|
onTap: () => AutoRouter.of(context).push(
|
||||||
|
const AllMotionPhotosRoute(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (isSearchEnabled)
|
if (isSearchEnabled)
|
||||||
|
@ -7,8 +7,6 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'
|
|||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
|
||||||
class SearchResultPage extends HookConsumerWidget {
|
class SearchResultPage extends HookConsumerWidget {
|
||||||
@ -110,14 +108,8 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
buildSearchResult() {
|
buildSearchResult() {
|
||||||
var searchResultPageState = ref.watch(searchResultPageProvider);
|
var searchResultPageState = ref.watch(searchResultPageProvider);
|
||||||
var searchResultRenderList = ref.watch(searchRenderListProvider);
|
|
||||||
var allSearchAssets = ref.watch(searchResultPageProvider).searchResult;
|
var allSearchAssets = ref.watch(searchResultPageProvider).searchResult;
|
||||||
|
|
||||||
var settings = ref.watch(appSettingsServiceProvider);
|
|
||||||
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
|
|
||||||
final showStorageIndicator =
|
|
||||||
settings.getSetting(AppSettingsEnum.storageIndicator);
|
|
||||||
|
|
||||||
if (searchResultPageState.isError) {
|
if (searchResultPageState.isError) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
@ -129,22 +121,10 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
return const Center(child: ImmichLoadingIndicator());
|
return const Center(child: ImmichLoadingIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (searchResultPageState.isSuccess) {
|
if (searchResultPageState.isSuccess) {
|
||||||
return searchResultRenderList.when(
|
return ImmichAssetGrid(
|
||||||
data: (result) {
|
assets: allSearchAssets,
|
||||||
return ImmichAssetGrid(
|
|
||||||
allAssets: allSearchAssets,
|
|
||||||
renderList: result,
|
|
||||||
assetsPerRow: assetsPerRow,
|
|
||||||
showStorageIndicator: showStorageIndicator,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: (err, stack) {
|
|
||||||
return Text("$err");
|
|
||||||
},
|
|
||||||
loading: () {
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,8 +21,11 @@ import 'package:immich_mobile/modules/login/views/change_password_page.dart';
|
|||||||
import 'package:immich_mobile/modules/login/views/login_page.dart';
|
import 'package:immich_mobile/modules/login/views/login_page.dart';
|
||||||
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart';
|
import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/views/all_motion_videos_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/views/all_videos_page.dart';
|
||||||
import 'package:immich_mobile/modules/search/views/curated_location_page.dart';
|
import 'package:immich_mobile/modules/search/views/curated_location_page.dart';
|
||||||
import 'package:immich_mobile/modules/search/views/curated_object_page.dart';
|
import 'package:immich_mobile/modules/search/views/curated_object_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/views/recently_added_page.dart';
|
||||||
import 'package:immich_mobile/modules/search/views/search_page.dart';
|
import 'package:immich_mobile/modules/search/views/search_page.dart';
|
||||||
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
|
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
|
||||||
import 'package:immich_mobile/modules/settings/views/settings_page.dart';
|
import 'package:immich_mobile/modules/settings/views/settings_page.dart';
|
||||||
@ -70,6 +73,9 @@ part 'router.gr.dart';
|
|||||||
AutoRoute(page: CuratedObjectPage, guards: [AuthGuard, DuplicateGuard]),
|
AutoRoute(page: CuratedObjectPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
AutoRoute(page: CreateAlbumPage, guards: [AuthGuard, DuplicateGuard]),
|
AutoRoute(page: CreateAlbumPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
AutoRoute(page: FavoritesPage, guards: [AuthGuard, DuplicateGuard]),
|
AutoRoute(page: FavoritesPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
|
AutoRoute(page: AllVideosPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
|
AutoRoute(page: AllMotionPhotosPage, guards: [AuthGuard, DuplicateGuard]),
|
||||||
|
AutoRoute(page: RecentlyAddedPage, guards: [AuthGuard, DuplicateGuard],),
|
||||||
CustomRoute<AssetSelectionPageResult?>(
|
CustomRoute<AssetSelectionPageResult?>(
|
||||||
page: AssetSelectionPage,
|
page: AssetSelectionPage,
|
||||||
guards: [AuthGuard, DuplicateGuard],
|
guards: [AuthGuard, DuplicateGuard],
|
||||||
|
@ -131,6 +131,29 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
child: const FavoritesPage(),
|
child: const FavoritesPage(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
AllVideosRoute.name: (routeData) {
|
||||||
|
return MaterialPageX<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const AllVideosPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
AllMotionPhotosRoute.name: (routeData) {
|
||||||
|
return MaterialPageX<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const AllMotionPhotosPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
RecentlyAddedRoute.name: (routeData) {
|
||||||
|
return CustomPage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const RecentlyAddedPage(),
|
||||||
|
transitionsBuilder: TransitionsBuilders.noTransition,
|
||||||
|
durationInMilliseconds: 200,
|
||||||
|
reverseDurationInMilliseconds: 200,
|
||||||
|
opaque: true,
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
AssetSelectionRoute.name: (routeData) {
|
AssetSelectionRoute.name: (routeData) {
|
||||||
return CustomPage<AssetSelectionPageResult?>(
|
return CustomPage<AssetSelectionPageResult?>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
@ -375,6 +398,30 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
duplicateGuard,
|
duplicateGuard,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
RouteConfig(
|
||||||
|
AllVideosRoute.name,
|
||||||
|
path: '/all-videos-page',
|
||||||
|
guards: [
|
||||||
|
authGuard,
|
||||||
|
duplicateGuard,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
RouteConfig(
|
||||||
|
AllMotionPhotosRoute.name,
|
||||||
|
path: '/all-motion-photos-page',
|
||||||
|
guards: [
|
||||||
|
authGuard,
|
||||||
|
duplicateGuard,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
RouteConfig(
|
||||||
|
RecentlyAddedRoute.name,
|
||||||
|
path: '/recently-added-page',
|
||||||
|
guards: [
|
||||||
|
authGuard,
|
||||||
|
duplicateGuard,
|
||||||
|
],
|
||||||
|
),
|
||||||
RouteConfig(
|
RouteConfig(
|
||||||
AssetSelectionRoute.name,
|
AssetSelectionRoute.name,
|
||||||
path: '/asset-selection-page',
|
path: '/asset-selection-page',
|
||||||
@ -721,6 +768,42 @@ class FavoritesRoute extends PageRouteInfo<void> {
|
|||||||
static const String name = 'FavoritesRoute';
|
static const String name = 'FavoritesRoute';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [AllVideosPage]
|
||||||
|
class AllVideosRoute extends PageRouteInfo<void> {
|
||||||
|
const AllVideosRoute()
|
||||||
|
: super(
|
||||||
|
AllVideosRoute.name,
|
||||||
|
path: '/all-videos-page',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'AllVideosRoute';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [AllMotionPhotosPage]
|
||||||
|
class AllMotionPhotosRoute extends PageRouteInfo<void> {
|
||||||
|
const AllMotionPhotosRoute()
|
||||||
|
: super(
|
||||||
|
AllMotionPhotosRoute.name,
|
||||||
|
path: '/all-motion-photos-page',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'AllMotionPhotosRoute';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [RecentlyAddedPage]
|
||||||
|
class RecentlyAddedRoute extends PageRouteInfo<void> {
|
||||||
|
const RecentlyAddedRoute()
|
||||||
|
: super(
|
||||||
|
RecentlyAddedRoute.name,
|
||||||
|
path: '/recently-added-page',
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'RecentlyAddedRoute';
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [AssetSelectionPage]
|
/// [AssetSelectionPage]
|
||||||
class AssetSelectionRoute extends PageRouteInfo<void> {
|
class AssetSelectionRoute extends PageRouteInfo<void> {
|
||||||
|
@ -169,7 +169,10 @@ class TabControllerPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: body,
|
body: HeroControllerScope(
|
||||||
|
controller: HeroController(),
|
||||||
|
child: body,
|
||||||
|
),
|
||||||
bottomNavigationBar: multiselectEnabled ? null : bottom,
|
bottomNavigationBar: multiselectEnabled ? null : bottom,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -41,6 +41,8 @@ ThemeData immichLightTheme = ThemeData(
|
|||||||
titleTextStyle: const TextStyle(
|
titleTextStyle: const TextStyle(
|
||||||
fontFamily: 'WorkSans',
|
fontFamily: 'WorkSans',
|
||||||
color: Colors.indigo,
|
color: Colors.indigo,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18,
|
||||||
),
|
),
|
||||||
backgroundColor: immichBackgroundColor,
|
backgroundColor: immichBackgroundColor,
|
||||||
foregroundColor: Colors.indigo,
|
foregroundColor: Colors.indigo,
|
||||||
@ -75,6 +77,17 @@ ThemeData immichLightTheme = ThemeData(
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.indigo,
|
color: Colors.indigo,
|
||||||
),
|
),
|
||||||
|
titleSmall: TextStyle(
|
||||||
|
fontSize: 16.0,
|
||||||
|
),
|
||||||
|
titleMedium: TextStyle(
|
||||||
|
fontSize: 18.0,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
titleLarge: TextStyle(
|
||||||
|
fontSize: 26.0,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@ -127,6 +140,8 @@ ThemeData immichDarkTheme = ThemeData(
|
|||||||
titleTextStyle: TextStyle(
|
titleTextStyle: TextStyle(
|
||||||
fontFamily: 'WorkSans',
|
fontFamily: 'WorkSans',
|
||||||
color: immichDarkThemePrimaryColor,
|
color: immichDarkThemePrimaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18,
|
||||||
),
|
),
|
||||||
backgroundColor: const Color.fromARGB(255, 32, 33, 35),
|
backgroundColor: const Color.fromARGB(255, 32, 33, 35),
|
||||||
foregroundColor: immichDarkThemePrimaryColor,
|
foregroundColor: immichDarkThemePrimaryColor,
|
||||||
@ -159,6 +174,18 @@ ThemeData immichDarkTheme = ThemeData(
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: immichDarkThemePrimaryColor,
|
color: immichDarkThemePrimaryColor,
|
||||||
),
|
),
|
||||||
|
titleSmall: const TextStyle(
|
||||||
|
fontSize: 16.0,
|
||||||
|
),
|
||||||
|
titleMedium: const TextStyle(
|
||||||
|
fontSize: 18.0,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
titleLarge: const TextStyle(
|
||||||
|
fontSize: 26.0,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
|
||||||
),
|
),
|
||||||
cardColor: Colors.grey[900],
|
cardColor: Colors.grey[900],
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
BIN
mobile/openapi/lib/model/asset_response_dto.dart
generated
BIN
mobile/openapi/lib/model/asset_response_dto.dart
generated
Binary file not shown.
Loading…
Reference in New Issue
Block a user