mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-04-20 11:08:03 +08:00
458 lines
15 KiB
Dart
458 lines
15 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:PiliPlus/common/widgets/dialog/export_import.dart';
|
|
import 'package:PiliPlus/common/widgets/disabled_icon.dart';
|
|
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
|
import 'package:PiliPlus/common/widgets/sliver_wrap.dart';
|
|
import 'package:PiliPlus/http/loading_state.dart';
|
|
import 'package:PiliPlus/models_new/search/search_rcmd/data.dart';
|
|
import 'package:PiliPlus/pages/search/controller.dart';
|
|
import 'package:PiliPlus/pages/search/widgets/hot_keyword.dart';
|
|
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
|
|
import 'package:PiliPlus/utils/em.dart' show Em;
|
|
import 'package:PiliPlus/utils/extension/size_ext.dart';
|
|
import 'package:PiliPlus/utils/storage.dart';
|
|
import 'package:PiliPlus/utils/storage_key.dart';
|
|
import 'package:PiliPlus/utils/utils.dart';
|
|
import 'package:flutter/material.dart' hide LayoutBuilder;
|
|
import 'package:get/get.dart';
|
|
|
|
class SearchPage extends StatefulWidget {
|
|
const SearchPage({super.key});
|
|
|
|
@override
|
|
State<SearchPage> createState() => _SearchPageState();
|
|
}
|
|
|
|
class _SearchPageState extends State<SearchPage> {
|
|
final _tag = Utils.generateRandomString(6);
|
|
late final SSearchController _searchController;
|
|
late ThemeData theme;
|
|
late bool isPortrait;
|
|
late EdgeInsets padding;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_searchController = Get.put(
|
|
SSearchController(_tag),
|
|
tag: _tag,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
theme = Theme.of(context);
|
|
padding = MediaQuery.viewPaddingOf(context);
|
|
isPortrait = MediaQuery.sizeOf(context).isPortrait;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final trending = _searchController.enableTrending
|
|
? _buildHotSearch()
|
|
: null;
|
|
final rcmd = _searchController.enableSearchRcmd
|
|
? _buildHotSearch(isTrending: false)
|
|
: null;
|
|
|
|
return Scaffold(
|
|
appBar: _buildAppBar,
|
|
body: Padding(
|
|
padding: .only(left: padding.left, right: padding.right),
|
|
child: CustomScrollView(
|
|
slivers: [
|
|
if (_searchController.searchSuggestion) _buildSearchSuggest(),
|
|
if (isPortrait) ...[
|
|
?trending,
|
|
_buildHistory,
|
|
?rcmd,
|
|
] else if (_searchController.enableTrending ||
|
|
_searchController.enableSearchRcmd)
|
|
SliverCrossAxisGroup(
|
|
slivers: [
|
|
SliverMainAxisGroup(slivers: [?trending, ?rcmd]),
|
|
_buildHistory,
|
|
],
|
|
)
|
|
else
|
|
_buildHistory,
|
|
SliverPadding(padding: .only(bottom: padding.bottom)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
PreferredSizeWidget get _buildAppBar => AppBar(
|
|
shape: Border(
|
|
bottom: BorderSide(
|
|
color: theme.dividerColor.withValues(alpha: 0.08),
|
|
width: 1,
|
|
),
|
|
),
|
|
actions: [
|
|
Obx(
|
|
() => _searchController.showUidBtn.value
|
|
? IconButton(
|
|
tooltip: 'UID搜索用户',
|
|
icon: const Icon(Icons.person_outline, size: 22),
|
|
onPressed: () => Get.toNamed(
|
|
'/member?mid=${_searchController.controller.text}',
|
|
),
|
|
)
|
|
: const SizedBox.shrink(),
|
|
),
|
|
IconButton(
|
|
tooltip: '清空',
|
|
icon: const Icon(Icons.clear, size: 22),
|
|
onPressed: _searchController.onClear,
|
|
),
|
|
IconButton(
|
|
tooltip: '搜索',
|
|
onPressed: _searchController.submit,
|
|
icon: const Icon(Icons.search, size: 22),
|
|
),
|
|
const SizedBox(width: 10),
|
|
],
|
|
title: TextField(
|
|
autofocus: true,
|
|
focusNode: _searchController.searchFocusNode,
|
|
controller: _searchController.controller,
|
|
textInputAction: TextInputAction.search,
|
|
onChanged: _searchController.onChange,
|
|
decoration: InputDecoration(
|
|
visualDensity: .standard,
|
|
hintText: _searchController.hintText ?? '搜索',
|
|
border: InputBorder.none,
|
|
),
|
|
onSubmitted: (value) => _searchController.submit(),
|
|
),
|
|
);
|
|
|
|
Widget _buildSearchSuggest() {
|
|
return Obx(() {
|
|
final list = _searchController.searchSuggestList;
|
|
return list.isNotEmpty &&
|
|
list.first.term != null &&
|
|
_searchController.controller.text != ''
|
|
? SliverList.list(
|
|
children: list
|
|
.map(
|
|
(item) => InkWell(
|
|
borderRadius: const .all(.circular(4)),
|
|
onTap: () => _searchController.onClickKeyword(item.term!),
|
|
child: Padding(
|
|
padding: const .only(left: 20, top: 9, bottom: 9),
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: Em.regTitle(item.textRich)
|
|
.map(
|
|
(e) => TextSpan(
|
|
text: e.text,
|
|
style: e.isEm
|
|
? TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary,
|
|
)
|
|
: null,
|
|
),
|
|
)
|
|
.toList(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
.toList(),
|
|
)
|
|
: const SliverToBoxAdapter();
|
|
});
|
|
}
|
|
|
|
Widget _buildHotSearch({
|
|
bool isTrending = true,
|
|
}) {
|
|
final text = Text(
|
|
isTrending ? '大家都在搜' : '搜索发现',
|
|
strutStyle: const StrutStyle(leading: 0, height: 1),
|
|
style: theme.textTheme.titleMedium!.copyWith(
|
|
height: 1,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
);
|
|
final outline = theme.colorScheme.outline;
|
|
final secondary = theme.colorScheme.secondary;
|
|
final style = TextStyle(
|
|
height: 1,
|
|
fontSize: 13,
|
|
color: outline,
|
|
);
|
|
return SliverPadding(
|
|
padding: EdgeInsets.fromLTRB(
|
|
10,
|
|
!isTrending && (isPortrait || _searchController.enableTrending)
|
|
? 4
|
|
: 25,
|
|
4,
|
|
25,
|
|
),
|
|
sliver: SliverMainAxisGroup(
|
|
slivers: [
|
|
SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
|
|
sliver: SliverToBoxAdapter(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
isTrending
|
|
? Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
text,
|
|
const SizedBox(width: 14),
|
|
TextButton(
|
|
style: const ButtonStyle(
|
|
visualDensity: .compact,
|
|
tapTargetSize: .shrinkWrap,
|
|
padding: WidgetStatePropertyAll(
|
|
.symmetric(horizontal: 10),
|
|
),
|
|
),
|
|
onPressed: () => Get.toNamed('/searchTrending'),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
'完整榜单',
|
|
strutStyle: const StrutStyle(
|
|
leading: 0,
|
|
height: 1,
|
|
),
|
|
style: style,
|
|
),
|
|
Icon(
|
|
size: 18,
|
|
Icons.keyboard_arrow_right,
|
|
color: outline,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
)
|
|
: text,
|
|
TextButton.icon(
|
|
style: const ButtonStyle(
|
|
visualDensity: .compact,
|
|
tapTargetSize: .shrinkWrap,
|
|
padding: WidgetStatePropertyAll(
|
|
.symmetric(horizontal: 10),
|
|
),
|
|
),
|
|
onPressed: isTrending
|
|
? _searchController.queryTrendingList
|
|
: _searchController.queryRecommendList,
|
|
icon: Icon(
|
|
Icons.refresh_outlined,
|
|
size: 18,
|
|
color: secondary,
|
|
),
|
|
label: Text(
|
|
'刷新',
|
|
strutStyle: const StrutStyle(leading: 0, height: 1),
|
|
style: TextStyle(
|
|
height: 1,
|
|
color: secondary,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Obx(
|
|
() => _buildHotKey(
|
|
isTrending
|
|
? _searchController.trendingState.value
|
|
: _searchController.recommendData.value,
|
|
isTrending,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
late final mainAxisExtent = 16 + MediaQuery.textScalerOf(context).scale(14);
|
|
Widget get _buildHistory {
|
|
return Obx(
|
|
() {
|
|
final list = _searchController.historyList;
|
|
if (list.isEmpty) {
|
|
return const SliverToBoxAdapter();
|
|
}
|
|
final secondary = theme.colorScheme.secondary;
|
|
return SliverPadding(
|
|
padding: EdgeInsets.fromLTRB(
|
|
10,
|
|
!isPortrait
|
|
? 25
|
|
: _searchController.enableTrending
|
|
? 0
|
|
: 6,
|
|
6,
|
|
25,
|
|
),
|
|
sliver: SliverMainAxisGroup(
|
|
slivers: [
|
|
SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
|
|
sliver: SliverToBoxAdapter(
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
'搜索历史',
|
|
strutStyle: const StrutStyle(leading: 0, height: 1),
|
|
style: theme.textTheme.titleMedium!.copyWith(
|
|
height: 1,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
_recordBtn,
|
|
_exportBtn,
|
|
const Spacer(),
|
|
TextButton.icon(
|
|
style: const ButtonStyle(
|
|
visualDensity: .compact,
|
|
tapTargetSize: .shrinkWrap,
|
|
padding: WidgetStatePropertyAll(
|
|
.symmetric(horizontal: 10),
|
|
),
|
|
),
|
|
onPressed: _searchController.onClearHistory,
|
|
icon: Icon(
|
|
Icons.clear_all_outlined,
|
|
size: 18,
|
|
color: secondary,
|
|
),
|
|
label: Text(
|
|
'清空',
|
|
style: TextStyle(
|
|
height: 1,
|
|
color: secondary,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
SliverFixedWrap(
|
|
mainAxisExtent: mainAxisExtent,
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
delegate: SliverChildBuilderDelegate(
|
|
addAutomaticKeepAlives: false,
|
|
addRepaintBoundaries: false,
|
|
childCount: list.length,
|
|
(context, index) => SearchText(
|
|
text: list[index],
|
|
onTap: _searchController.onClickKeyword,
|
|
onLongPress: _searchController.onLongSelect,
|
|
fontSize: 14,
|
|
height: 1,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 11,
|
|
vertical: 8,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget get _recordBtn => Obx(
|
|
() {
|
|
bool enable = _searchController.recordSearchHistory.value;
|
|
return IconButton(
|
|
iconSize: 22,
|
|
tooltip: enable ? '记录搜索' : '无痕搜索',
|
|
icon: DisabledIcon(
|
|
disable: !enable,
|
|
child: Icon(
|
|
Icons.history,
|
|
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
|
),
|
|
),
|
|
style: const ButtonStyle(
|
|
visualDensity: .comfortable,
|
|
tapTargetSize: .shrinkWrap,
|
|
padding: WidgetStatePropertyAll(.zero),
|
|
),
|
|
onPressed: () {
|
|
enable = !enable;
|
|
_searchController.recordSearchHistory.value = enable;
|
|
GStorage.setting.put(
|
|
SettingBoxKey.recordSearchHistory,
|
|
enable,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
Widget get _exportBtn => IconButton(
|
|
iconSize: 22,
|
|
tooltip: '导入/导出历史记录',
|
|
icon: Icon(
|
|
Icons.import_export_outlined,
|
|
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
|
),
|
|
style: const ButtonStyle(
|
|
visualDensity: .comfortable,
|
|
tapTargetSize: .shrinkWrap,
|
|
padding: WidgetStatePropertyAll(.zero),
|
|
),
|
|
onPressed: () => showImportExportDialog<List>(
|
|
context,
|
|
title: '历史记录',
|
|
localFileName: () => 'search',
|
|
onExport: () => jsonEncode(_searchController.historyList),
|
|
onImport: (json) {
|
|
final list = List<String>.from(json);
|
|
_searchController.historyList.value = list;
|
|
GStorage.historyWord.put('cacheList', list);
|
|
},
|
|
),
|
|
);
|
|
|
|
Widget _buildHotKey(
|
|
LoadingState<SearchRcmdData> loadingState,
|
|
bool isTrending,
|
|
) {
|
|
return switch (loadingState) {
|
|
Success(:final response) when (response.list?.isNotEmpty ?? false) =>
|
|
SliverHotKeyword(
|
|
hotSearchList: response.list!,
|
|
onClick: _searchController.onClickKeyword,
|
|
),
|
|
Error(:final errMsg) => HttpError(
|
|
safeArea: false,
|
|
errMsg: errMsg,
|
|
onReload: isTrending
|
|
? _searchController.queryTrendingList
|
|
: _searchController.queryRecommendList,
|
|
),
|
|
_ => const SliverToBoxAdapter(),
|
|
};
|
|
}
|
|
}
|