kopia lustrzana https://gitlab.com/mysocialportal/relatica
Quick prototyping of new explorer screen
rodzic
dd40c9a577
commit
841a7ea7b6
|
@ -30,7 +30,7 @@ class _MediaAttachmentViewerControlState
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final item = widget.attachments[widget.index];
|
||||
final width = widget.width! * 0.9;
|
||||
final width = widget.width == null ? null : widget.width! * 0.9;
|
||||
final height = widget.height;
|
||||
openMediaScreenCallback() {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'screens/circle_management_screen.dart';
|
|||
import 'screens/contacts_screen.dart';
|
||||
import 'screens/disable_focus_mode_screen.dart';
|
||||
import 'screens/editor.dart';
|
||||
import 'screens/explore_screen.dart';
|
||||
import 'screens/filter_editor_screen.dart';
|
||||
import 'screens/filters_screen.dart';
|
||||
import 'screens/follow_request_adjudication_screen.dart';
|
||||
|
@ -23,7 +24,6 @@ import 'screens/message_threads_browser_screen.dart';
|
|||
import 'screens/messages_new_thread.dart';
|
||||
import 'screens/notifications_screen.dart';
|
||||
import 'screens/post_screen.dart';
|
||||
import 'screens/search_screen.dart';
|
||||
import 'screens/settings_screen.dart';
|
||||
import 'screens/sign_in.dart';
|
||||
import 'screens/splash.dart';
|
||||
|
@ -296,7 +296,7 @@ final routes = [
|
|||
name: ScreenPaths.search,
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
name: ScreenPaths.search,
|
||||
child: const SearchScreen(),
|
||||
child: const ExploreScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
|
|
|
@ -0,0 +1,356 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../controls/app_bottom_nav_bar.dart';
|
||||
import '../controls/current_profile_button.dart';
|
||||
import '../controls/image_control.dart';
|
||||
import '../controls/responsive_max_width.dart';
|
||||
import '../controls/search_result_status_control.dart';
|
||||
import '../controls/standard_app_drawer.dart';
|
||||
import '../models/auth/profile.dart';
|
||||
import '../models/connection.dart';
|
||||
import '../models/networking/paging_data.dart';
|
||||
import '../models/search_results.dart';
|
||||
import '../models/search_types.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../riverpod_controllers/account_services.dart';
|
||||
import '../riverpod_controllers/connection_manager_services.dart';
|
||||
import '../riverpod_controllers/entry_tree_item_services.dart';
|
||||
import '../riverpod_controllers/networking/friendica_search_client_services.dart';
|
||||
import '../riverpod_controllers/networking/network_status_services.dart';
|
||||
import '../routes.dart';
|
||||
import '../utils/snackbar_builder.dart';
|
||||
|
||||
class ExploreScreen extends ConsumerWidget {
|
||||
const ExploreScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
drawer: const StandardAppDrawer(skipPopDismiss: false),
|
||||
appBar: AppBar(
|
||||
leading: const CurrentProfileButton(),
|
||||
title: TabBar(
|
||||
tabs: [Tab(icon: Icon(Icons.search)), Tab(icon: Icon(Icons.bolt))],
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: TabBarView(children: [_SearchPanel(), Icon(Icons.bolt)]),
|
||||
),
|
||||
bottomNavigationBar: const AppBottomNavBar(
|
||||
currentButton: NavBarButtons.search,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchPanel extends ConsumerStatefulWidget {
|
||||
@override
|
||||
ConsumerState<_SearchPanel> createState() => _SearchPanelState();
|
||||
}
|
||||
|
||||
class _SearchPanelState extends ConsumerState<_SearchPanel> {
|
||||
static const limit = 50;
|
||||
static final _logger = Logger('SearchPanel');
|
||||
var searchTextController = TextEditingController();
|
||||
var searchType = SearchTypes.statusesText;
|
||||
PagingData nextPage = PagingData(limit: limit);
|
||||
var searchResult = SearchResults.empty();
|
||||
Profile? profileOfSearchRequest;
|
||||
|
||||
PagingData genNextPageData() {
|
||||
late final int offset;
|
||||
switch (searchType) {
|
||||
case SearchTypes.hashTag:
|
||||
offset = searchResult.hashtags.length;
|
||||
break;
|
||||
case SearchTypes.account:
|
||||
offset = searchResult.accounts.length;
|
||||
break;
|
||||
case SearchTypes.statusesText:
|
||||
offset = searchResult.statuses.length;
|
||||
break;
|
||||
case SearchTypes.directLink:
|
||||
offset = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return PagingData(limit: limit, offset: offset);
|
||||
}
|
||||
|
||||
Future<void> updateSearchResults(Profile profile, {bool reset = true}) async {
|
||||
profileOfSearchRequest = profile;
|
||||
if (reset) {
|
||||
nextPage = PagingData(limit: limit);
|
||||
searchResult = SearchResults.empty();
|
||||
}
|
||||
|
||||
final result = await ref.watch(searchResultsProvider(
|
||||
profile, searchType, searchTextController.text, nextPage)
|
||||
.future);
|
||||
result.match(
|
||||
onSuccess: (result) {
|
||||
searchResult = reset ? result.data : searchResult.merge(result.data);
|
||||
nextPage = result.next ?? genNextPageData();
|
||||
profileOfSearchRequest = profile;
|
||||
},
|
||||
onError: (error) =>
|
||||
buildSnackbar(context, 'Error getting search result: $error'),
|
||||
);
|
||||
}
|
||||
|
||||
clearSearchResults() {
|
||||
setState(() {
|
||||
searchResult = SearchResults.empty();
|
||||
searchTextController.text = '';
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finer('Build');
|
||||
final profile = ref.watch(activeProfileProvider);
|
||||
final searching = ref.watch(searchLoadingStatusProvider(profile));
|
||||
late Widget body;
|
||||
|
||||
if (profile != profileOfSearchRequest) {
|
||||
body = buildEmptyResult();
|
||||
} else if (searchResult.isEmpty && searching) {
|
||||
body = Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Searching for ${searchType.toLabel()} on: ${searchTextController.text}',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
body = ResponsiveMaxWidth(child: buildResultBody(profile));
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
if (searching) {
|
||||
return;
|
||||
}
|
||||
updateSearchResults(profile);
|
||||
return;
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 50.0, child: CurrentProfileButton()),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextField(
|
||||
controller: searchTextController,
|
||||
onSubmitted: (value) {
|
||||
searchTextController.text = value;
|
||||
updateSearchResults(profile);
|
||||
},
|
||||
onTapOutside: (event) {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: searchType == SearchTypes.directLink
|
||||
? 'URL'
|
||||
: '${searchType.toLabel()} Search Text',
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
updateSearchResults(profile);
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
),
|
||||
PopupMenuButton<SearchTypes>(
|
||||
initialValue: searchType,
|
||||
onSelected: (type) {
|
||||
setState(() {
|
||||
searchType = type;
|
||||
});
|
||||
},
|
||||
itemBuilder: (_) => SearchTypes.values
|
||||
.map((e) => PopupMenuItem(
|
||||
value: e,
|
||||
child: Text(
|
||||
e.toLabel(),
|
||||
)))
|
||||
.toList()),
|
||||
],
|
||||
),
|
||||
if (searching) const LinearProgressIndicator(),
|
||||
Expanded(child: body),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildResultBody(Profile profile) {
|
||||
_logger.finer('Building search result body with: $searchResult');
|
||||
switch (searchType) {
|
||||
case SearchTypes.hashTag:
|
||||
return buildHashtagResultWidget(profile);
|
||||
case SearchTypes.account:
|
||||
return buildAccountResultWidget(profile);
|
||||
case SearchTypes.statusesText:
|
||||
return buildStatusResultWidget(profile);
|
||||
case SearchTypes.directLink:
|
||||
return buildDirectLinkResult(profile);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildHashtagResultWidget(Profile profile) {
|
||||
final hashtags = searchResult.hashtags;
|
||||
if (hashtags.isEmpty) {
|
||||
return buildEmptyResult();
|
||||
}
|
||||
return ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == hashtags.length) {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
updateSearchResults(profile, reset: false);
|
||||
},
|
||||
child: const Text('Load more results'),
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
title: Text(hashtags[index]),
|
||||
);
|
||||
},
|
||||
itemCount: hashtags.length + 1,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAccountResultWidget(Profile profile) {
|
||||
final accounts = searchResult.accounts;
|
||||
if (accounts.isEmpty) {
|
||||
return buildEmptyResult();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemBuilder: (_, index) {
|
||||
if (index == accounts.length) {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
updateSearchResults(profile, reset: false);
|
||||
},
|
||||
child: const Text('Load more results'),
|
||||
);
|
||||
}
|
||||
|
||||
return buildConnectionListTile(profile, accounts[index]);
|
||||
},
|
||||
itemCount: accounts.length + 1,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildStatusResultWidget(Profile profile) {
|
||||
final statuses = searchResult.statuses;
|
||||
if (statuses.isEmpty) {
|
||||
return buildEmptyResult();
|
||||
}
|
||||
return ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == statuses.length) {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
updateSearchResults(profile, reset: false);
|
||||
},
|
||||
child: const Text('Load more results'),
|
||||
);
|
||||
}
|
||||
return buildStatusListTile(profile, statuses[index]);
|
||||
},
|
||||
itemCount: statuses.length + 1,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildDirectLinkResult(Profile profile) {
|
||||
if (searchResult.isEmpty) {
|
||||
return buildEmptyResult();
|
||||
}
|
||||
|
||||
return ListView(physics: const AlwaysScrollableScrollPhysics(), children: [
|
||||
if (searchResult.statuses.isNotEmpty)
|
||||
buildStatusListTile(profile, searchResult.statuses.first),
|
||||
if (searchResult.accounts.isNotEmpty)
|
||||
buildConnectionListTile(profile, searchResult.accounts.first),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget buildEmptyResult() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(searchTextController.text.isEmpty
|
||||
? 'Type search text to search'
|
||||
: 'No results for ${searchType.toLabel()} search on: ${searchTextController.text}'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildConnectionListTile(Profile profile, Connection connection) {
|
||||
return ListTile(
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(connectionByIdProvider(profile, connection.id))
|
||||
.withErrorAsync((_) => ref
|
||||
.read(connectionModifierProvider(profile, connection).notifier)
|
||||
.fullRefresh());
|
||||
|
||||
if (mounted) {
|
||||
context.pushNamed(ScreenPaths.userProfile,
|
||||
pathParameters: {'id': connection.id});
|
||||
}
|
||||
},
|
||||
leading: ImageControl(
|
||||
imageUrl: connection.avatarUrl.toString(),
|
||||
iconOverride: const Icon(Icons.person),
|
||||
width: 32.0,
|
||||
onTap: () => context.pushNamed(ScreenPaths.userProfile,
|
||||
pathParameters: {'id': connection.id}),
|
||||
),
|
||||
title: Text('${connection.name} (${connection.handle})'),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildStatusListTile(Profile profile, TimelineEntry status) {
|
||||
return SearchResultStatusControl(status, () async {
|
||||
final result = await ref
|
||||
.read(timelineUpdaterProvider(profile).notifier)
|
||||
.refreshStatusChain(status.id);
|
||||
if (context.mounted) {
|
||||
result.match(
|
||||
onSuccess: (entry) =>
|
||||
context.push('/post/view/${entry.id}/${status.id}'),
|
||||
onError: (error) =>
|
||||
buildSnackbar(context, 'Error getting post: $error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue