Merge branch 'design'

This commit is contained in:
guozhigq
2024-10-19 15:46:22 +08:00
46 changed files with 782 additions and 783 deletions

View File

@ -183,8 +183,10 @@ class _BangumiPageState extends State<BangumiPage>
return HttpError(
errMsg: data['msg'],
fn: () {
_futureBuilderFuture =
_bangumidController.queryBangumiListFeed();
setState(() {
_futureBuilderFuture =
_bangumidController.queryBangumiListFeed();
});
},
);
}

View File

@ -77,10 +77,10 @@ class _BlackListPageState extends State<BlackListPage> {
List<BlackListItem> list = _blackListController.blackList;
return Obx(
() => list.isEmpty
? CustomScrollView(
slivers: [
HttpError(errMsg: '你没有拉黑任何人哦_', fn: () => {})
],
? HttpError(
errMsg: '你没有拉黑任何人哦_',
fn: () => {},
isInSliver: false,
)
: ListView.builder(
controller: scrollController,
@ -119,13 +119,10 @@ class _BlackListPageState extends State<BlackListPage> {
),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => _blackListController.queryBlacklist(),
)
],
return HttpError(
errMsg: data['msg'],
fn: () => _blackListController.queryBlacklist(),
isInSliver: false,
);
}
} else {

View File

@ -14,7 +14,7 @@ class DynamicDetailController extends GetxController {
int? type;
dynamic item;
int? floor;
int currentPage = 0;
String nextOffset = "";
bool isLoadingMore = false;
RxString noMore = ''.obs;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
@ -49,25 +49,25 @@ class DynamicDetailController extends GetxController {
Future queryReplyList({reqType = 'init'}) async {
if (reqType == 'init') {
currentPage = 0;
nextOffset = "";
}
var res = await ReplyHttp.replyList(
oid: oid!,
pageNum: currentPage + 1,
nextOffset: nextOffset,
type: type!,
sort: _sortType.index,
);
if (res['status']) {
List<ReplyItemModel> replies = res['data'].replies;
acount.value = res['data'].page.acount;
acount.value = res['data'].cursor.allCount;
nextOffset = res['data'].cursor.paginationReply.nextOffset ?? "";
if (replies.isNotEmpty) {
currentPage++;
noMore.value = '加载中...';
if (replies.length < 20) {
if (res['data'].cursor.isEnd == true) {
noMore.value = '没有更多了';
}
} else {
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
noMore.value = nextOffset == "" ? '还没有评论' : '没有更多了';
}
if (reqType == 'init') {
// 添加置顶回复

View File

@ -103,17 +103,14 @@ class _FansPageState extends State<FansPage> {
),
);
} else {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: data['msg'],
fn: () {
_futureBuilderFuture =
_fansController.queryFans('init');
},
)
],
return HttpError(
errMsg: data['msg'],
fn: () {
setState(() {
_futureBuilderFuture = _fansController.queryFans('init');
});
},
isInSliver: false,
);
}
} else {

View File

@ -112,23 +112,19 @@ class _FavPageState extends State<FavPage> {
),
);
} else {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture = _favController.queryFavFolder();
});
}
},
),
],
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture = _favController.queryFavFolder();
});
}
},
isInSliver: false,
);
}
} else {

View File

@ -94,13 +94,14 @@ class _FollowListState extends State<FollowList> {
: const CustomScrollView(slivers: [NoData()]),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => widget.ctr.queryFollowings('init'),
)
],
return HttpError(
errMsg: data['msg'],
fn: () {
setState(() {
_futureBuilderFuture = widget.ctr.queryFollowings('init');
});
},
isInSliver: false,
);
}
} else {

View File

@ -112,13 +112,10 @@ class _OwnerFollowListState extends State<OwnerFollowList>
: const CustomScrollView(slivers: [NoData()]),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => widget.ctr.queryFollowings('init'),
)
],
return HttpError(
errMsg: data['msg'],
fn: () => widget.ctr.queryFollowings('init'),
isInSliver: false,
);
}
} else {

View File

@ -82,10 +82,10 @@ class _FollowSearchPageState extends State<FollowSearchPage> {
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data;
if (data == null) {
return CustomScrollView(
slivers: [
HttpError(errMsg: snapshot.data['msg'], fn: reRequest)
],
return HttpError(
errMsg: snapshot.data['msg'],
fn: reRequest,
isInSliver: false,
);
}
if (data['status']) {
@ -101,15 +101,17 @@ class _FollowSearchPageState extends State<FollowSearchPage> {
);
}),
)
: CustomScrollView(
slivers: [HttpError(errMsg: '未搜索到结果', fn: reRequest)],
: HttpError(
errMsg: '未搜索到结果',
fn: reRequest,
isInSliver: false,
),
);
} else {
return CustomScrollView(
slivers: [
HttpError(errMsg: snapshot.data['msg'], fn: reRequest)
],
return HttpError(
errMsg: snapshot.data['msg'],
fn: reRequest,
isInSliver: false,
);
}
} else {

View File

@ -15,7 +15,7 @@ class HtmlRenderController extends GetxController {
RxInt oid = (-1).obs;
late Map response;
int? floor;
int currentPage = 0;
String nextOffset = "";
bool isLoadingMore = false;
RxString noMore = ''.obs;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
@ -52,21 +52,21 @@ class HtmlRenderController extends GetxController {
Future queryReplyList({reqType = 'init'}) async {
var res = await ReplyHttp.replyList(
oid: oid.value,
pageNum: currentPage + 1,
nextOffset: nextOffset,
type: type,
sort: _sortType.index,
);
if (res['status']) {
List<ReplyItemModel> replies = res['data'].replies;
acount.value = res['data'].page.acount;
acount.value = res['data'].cursor.allCount;
nextOffset = res['data'].cursor.paginationReply.nextOffset ?? "";
if (replies.isNotEmpty) {
currentPage++;
noMore.value = '加载中...';
if (replies.length < 20) {
if (res['data'].cursor.isEnd == true) {
noMore.value = '没有更多了';
}
} else {
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
noMore.value = nextOffset == "" ? '还没有评论' : '没有更多了';
}
if (reqType == 'init') {
// 添加置顶回复
@ -102,7 +102,7 @@ class HtmlRenderController extends GetxController {
}
sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels;
currentPage = 0;
nextOffset = "";
replyList.clear();
queryReplyList(reqType: 'init');
}

View File

@ -380,13 +380,10 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
);
} else {
// 请求错误
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
isInSliver: false,
);
}
} else {

View File

@ -22,11 +22,11 @@ class LaterController extends GetxController {
userInfo = userInfoCache.get('userInfoCache');
}
Future queryLaterList() async {
Future queryLaterList({type = 'init'}) async {
if (userInfo == null) {
return {'status': false, 'msg': '账号未登录', 'code': -101};
}
isLoading.value = true;
isLoading.value = type == 'init';
var res = await UserHttp.seeYouLater();
if (res['status']) {
count = res['data']['count'];

View File

@ -66,67 +66,74 @@ class _LaterPageState extends State<LaterPage> {
const SizedBox(width: 8),
],
),
body: CustomScrollView(
controller: _laterController.scrollController,
slivers: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
return Obx(
() => _laterController.laterList.isNotEmpty &&
!_laterController.isLoading.value
? SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
var videoItem = _laterController.laterList[index];
return VideoCardH(
videoItem: videoItem,
source: 'later',
onPressedFn: () => _laterController.toViewDel(
aid: videoItem.aid));
}, childCount: _laterController.laterList.length),
)
: _laterController.isLoading.value
? const SliverToBoxAdapter(
child: Center(child: Text('加载中')),
)
: const NoData(),
);
body: RefreshIndicator(
onRefresh: () async {
await _laterController.queryLaterList(type: 'onRefresh');
},
child: CustomScrollView(
controller: _laterController.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
return Obx(
() => _laterController.laterList.isNotEmpty &&
!_laterController.isLoading.value
? SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
var videoItem =
_laterController.laterList[index];
return VideoCardH(
videoItem: videoItem,
source: 'later',
onPressedFn: () => _laterController
.toViewDel(aid: videoItem.aid));
}, childCount: _laterController.laterList.length),
)
: _laterController.isLoading.value
? const SliverToBoxAdapter(
child: Center(child: Text('加载中')),
)
: const NoData(),
);
} else {
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_laterController.queryLaterList();
});
}
},
);
}
} else {
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_laterController.queryLaterList();
});
}
},
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 10),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 10),
);
}
},
),
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.of(context).padding.bottom + 10,
},
),
)
],
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.of(context).padding.bottom + 10,
),
)
],
),
),
floatingActionButton: Obx(
() => _laterController.laterList.isNotEmpty

View File

@ -11,6 +11,7 @@ import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/pages/rank/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/storage.dart';
import './controller.dart';
@ -126,6 +127,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
double sheetHeight = MediaQuery.sizeOf(context).height -
MediaQuery.of(context).padding.top -
MediaQuery.sizeOf(context).width * 9 / 16;
GlobalDataCache().sheetHeight = sheetHeight;
localCache.put('sheetHeight', sheetHeight);
localCache.put('statusBarHeight', statusBarHeight);

View File

@ -138,16 +138,10 @@ class _MemberArticlePageState extends State<MemberArticlePage> {
}
Widget _buildError(String errMsg) {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: HttpError(
errMsg: errMsg,
fn: () {},
),
),
],
return HttpError(
errMsg: errMsg,
fn: () {},
isInSliver: false,
);
}

View File

@ -164,13 +164,10 @@ class _MemberSearchPageState extends State<MemberSearchPage>
),
);
} else {
return CustomScrollView(
slivers: <Widget>[
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
isInSliver: false,
);
}
} else {

View File

@ -85,18 +85,14 @@ class _MessageLikePageState extends State<MessageLikePage> {
);
} else {
// 请求错误
return CustomScrollView(
slivers: [
HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageLikeCtr.queryMessageLike();
});
},
)
],
return HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture = _messageLikeCtr.queryMessageLike();
});
},
isInSliver: false,
);
}
} else {

View File

@ -82,18 +82,15 @@ class _MessageReplyPageState extends State<MessageReplyPage> {
);
} else {
// 请求错误
return CustomScrollView(
slivers: [
HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageReplyCtr.queryMessageReply();
});
},
)
],
return HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageReplyCtr.queryMessageReply();
});
},
isInSliver: false,
);
}
} else {

View File

@ -63,18 +63,15 @@ class _MessageSystemPageState extends State<MessageSystemPage> {
);
} else {
// 请求错误
return CustomScrollView(
slivers: [
HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageSystemCtr.queryMessageSystem();
});
},
)
],
return HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageSystemCtr.queryMessageSystem();
});
},
isInSliver: false,
);
}
} else {

View File

@ -1,10 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/suggest.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/storage.dart';
class SSearchController extends GetxController {
@ -12,7 +15,7 @@ class SSearchController extends GetxController {
RxString searchKeyWord = ''.obs;
Rx<TextEditingController> controller = TextEditingController().obs;
RxList<HotSearchItem> hotSearchList = <HotSearchItem>[].obs;
Box histiryWord = GStrorage.historyword;
Box localCache = GStrorage.localCache;
List historyCacheList = [];
RxList historyList = [].obs;
RxList<SearchSuggestItem> searchSuggestList = <SearchSuggestItem>[].obs;
@ -22,48 +25,55 @@ class SSearchController extends GetxController {
RxString defaultSearch = ''.obs;
Box setting = GStrorage.setting;
bool enableHotKey = true;
bool enableSearchSuggest = true;
late StreamController<bool> clearStream = StreamController<bool>.broadcast();
@override
void onInit() {
super.onInit();
// 其他页面跳转过来
if (Get.parameters.keys.isNotEmpty) {
if (Get.parameters['keyword'] != null) {
onClickKeyword(Get.parameters['keyword']!);
final parameters = Get.parameters;
if (parameters.keys.isNotEmpty) {
final keyword = parameters['keyword'];
if (keyword != null) {
onClickKeyword(keyword);
}
if (Get.parameters['hintText'] != null) {
hintText = Get.parameters['hintText']!;
final hint = parameters['hintText'];
if (hint != null) {
hintText = hint;
searchKeyWord.value = hintText;
}
}
historyCacheList = histiryWord.get('cacheList') ?? [];
historyCacheList = GlobalDataCache().historyCacheList;
historyList.value = historyCacheList;
enableHotKey = setting.get(SettingBoxKey.enableHotKey, defaultValue: true);
enableSearchSuggest = GlobalDataCache().enableSearchSuggest;
}
void onChange(value) {
searchKeyWord.value = value;
if (value == '') {
searchSuggestList.value = [];
clearStream.add(false);
return;
}
_debouncer.call(() => querySearchSuggest(value));
clearStream.add(true);
if (enableSearchSuggest) {
_debouncer.call(() => querySearchSuggest(value));
}
}
void onClear() {
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
controller.value.clear();
searchKeyWord.value = '';
searchSuggestList.value = [];
} else {
Get.back();
}
controller.value.clear();
searchKeyWord.value = '';
searchSuggestList.value = [];
clearStream.add(false);
}
// 搜索
void submit() {
// ignore: unrelated_type_equality_checks
if (searchKeyWord == '') {
if (searchKeyWord.value == '') {
return;
}
List arr = historyCacheList.where((e) => e != searchKeyWord.value).toList();
@ -73,7 +83,7 @@ class SSearchController extends GetxController {
historyList.value = historyCacheList;
// 手动刷新
historyList.refresh();
histiryWord.put('cacheList', historyCacheList);
localCache.put('cacheList', historyCacheList);
searchFocusNode.unfocus();
Get.toNamed('/searchResult', parameters: {'keyword': searchKeyWord.value});
}
@ -117,13 +127,14 @@ class SSearchController extends GetxController {
int index = historyList.indexOf(word);
historyList.removeAt(index);
historyList.refresh();
histiryWord.put('cacheList', historyList);
localCache.put('cacheList', historyList);
}
onClearHis() {
historyList.value = [];
historyCacheList = [];
historyList.refresh();
histiryWord.put('cacheList', []);
localCache.put('cacheList', []);
SmartDialog.showToast('搜索历史已清空');
}
}

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
@ -54,7 +53,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
actions: [
IconButton(
onPressed: () => _searchController.submit(),
icon: const Icon(CupertinoIcons.search, size: 22),
icon: const Icon(Icons.search),
),
const SizedBox(width: 10)
],
@ -68,13 +67,19 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
decoration: InputDecoration(
hintText: _searchController.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _searchController.onClear(),
suffixIcon: StreamBuilder(
initialData: false,
stream: _searchController.clearStream.stream,
builder: (_, snapshot) {
if (snapshot.data == true) {
return IconButton(
icon: const Icon(Icons.clear, size: 22),
onPressed: () => _searchController.onClear(),
);
} else {
return const SizedBox();
}
},
),
),
onSubmitted: (String value) => _searchController.submit(),
@ -84,7 +89,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 12),
const SizedBox(height: 6),
// 搜索建议
_searchSuggest(),
// 热搜
@ -135,7 +140,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
padding: const EdgeInsets.fromLTRB(6, 0, 6, 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -153,7 +158,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
padding: MaterialStateProperty.all(const EdgeInsets.only(
left: 10, top: 6, bottom: 6, right: 10)),
),
onPressed: () => ctr.queryHotSearchList(),
onPressed: ctr.queryHotSearchList,
icon: const Icon(Icons.refresh_outlined, size: 18),
label: const Text('刷新'),
),
@ -187,13 +192,10 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
isInSliver: false,
);
}
} else {
@ -202,6 +204,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
return HotKeyword(
width: width,
hotSearchList: _searchController.hotSearchList,
onClick: () {},
);
} else {
return const SizedBox();
@ -220,13 +223,13 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
return Obx(
() => Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(10, 25, 6, 0),
padding: const EdgeInsets.fromLTRB(10, 20, 4, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_searchController.historyList.isNotEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(6, 0, 0, 2),
padding: const EdgeInsets.fromLTRB(6, 0, 6, 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -237,10 +240,19 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
TextButton(
onPressed: () => _searchController.onClearHis(),
child: const Text('清空'),
)
SizedBox(
height: 34,
child: TextButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all(
const EdgeInsets.only(
left: 10, top: 6, bottom: 6, right: 10)),
),
onPressed: _searchController.onClearHis,
icon: const Icon(Icons.clear_all_outlined, size: 18),
label: const Text('清空'),
),
),
],
),
),

View File

@ -1,15 +1,15 @@
// ignore: file_names
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
class HotKeyword extends StatelessWidget {
final double? width;
final List? hotSearchList;
final Function? onClick;
final double width;
final List hotSearchList;
final Function onClick;
const HotKeyword({
this.width,
this.hotSearchList,
this.onClick,
required this.width,
required this.hotSearchList,
required this.onClick,
super.key,
});
@ -18,45 +18,67 @@ class HotKeyword extends StatelessWidget {
return Wrap(
runSpacing: 0.4,
spacing: 5.0,
children: [
for (var i in hotSearchList!)
SizedBox(
width: width! / 2 - 4,
child: Material(
borderRadius: BorderRadius.circular(3),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onClick!(i.keyword),
child: Padding(
padding: EdgeInsets.only(
left: 2,
right: hotSearchList!.indexOf(i) % 2 == 1 ? 10 : 0),
child: Row(
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.fromLTRB(6, 5, 4, 5),
child: Text(
i.keyword!,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: const TextStyle(fontSize: 14),
),
),
),
if (i.icon != null && i.icon != '')
SizedBox(
height: 15,
child: CachedNetworkImage(
imageUrl: i.icon!, height: 15.0),
),
],
),
),
),
),
),
],
children: hotSearchList.map((item) {
return HotKeywordItem(
width: width,
item: item,
onClick: onClick,
isRightPadding: hotSearchList.indexOf(item) % 2 == 1,
);
}).toList(),
);
}
}
class HotKeywordItem extends StatelessWidget {
final double width;
final dynamic item;
final Function onClick;
final bool isRightPadding;
const HotKeywordItem({
required this.width,
required this.item,
required this.onClick,
required this.isRightPadding,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width / 2 - 4,
child: Material(
borderRadius: BorderRadius.circular(4),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onClick.call(item.keyword),
child: Padding(
padding: EdgeInsets.only(left: 2, right: isRightPadding ? 10 : 0),
child: Row(
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.fromLTRB(6, 5, 4, 5),
child: Text(
item.keyword,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: const TextStyle(fontSize: 14),
),
),
),
if (item.icon != null && item.icon != '')
SizedBox(
height: 15,
child:
CachedNetworkImage(imageUrl: item.icon!, height: 15.0),
),
],
),
),
),
),
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:pilipala/utils/feed_back.dart';
class SearchText extends StatelessWidget {
final String? searchText;
@ -17,30 +18,31 @@ class SearchText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Material(
color: isSelect
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
? colorScheme.primaryContainer
: colorScheme.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
child: Padding(
padding: EdgeInsets.zero,
child: InkWell(
onTap: () {
onSelect!(searchText);
onSelect?.call(searchText);
},
onLongPress: () {
onLongSelect!(searchText);
feedBack();
onLongSelect?.call(searchText);
},
borderRadius: BorderRadius.circular(6),
child: Padding(
padding:
const EdgeInsets.only(top: 5, bottom: 5, left: 11, right: 11),
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 11),
child: Text(
searchText!,
style: TextStyle(
color: isSelect
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
),
),

View File

@ -109,33 +109,25 @@ class _SearchPanelState extends State<SearchPanel>
}
});
} else {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: data['msg'],
fn: () {
setState(() {
_searchPanelController.onSearch();
});
},
),
],
return HttpError(
errMsg: data['msg'],
fn: () {
setState(() {
_searchPanelController.onSearch();
});
},
isInSliver: false,
);
}
} else {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: '没有相关数据',
fn: () {
setState(() {
_searchPanelController.onSearch();
});
},
),
],
return HttpError(
errMsg: '没有相关数据',
fn: () {
setState(() {
_searchPanelController.onSearch();
});
},
isInSliver: false,
);
}
} else {

View File

@ -174,14 +174,11 @@ Widget searchArticlePanel(BuildContext context, ctr, list) {
);
},
)
: CustomScrollView(
slivers: [
HttpError(
errMsg: '没有数据',
isShowBtn: false,
fn: () => {},
)
],
: HttpError(
errMsg: '没有数据',
isShowBtn: false,
fn: () => {},
isInSliver: false,
),
);
}

View File

@ -46,14 +46,11 @@ class SearchVideoPanel extends StatelessWidget {
);
},
)
: CustomScrollView(
slivers: [
HttpError(
errMsg: '没有数据',
isShowBtn: false,
fn: () => {},
)
],
: HttpError(
errMsg: '没有数据',
isShowBtn: false,
fn: () => {},
isInSliver: false,
),
),
// 分类筛选

View File

@ -5,6 +5,7 @@ import 'package:hive/hive.dart';
import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/common/reply_sort_type.dart';
import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/storage.dart';
import '../home/index.dart';
@ -146,6 +147,15 @@ class _ExtraSettingState extends State<ExtraSetting> {
setKey: SettingBoxKey.enableHotKey,
defaultVal: true,
),
SetSwitchItem(
title: '展示搜索建议',
subTitle: '输入搜索内容时展示建议词',
setKey: SettingBoxKey.enableSearchSuggest,
defaultVal: true,
callFn: (val) {
GlobalDataCache().enableSearchSuggest = val;
},
),
SetSwitchItem(
title: '搜索默认词',
subTitle: '是否展示搜索框默认词',

View File

@ -68,30 +68,27 @@ class _SubPageState extends State<SubPage> {
),
);
} else {
return const CustomScrollView(
physics: NeverScrollableScrollPhysics(),
slivers: [HttpError(errMsg: '', btnText: '没有数据', fn: null)],
return const HttpError(
errMsg: '',
btnText: '没有数据',
fn: null,
isInSliver: false,
);
}
} else {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_subController.querySubFolder();
});
}
},
),
],
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture = _subController.querySubFolder();
});
}
},
isInSliver: false,
);
}
} else {

View File

@ -242,6 +242,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
showBottomSheet(
context: context,
enableDrag: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(25),
topRight: Radius.circular(25),
),
),
builder: (BuildContext context) {
return AiDetail(modelResult: videoIntroController.modelResult);
},

View File

@ -21,11 +21,9 @@ class VideoReplyController extends GetxController {
// rpid 请求楼中楼回复
String? rpid;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
// 当前页
int currentPage = 0;
String nextOffset = "";
bool isLoadingMore = false;
RxString noMore = ''.obs;
int ps = 20;
RxInt count = 0.obs;
// 当前回复的回复
ReplyItemModel? currentReplyItem;
@ -57,7 +55,7 @@ class VideoReplyController extends GetxController {
}
isLoadingMore = true;
if (type == 'init') {
currentPage = 0;
nextOffset = '';
noMore.value = '';
}
if (noMore.value == '没有更多了') {
@ -66,28 +64,20 @@ class VideoReplyController extends GetxController {
}
final res = await ReplyHttp.replyList(
oid: aid!,
pageNum: currentPage + 1,
ps: ps,
nextOffset: nextOffset,
type: ReplyType.video.index,
sort: _sortType.index,
);
if (res['status']) {
final List<ReplyItemModel> replies = res['data'].replies;
nextOffset = res['data'].cursor.paginationReply.nextOffset ?? "";
if (replies.isNotEmpty) {
noMore.value = '加载中...';
/// 第一页回复数小于20
if (currentPage == 0 && replies.length < 18) {
noMore.value = '没有更多了';
}
currentPage++;
if (replyList.length == res['data'].page.acount) {
if (res['data'].cursor.isEnd == true) {
noMore.value = '没有更多了';
}
} else {
// 未登录状态replies可能返回null
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了';
noMore.value = nextOffset == "" ? '还没有评论' : '没有更多了';
}
if (type == 'init') {
// 添加置顶回复
@ -99,7 +89,7 @@ class VideoReplyController extends GetxController {
}
}
replies.insertAll(0, res['data'].topReplies);
count.value = res['data'].page.count;
count.value = res['data'].cursor.allCount;
replyList.value = replies;
} else {
replyList.addAll(replies);
@ -130,7 +120,7 @@ class VideoReplyController extends GetxController {
}
sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels;
currentPage = 0;
nextOffset = "";
noMore.value = '';
replyList.clear();
queryReplyList(type: 'init');

View File

@ -238,28 +238,53 @@ class ReplyItem extends StatelessWidget {
// title
Container(
margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4),
child: Text.rich(
style: const TextStyle(height: 1.75),
maxLines:
replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999,
overflow: TextOverflow.ellipsis,
TextSpan(
children: [
if (replyItem!.isTop!)
const WidgetSpan(
alignment: PlaceholderAlignment.top,
child: PBadge(
text: 'TOP',
size: 'small',
stack: 'normal',
type: 'line',
fs: 9,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
String text = replyItem?.content?.message ?? '';
bool didExceedMaxLines = false;
final double maxWidth = boxConstraints.maxWidth;
TextPainter? textPainter;
final int maxLines =
replyItem!.content!.isText! && replyLevel == '1' ? 6 : 999;
try {
textPainter = TextPainter(
text: TextSpan(text: text),
maxLines: maxLines,
textDirection: Directionality.of(context),
);
textPainter.layout(maxWidth: maxWidth);
didExceedMaxLines = textPainter.didExceedMaxLines;
} catch (e) {
debugPrint('Error while measuring text: $e');
didExceedMaxLines = false;
}
return Text.rich(
style: const TextStyle(height: 1.75),
TextSpan(
children: [
if (replyItem!.isTop!)
const WidgetSpan(
alignment: PlaceholderAlignment.top,
child: PBadge(
text: 'TOP',
size: 'small',
stack: 'normal',
type: 'line',
fs: 9,
),
),
buildContent(
context,
replyItem!,
replyReply,
null,
didExceedMaxLines,
textPainter,
),
buildContent(context, replyItem!, replyReply, null),
],
),
),
],
),
);
}),
),
// 操作区域
bottonAction(context, replyItem!.replyControl, replySave),
@ -465,8 +490,8 @@ class ReplyItemRow extends StatelessWidget {
fs: 9,
),
),
buildContent(
context, replies![i], replyReply, replyItem),
buildContent(context, replies![i], replyReply,
replyItem, false, null),
],
),
),
@ -508,7 +533,13 @@ class ReplyItemRow extends StatelessWidget {
}
InlineSpan buildContent(
BuildContext context, replyItem, replyReply, fReplyItem) {
BuildContext context,
replyItem,
replyReply,
fReplyItem,
bool didExceedMaxLines,
TextPainter? textPainter,
) {
final String routePath = Get.currentRoute;
bool isVideoPage = routePath.startsWith('/video');
ColorScheme colorScheme = Theme.of(context).colorScheme;
@ -519,6 +550,25 @@ InlineSpan buildContent(
final content = replyItem.content;
final List<InlineSpan> spanChilds = <InlineSpan>[];
if (didExceedMaxLines && content.message != '') {
final textSize = textPainter!.size;
var position = textPainter.getPositionForOffset(
Offset(
textSize.width,
textSize.height,
),
);
final endOffset = textPainter.getOffsetBefore(position.offset);
if (endOffset != null && endOffset > 0) {
content.message = content.message.substring(0, endOffset);
} else {
content.message = content.message.substring(0, position.offset);
}
} else {
content.message = content.message2;
}
// 投票
if (content.vote.isNotEmpty) {
content.message.splitMapJoin(RegExp(r"\{vote:.*?\}"),
@ -547,13 +597,6 @@ InlineSpan buildContent(
});
}
content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
content.message = content.message
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&nbsp;', ' ');
// 构建正则表达式
final List<String> specialTokens = [
...content.emote.keys,
@ -874,6 +917,18 @@ InlineSpan buildContent(
}
}
}
if (didExceedMaxLines) {
spanChilds.add(
TextSpan(
text: '\n查看更多',
style: TextStyle(
color: colorScheme.primary,
),
),
);
}
// 图片渲染
if (content.pictures.isNotEmpty) {
final List<String> picList = <String>[];

View File

@ -1,16 +1,11 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/video/ai.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/utils.dart';
Box localCache = GStrorage.localCache;
late double sheetHeight;
class AiDetail extends StatelessWidget {
final ModelResult? modelResult;
@ -21,124 +16,21 @@ class AiDetail extends StatelessWidget {
@override
Widget build(BuildContext context) {
sheetHeight = localCache.get('sheetHeight');
return Container(
color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.only(left: 14, right: 14),
height: sheetHeight,
padding: const EdgeInsets.only(left: 16, right: 16),
height: GlobalDataCache().sheetHeight,
child: Column(
children: [
InkWell(
onTap: () => Get.back(),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: const BorderRadius.all(Radius.circular(3)),
),
),
),
),
),
_buildHeader(context),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
if (modelResult!.resultType != 0 &&
modelResult!.summary != '') ...[
SelectableText(
modelResult!.summary!,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
if (modelResult!.summary != '') ...[
_buildSummaryText(modelResult!.summary!),
const SizedBox(height: 20),
],
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: modelResult!.outline!.length,
itemBuilder: (context, index) {
final outline = modelResult!.outline![index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
outline.title!,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
const SizedBox(height: 6),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: outline.partOutline!.length,
itemBuilder: (context, i) {
final part = outline.partOutline![i];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
try {
final controller =
Get.find<VideoDetailController>(
tag: Get.arguments['heroTag'],
);
controller.plPlayerController.seekTo(
Duration(
seconds: Utils.duration(
Utils.tampToSeektime(
part.timestamp!),
).toInt(),
),
);
} catch (_) {}
},
child: SelectableText.rich(
TextSpan(
style: TextStyle(
fontSize: 13,
color: Theme.of(context)
.colorScheme
.onSurface,
height: 1.5,
),
children: [
TextSpan(
text: Utils.tampToSeektime(
part.timestamp!),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
const TextSpan(text: ' '),
TextSpan(text: part.content!),
],
),
),
),
const SizedBox(height: 20),
],
);
},
),
],
);
},
)
_buildOutlineList(context),
],
),
),
@ -148,77 +40,113 @@ class AiDetail extends StatelessWidget {
);
}
InlineSpan buildContent(BuildContext context, content) {
List descV2 = content.descV2;
// type
// 1 普通文本
// 2 @用户
List<TextSpan> spanChilds = List.generate(descV2.length, (index) {
final currentDesc = descV2[index];
switch (currentDesc.type) {
case 1:
List<InlineSpan> spanChildren = [];
RegExp urlRegExp = RegExp(r'https?://\S+\b');
Iterable<Match> matches = urlRegExp.allMatches(currentDesc.rawText);
Widget _buildHeader(BuildContext context) {
return Center(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).hintColor,
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
height: 4,
width: 40,
margin: const EdgeInsets.symmetric(vertical: 16),
),
);
}
int previousEndIndex = 0;
for (Match match in matches) {
if (match.start > previousEndIndex) {
spanChildren.add(TextSpan(
text: currentDesc.rawText
.substring(previousEndIndex, match.start)));
}
spanChildren.add(
TextSpan(
text: match.group(0),
style: TextStyle(
color: Theme.of(context).colorScheme.primary), // 设置颜色为蓝色
recognizer: TapGestureRecognizer()
..onTap = () {
// 处理点击事件
try {
Get.toNamed(
'/webview',
parameters: {
'url': match.group(0)!,
'type': 'url',
'pageTitle': match.group(0)!,
},
);
} catch (err) {
SmartDialog.showToast(err.toString());
}
},
),
);
previousEndIndex = match.end;
}
Widget _buildSummaryText(String summary) {
return SelectableText(
summary,
textAlign: TextAlign.justify,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.6,
),
);
}
if (previousEndIndex < currentDesc.rawText.length) {
spanChildren.add(TextSpan(
text: currentDesc.rawText.substring(previousEndIndex)));
}
Widget _buildOutlineList(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: modelResult!.outline!.length,
itemBuilder: (context, index) {
final outline = modelResult!.outline![index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildOutlineTitle(outline.title!),
const SizedBox(height: 20),
_buildPartOutlineList(context, outline.partOutline!),
],
);
},
);
}
TextSpan result = TextSpan(children: spanChildren);
return result;
case 2:
final colorSchemePrimary = Theme.of(context).colorScheme.primary;
final heroTag = Utils.makeHeroTag(currentDesc.bizId);
return TextSpan(
text: '@${currentDesc.rawText}',
style: TextStyle(color: colorSchemePrimary),
Widget _buildOutlineTitle(String title) {
return SelectableText(
title,
textAlign: TextAlign.justify,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.5,
),
);
}
Widget _buildPartOutlineList(
BuildContext context, List<PartOutline> partOutline) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: partOutline.length,
itemBuilder: (context, i) {
final part = partOutline[i];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildPartText(context, part),
const SizedBox(height: 20),
],
);
},
);
}
void _onPartTap(BuildContext context, int timestamp) {
try {
final controller = Get.find<VideoDetailController>(
tag: Get.arguments['heroTag'],
);
controller.plPlayerController.seekTo(
Duration(seconds: timestamp),
);
} catch (_) {}
}
Widget _buildPartText(BuildContext context, PartOutline part) {
return SelectableText.rich(
TextSpan(
style: TextStyle(
fontSize: 15,
color: Theme.of(context).colorScheme.onSurface,
),
children: [
TextSpan(
text: Utils.tampToSeektime(part.timestamp!),
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed(
'/member?mid=${currentDesc.bizId}',
arguments: {'face': '', 'heroTag': heroTag},
);
},
);
default:
return const TextSpan();
}
});
return TextSpan(children: spanChilds);
..onTap = () => _onPartTap(context, part.timestamp!),
),
const TextSpan(text: ' '),
TextSpan(text: part.content!),
],
),
);
}
}