mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(mobile): new mobile UI (#12582)
This commit is contained in:
parent
b59abdff3d
commit
e9813315e7
56 changed files with 1960 additions and 1274 deletions
|
|
@ -92,6 +92,7 @@ class PersonResultPage extends HookConsumerWidget {
|
|||
Text(
|
||||
name.value,
|
||||
style: context.textTheme.titleLarge,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -125,9 +126,11 @@ class PersonResultPage extends HookConsumerWidget {
|
|||
headers: ApiService.getRequestHeaders(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: buildTitleBlock(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
||||
child: buildTitleBlock(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,25 +1,11 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
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/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||
import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
||||
import 'package:immich_mobile/widgets/search/curated_people_row.dart';
|
||||
import 'package:immich_mobile/widgets/search/curated_places_row.dart';
|
||||
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
|
||||
import 'package:immich_mobile/widgets/search/search_row_section.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/scaffold_error_body.dart';
|
||||
|
||||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
|
|
@ -28,12 +14,6 @@ class SearchPage extends HookConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final places = ref.watch(getPreviewPlacesProvider);
|
||||
final curatedPeople = ref.watch(getAllPeopleProvider);
|
||||
final isMapEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map));
|
||||
final double imageSize = math.min(context.width / 3, 150);
|
||||
|
||||
TextStyle categoryTitleStyle = const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15.0,
|
||||
|
|
@ -41,87 +21,6 @@ class SearchPage extends HookConsumerWidget {
|
|||
|
||||
Color categoryIconColor = context.colorScheme.onSurface;
|
||||
|
||||
showNameEditModel(
|
||||
String personId,
|
||||
String personName,
|
||||
) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return PersonNameEditForm(personId: personId, personName: personName);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildPeople() {
|
||||
return curatedPeople.widgetWhen(
|
||||
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
||||
onData: (people) {
|
||||
return SearchRowSection(
|
||||
onViewAllPressed: () => context.pushRoute(const AllPeopleRoute()),
|
||||
title: "search_page_people".tr(),
|
||||
isEmpty: people.isEmpty,
|
||||
child: CuratedPeopleRow(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
content: people
|
||||
.map((e) => SearchCuratedContent(label: e.name, id: e.id))
|
||||
.take(12)
|
||||
.toList(),
|
||||
onTap: (content, index) {
|
||||
context.pushRoute(
|
||||
PersonResultRoute(
|
||||
personId: content.id,
|
||||
personName: content.label,
|
||||
),
|
||||
);
|
||||
},
|
||||
onNameTap: (person, index) => {
|
||||
showNameEditModel(person.id, person.label),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildPlaces() {
|
||||
return places.widgetWhen(
|
||||
onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
|
||||
onData: (data) {
|
||||
return SearchRowSection(
|
||||
onViewAllPressed: () => context.pushRoute(const AllPlacesRoute()),
|
||||
title: "search_page_places".tr(),
|
||||
isEmpty: !isMapEnabled && data.isEmpty,
|
||||
child: CuratedPlacesRow(
|
||||
isMapEnabled: isMapEnabled,
|
||||
content: data,
|
||||
imageSize: imageSize,
|
||||
onTap: (content, index) {
|
||||
context.pushRoute(
|
||||
SearchInputRoute(
|
||||
prefilter: SearchFilter(
|
||||
people: {},
|
||||
location: SearchLocationFilter(
|
||||
city: content.label,
|
||||
),
|
||||
camera: SearchCameraFilter(),
|
||||
date: SearchDateFilter(),
|
||||
display: SearchDisplayFilters(
|
||||
isNotInAlbum: false,
|
||||
isArchive: false,
|
||||
isFavorite: false,
|
||||
),
|
||||
mediaType: AssetType.other,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildSearchButton() {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
|
|
@ -165,20 +64,17 @@ class SearchPage extends HookConsumerWidget {
|
|||
body: ListView(
|
||||
children: [
|
||||
buildSearchButton(),
|
||||
const SizedBox(height: 8.0),
|
||||
buildPeople(),
|
||||
const SizedBox(height: 8.0),
|
||||
buildPlaces(),
|
||||
const SizedBox(height: 24.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'search_page_your_activity',
|
||||
'search_page_categories',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
const SizedBox(height: 12.0),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.favorite_border_rounded,
|
||||
|
|
@ -200,16 +96,7 @@ class SearchPage extends HookConsumerWidget {
|
|||
).tr(),
|
||||
onTap: () => context.pushRoute(const RecentlyAddedRoute()),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'search_page_categories',
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
const CategoryDivider(),
|
||||
ListTile(
|
||||
title: Text('search_page_videos', style: categoryTitleStyle).tr(),
|
||||
leading: Icon(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class SearchInputPage extends HookConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isContextualSearch = useState(true);
|
||||
final textSearchController = useTextEditingController();
|
||||
final focusNode = useFocusNode();
|
||||
final filter = useState<SearchFilter>(
|
||||
SearchFilter(
|
||||
people: prefilter?.people ?? {},
|
||||
|
|
@ -440,6 +441,10 @@ class SearchInputPage extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
handleTextSubmitted(String value) {
|
||||
if (value.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isContextualSearch.value) {
|
||||
filter.value = filter.value.copyWith(
|
||||
context: value,
|
||||
|
|
@ -489,38 +494,82 @@ class SearchInputPage extends HookConsumerWidget {
|
|||
appBar: AppBar(
|
||||
automaticallyImplyLeading: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: isContextualSearch.value
|
||||
? const Icon(Icons.abc_rounded)
|
||||
: const Icon(Icons.image_search_rounded),
|
||||
onPressed: () {
|
||||
isContextualSearch.value = !isContextualSearch.value;
|
||||
textSearchController.clear();
|
||||
},
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 14.0),
|
||||
child: IconButton(
|
||||
icon: isContextualSearch.value
|
||||
? const Icon(Icons.abc_rounded)
|
||||
: const Icon(Icons.image_search_rounded),
|
||||
onPressed: () {
|
||||
isContextualSearch.value = !isContextualSearch.value;
|
||||
textSearchController.clear();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
onPressed: () => context.router.maybePop(),
|
||||
),
|
||||
title: TextField(
|
||||
controller: textSearchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: isContextualSearch.value
|
||||
? 'contextual_search'.tr()
|
||||
: 'filename_search'.tr(),
|
||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.themeData.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
title: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: context.colorScheme.onSurface.withAlpha(0),
|
||||
width: 0,
|
||||
),
|
||||
enabledBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
focusedBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withOpacity(0.075),
|
||||
context.colorScheme.primary.withOpacity(0.09),
|
||||
context.colorScheme.primary.withOpacity(0.075),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
onSubmitted: handleTextSubmitted,
|
||||
child: TextField(
|
||||
controller: textSearchController,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.all(8),
|
||||
prefixIcon: prefilter != null
|
||||
? null
|
||||
: Icon(
|
||||
Icons.search_rounded,
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
hintText: isContextualSearch.value
|
||||
? 'contextual_search'.tr()
|
||||
: 'filename_search'.tr(),
|
||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.themeData.colorScheme.onSurfaceSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: BorderSide(
|
||||
color: context.colorScheme.surfaceDim,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: BorderSide(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: BorderSide(
|
||||
color: context.colorScheme.surfaceDim,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
borderSide: BorderSide(
|
||||
color: context.colorScheme.primary.withAlpha(100),
|
||||
),
|
||||
),
|
||||
),
|
||||
onSubmitted: handleTextSubmitted,
|
||||
focusNode: focusNode,
|
||||
onTapOutside: (_) => focusNode.unfocus(),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue