feat: dyn topic rcmd

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-06-09 19:50:07 +08:00
parent f1e4130201
commit 10efd96788
15 changed files with 260 additions and 142 deletions

View File

@@ -11,7 +11,7 @@ import 'package:PiliPlus/common/widgets/pair.dart';
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/models/common/publish_panel_type.dart';
import 'package:PiliPlus/models/common/reply/reply_option_type.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/topic_item.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/pages/common/common_publish_page.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/controller.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/view.dart';
@@ -621,7 +621,7 @@ class _CreateDynPanelState extends CommonPublishPageState<CreateDynPanel> {
double _offset = 0;
Future<void> _onSelectTopic() async {
TopicPubSearchItem? res = await showModalBottomSheet(
TopicItem? res = await showModalBottomSheet(
context: context,
useSafeArea: true,
isScrollControlled: true,
@@ -643,7 +643,7 @@ class _CreateDynPanelState extends CommonPublishPageState<CreateDynPanel> {
),
);
if (res != null) {
topic.value = Pair(first: res.id!, second: res.name!);
topic.value = Pair(first: res.id, second: res.name);
}
}
}

View File

@@ -1,13 +1,13 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/search.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/data.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/topic_item.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
class SelectTopicController
extends CommonListController<TopicPubSearchData, TopicPubSearchItem> {
extends CommonListController<TopicPubSearchData, TopicItem> {
final focusNode = FocusNode();
final controller = TextEditingController();
@@ -20,7 +20,7 @@ class SelectTopicController
}
@override
List<TopicPubSearchItem>? getDataList(TopicPubSearchData response) {
List<TopicItem>? getDataList(TopicPubSearchData response) {
return response.topicItems;
}

View File

@@ -1,12 +1,11 @@
import 'dart:async';
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_pub_search/topic_item.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/controller.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/widgets/item.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:stream_transform/stream_transform.dart';
@@ -140,70 +139,41 @@ class _SelectTopicPanelState extends State<SelectTopicPanel> {
}
Widget _buildBody(
ThemeData theme, LoadingState<List<TopicPubSearchItem>?> loadingState) {
ThemeData theme, LoadingState<List<TopicItem>?> loadingState) {
return switch (loadingState) {
Loading() => loadingWidget,
Success<List<TopicPubSearchItem>?>(:var response) =>
response?.isNotEmpty == true
? NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is UserScrollNotification) {
if (_controller.focusNode.hasFocus) {
_controller.focusNode.unfocus();
}
} else if (notification is ScrollEndNotification) {
widget.callback?.call(notification.metrics.pixels);
Success<List<TopicItem>?>(:var response) => response?.isNotEmpty == true
? NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is UserScrollNotification) {
if (_controller.focusNode.hasFocus) {
_controller.focusNode.unfocus();
}
return false;
},
child: ListView.builder(
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom +
MediaQuery.viewInsetsOf(context).bottom +
80,
),
controller: widget.scrollController,
itemBuilder: (context, index) {
if (index == response.length - 1) {
_controller.onLoadMore();
}
final item = response[index];
return ListTile(
dense: true,
onTap: () => Get.back(result: item),
title: Text.rich(
TextSpan(
children: [
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: EdgeInsets.only(right: 5),
child: Icon(
CustomIcon.topic_tag,
size: 18,
),
),
),
TextSpan(
text: item.name,
style: const TextStyle(fontSize: 14),
),
],
),
),
subtitle: Padding(
padding: const EdgeInsets.only(left: 23),
child: Text(
'${Utils.numFormat(item.view)}浏览 · ${Utils.numFormat(item.discuss)}讨论',
style: TextStyle(color: theme.colorScheme.outline),
),
),
);
},
itemCount: response!.length,
} else if (notification is ScrollEndNotification) {
widget.callback?.call(notification.metrics.pixels);
}
return false;
},
child: ListView.builder(
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom +
MediaQuery.viewInsetsOf(context).bottom +
80,
),
)
: _errWidget(),
controller: widget.scrollController,
itemBuilder: (context, index) {
if (index == response.length - 1) {
_controller.onLoadMore();
}
return DynTopicItem(
item: response[index],
onTap: (item) => Get.back(result: item),
);
},
itemCount: response!.length,
),
)
: _errWidget(),
Error(:var errMsg) => _errWidget(errMsg),
};
}

View File

@@ -0,0 +1,50 @@
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
class DynTopicItem extends StatelessWidget {
const DynTopicItem({
super.key,
required this.item,
required this.onTap,
});
final TopicItem item;
final ValueChanged<TopicItem> onTap;
@override
Widget build(BuildContext context) {
return ListTile(
dense: true,
onTap: () => onTap(item),
title: Text.rich(
TextSpan(
children: [
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: EdgeInsets.only(right: 5),
child: Icon(
CustomIcon.topic_tag,
size: 18,
),
),
),
TextSpan(
text: item.name,
style: const TextStyle(fontSize: 14),
),
],
),
),
subtitle: Padding(
padding: const EdgeInsets.only(left: 23),
child: Text(
'${Utils.numFormat(item.view)}浏览 · ${Utils.numFormat(item.discuss)}讨论',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
);
}
}

View File

@@ -40,7 +40,7 @@ class DynTopicController
topState.value = await DynamicsHttp.topicTop(topicId: topicId);
if (topState.value.isSuccess) {
var topicItem = topState.value.data!.topicItem!;
topicName = topicItem.name!;
topicName = topicItem.name;
isFav.value = topicItem.isFav;
isLike.value = topicItem.isLike;
}

View File

@@ -153,7 +153,7 @@ class _DynTopicPageState extends State<DynTopicPage> {
pinned: true,
callback: (value) => _controller.appbarOffset =
value - kToolbarHeight - paddingTop - 7,
title: IgnorePointer(child: Text(response!.topicItem!.name!)),
title: IgnorePointer(child: Text(response!.topicItem!.name)),
flexibleSpace: Container(
decoration: BoxDecoration(
image: DecorationImage(
@@ -206,7 +206,7 @@ class _DynTopicPageState extends State<DynTopicPage> {
),
),
Text(
response.topicItem!.name!,
response.topicItem!.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,

View File

@@ -0,0 +1,17 @@
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
class DynTopicRcmdController
extends CommonListController<List<TopicItem>?, TopicItem> {
@override
void onInit() {
super.onInit();
queryData();
}
@override
Future<LoadingState<List<TopicItem>?>> customGetData() =>
DynamicsHttp.dynTopicRcmd();
}

View File

@@ -0,0 +1,70 @@
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart';
import 'package:PiliPlus/pages/dynamics_select_topic/widgets/item.dart';
import 'package:PiliPlus/pages/dynamics_topic_rcmd/controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class DynTopicRcmdPage extends StatefulWidget {
const DynTopicRcmdPage({super.key});
@override
State<DynTopicRcmdPage> createState() => _DynTopicRcmdPageState();
}
class _DynTopicRcmdPageState extends State<DynTopicRcmdPage> {
final DynTopicRcmdController _controller = Get.put(DynTopicRcmdController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('话题')),
body: SafeArea(
top: false,
bottom: false,
child: refreshIndicator(
onRefresh: _controller.onRefresh,
child: CustomScrollView(
slivers: [
SliverPadding(
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: Obx(() => _buildBody(_controller.loadingState.value)),
),
],
),
),
),
);
}
Widget _buildBody(LoadingState<List<TopicItem>?> loadingState) {
return switch (loadingState) {
Loading() => const SliverToBoxAdapter(child: LinearProgressIndicator()),
Success(:var response) => response?.isNotEmpty == true
? SliverList.builder(
itemCount: response!.length,
itemBuilder: (context, index) {
return DynTopicItem(
item: response[index],
onTap: (item) => Get.toNamed(
'/dynTopic',
parameters: {
'id': item.id.toString(),
'name': item.name,
},
),
);
},
)
: HttpError(onReload: _controller.onReload),
Error(:var errMsg) => HttpError(
errMsg: errMsg,
onReload: _controller.onReload,
),
};
}
}

View File

@@ -174,31 +174,48 @@ class _SearchPageState extends State<SearchPage> {
mainAxisSize: MainAxisSize.min,
children: [
text,
Padding(
padding: const EdgeInsets.only(left: 14),
child: SizedBox(
height: 34,
child: TextButton.icon(
onPressed: () => Get.toNamed(
'/searchTrending',
parameters: {'tag': _tag},
),
label: Text(
'完整榜单',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.outline,
),
),
icon: Icon(
size: 16,
Icons.keyboard_arrow_right,
const SizedBox(width: 14),
SizedBox(
height: 34,
child: TextButton.icon(
onPressed: () => Get.toNamed(
'/searchTrending',
parameters: {'tag': _tag},
),
label: Text(
'完整榜单',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.outline,
),
iconAlignment: IconAlignment.end,
),
icon: Icon(
size: 16,
Icons.keyboard_arrow_right,
color: theme.colorScheme.outline,
),
iconAlignment: IconAlignment.end,
),
)
),
SizedBox(
height: 34,
child: TextButton.icon(
onPressed: () => Get.toNamed('/dynTopicRcmd'),
label: Text(
'话题',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.outline,
),
),
icon: Icon(
size: 16,
Icons.keyboard_arrow_right,
color: theme.colorScheme.outline,
),
iconAlignment: IconAlignment.end,
),
),
],
)
: text,