mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-11 20:47:52 +08:00
Compare commits
15 Commits
release4
...
release5-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bae395c5d0 | ||
|
|
e26ec2ea42 | ||
|
|
f25eb7be82 | ||
|
|
c0879ee169 | ||
|
|
36aa12025e | ||
|
|
0ebe4e9a80 | ||
|
|
a3d345e3ee | ||
|
|
2e45fafb0b | ||
|
|
6c62cebdba | ||
|
|
d3bd218718 | ||
|
|
0ff53ddbde | ||
|
|
bcefaa123e | ||
|
|
e2da6a2936 | ||
|
|
44bd5afb70 | ||
|
|
d8d6cf4d26 |
@@ -8,9 +8,9 @@
|
||||
<h1>PiliPalaX</h1>
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
</div>
|
||||
<p>使用Flutter开发的BiliBili第三方客户端</p>
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ class DynamicSliverAppBar extends StatefulWidget {
|
||||
final CustomClipper<Path>? appBarClipper;
|
||||
|
||||
@override
|
||||
_DynamicSliverAppBarState createState() => _DynamicSliverAppBarState();
|
||||
State<DynamicSliverAppBar> createState() => _DynamicSliverAppBarState();
|
||||
}
|
||||
|
||||
class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
|
||||
27
lib/common/widgets/icon_button.dart
Normal file
27
lib/common/widgets/icon_button.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget iconButton({
|
||||
required BuildContext context,
|
||||
String? tooltip,
|
||||
required IconData icon,
|
||||
required VoidCallback? onPressed,
|
||||
double size = 36,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: IconButton(
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
icon: Icon(
|
||||
icon,
|
||||
size: size / 2,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.all(0),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -67,8 +67,7 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
child: Builder(
|
||||
builder: (context) => CachedNetworkImage(
|
||||
imageUrl:
|
||||
'${src?.startsWith('//') == true ? 'https:$src' : src}${thumbnail ? '@${quality ?? defaultImgQuality}q.webp' : ''}'
|
||||
.http2https,
|
||||
'${src?.startsWith('//') == true ? 'https:$src' : src?.http2https}${thumbnail ? '@${quality ?? defaultImgQuality}q.webp' : ''}',
|
||||
width: width,
|
||||
height: ignoreHeight == null || ignoreHeight == false
|
||||
? height
|
||||
@@ -107,6 +106,7 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget placeholder(BuildContext context) {
|
||||
int cacheWidth = width.cacheSize(context);
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
@@ -128,7 +128,7 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
: 'assets/images/loading.png',
|
||||
width: width,
|
||||
height: height,
|
||||
cacheWidth: width.cacheSize(context),
|
||||
cacheWidth: cacheWidth == 0 ? null : cacheWidth,
|
||||
// cacheHeight: height.cacheSize(context),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -33,7 +33,7 @@ class SearchHttp {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': '请求错误 🙅',
|
||||
'msg': '请求错误',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:hive/hive.dart';
|
||||
import 'package:PiliPalaX/common/widgets/custom_toast.dart';
|
||||
import 'package:PiliPalaX/http/init.dart';
|
||||
import 'package:PiliPalaX/models/common/color_type.dart';
|
||||
import 'package:PiliPalaX/pages/search/index.dart';
|
||||
import 'package:PiliPalaX/pages/video/detail/index.dart';
|
||||
import 'package:PiliPalaX/router/app_pages.dart';
|
||||
import 'package:PiliPalaX/pages/main/view.dart';
|
||||
@@ -190,7 +189,6 @@ class MyApp extends StatelessWidget {
|
||||
},
|
||||
navigatorObservers: [
|
||||
VideoDetailPage.routeObserver,
|
||||
SearchPage.routeObserver,
|
||||
MainApp.routeObserver,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -399,6 +399,7 @@ class _BangumiInfoState extends State<BangumiInfo>
|
||||
: widget.bangumiDetail!.episodes!.first.cid),
|
||||
changeFuc: bangumiIntroController.changeSeasonOrbangu,
|
||||
showEpisodes: widget.showEpisodes,
|
||||
newEp: bangumiItem?.newEp,
|
||||
)
|
||||
],
|
||||
],
|
||||
|
||||
@@ -15,6 +15,7 @@ class BangumiPanel extends StatefulWidget {
|
||||
required this.changeFuc,
|
||||
required this.showEpisodes,
|
||||
required this.heroTag,
|
||||
this.newEp,
|
||||
});
|
||||
|
||||
final List<EpisodeItem> pages;
|
||||
@@ -22,6 +23,7 @@ class BangumiPanel extends StatefulWidget {
|
||||
final Function changeFuc;
|
||||
final Function showEpisodes;
|
||||
final String heroTag;
|
||||
final dynamic newEp;
|
||||
|
||||
@override
|
||||
State<BangumiPanel> createState() => _BangumiPanelState();
|
||||
@@ -126,7 +128,9 @@ class _BangumiPanelState extends State<BangumiPanel> {
|
||||
cid,
|
||||
),
|
||||
child: Text(
|
||||
'全${widget.pages.length}话',
|
||||
widget.newEp?['desc']?.contains('连载') == true
|
||||
? '连载中,更新至第${widget.newEp?['title']}话'
|
||||
: '全${widget.newEp?['title']}话',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:PiliPalaX/models/common/reply_type.dart';
|
||||
import 'package:PiliPalaX/pages/common/common_controller.dart';
|
||||
import 'package:PiliPalaX/pages/video/detail/reply_new/reply_page.dart';
|
||||
import 'package:PiliPalaX/utils/extension.dart';
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -120,6 +121,7 @@ abstract class ReplyController extends CommonController {
|
||||
dynamic oid,
|
||||
dynamic replyItem,
|
||||
int index = 0,
|
||||
ReplyType? replyType,
|
||||
}) {
|
||||
dynamic key = oid ?? replyItem.oid + replyItem.id;
|
||||
Navigator.of(context)
|
||||
@@ -130,7 +132,9 @@ abstract class ReplyController extends CommonController {
|
||||
oid: oid ?? replyItem.oid.toInt(),
|
||||
root: oid != null ? 0 : replyItem.id.toInt(),
|
||||
parent: oid != null ? 0 : replyItem.id.toInt(),
|
||||
replyType: ReplyType.video,
|
||||
replyType: replyItem != null
|
||||
? ReplyType.values[replyItem.type.toInt()]
|
||||
: replyType,
|
||||
replyItem: replyItem,
|
||||
savedReply: savedReplies[key],
|
||||
onSaveReply: (reply) {
|
||||
@@ -155,20 +159,18 @@ abstract class ReplyController extends CommonController {
|
||||
),
|
||||
)
|
||||
.then(
|
||||
(value) {
|
||||
// TODO: data cast
|
||||
if (value != null && value['data'] != null) {
|
||||
(res) {
|
||||
if (res != null) {
|
||||
savedReplies[key] = null;
|
||||
if (value['data'] is ReplyInfo) {
|
||||
MainListReply response =
|
||||
(loadingState.value as Success?)?.response ?? MainListReply();
|
||||
if (oid != null) {
|
||||
response.replies.insert(0, value['data']);
|
||||
} else {
|
||||
response.replies[index].replies.add(value['data']);
|
||||
}
|
||||
loadingState.value = LoadingState.success(response);
|
||||
ReplyInfo replyInfo = Utils.replyCast(res);
|
||||
MainListReply response =
|
||||
(loadingState.value as Success?)?.response ?? MainListReply();
|
||||
if (oid != null) {
|
||||
response.replies.insert(hasUpTop ? 1 : 0, replyInfo);
|
||||
} else {
|
||||
response.replies[index].replies.add(replyInfo);
|
||||
}
|
||||
loadingState.value = LoadingState.success(response);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:math';
|
||||
import 'package:PiliPalaX/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPalaX/pages/video/detail/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -88,8 +89,9 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
||||
if (opusId != null) {
|
||||
isOpusId = true;
|
||||
_dynamicDetailController = Get.put(
|
||||
DynamicDetailController(oid, replyType),
|
||||
tag: opusId.toString());
|
||||
DynamicDetailController(oid, replyType),
|
||||
tag: Utils.makeHeroTag(opusId),
|
||||
);
|
||||
await _dynamicDetailController.reqHtmlByOpusId(opusId!);
|
||||
setState(() {});
|
||||
}
|
||||
@@ -99,8 +101,10 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
||||
} catch (_) {}
|
||||
}
|
||||
if (!isOpusId) {
|
||||
_dynamicDetailController =
|
||||
Get.put(DynamicDetailController(oid, replyType), tag: oid.toString());
|
||||
_dynamicDetailController = Get.put(
|
||||
DynamicDetailController(oid, replyType),
|
||||
tag: Utils.makeHeroTag(oid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +301,11 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
||||
feedBack();
|
||||
dynamic oid = _dynamicDetailController.oid ??
|
||||
IdUtils.bv2av(Get.parameters['bvid']!);
|
||||
_dynamicDetailController.onReply(context, oid: oid);
|
||||
_dynamicDetailController.onReply(
|
||||
context,
|
||||
oid: oid,
|
||||
replyType: ReplyType.values[replyType],
|
||||
);
|
||||
},
|
||||
tooltip: '评论动态',
|
||||
child: const Icon(Icons.reply),
|
||||
|
||||
@@ -23,9 +23,7 @@ class DynamicsTabController extends CommonController {
|
||||
@override
|
||||
Future onRefresh() async {
|
||||
if (dynamicsType == 'all') {
|
||||
if (mainController.navigationBars[1]['count'] != 0) {
|
||||
mainController.clearUnread();
|
||||
}
|
||||
mainController.setCount();
|
||||
}
|
||||
offset = '';
|
||||
await queryData();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:PiliPalaX/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -25,7 +26,7 @@ class _FansPageState extends State<FansPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
mid = Get.parameters['mid']!;
|
||||
_fansController = Get.put(FansController(), tag: mid);
|
||||
_fansController = Get.put(FansController(), tag: Utils.makeHeroTag(mid));
|
||||
_fansController.scrollController.addListener(
|
||||
() async {
|
||||
if (_fansController.scrollController.position.pixels >=
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:PiliPalaX/pages/fav_search/view.dart' show SearchType;
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'controller.dart';
|
||||
@@ -20,7 +21,8 @@ class _FollowPageState extends State<FollowPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
mid = Get.parameters['mid']!;
|
||||
_followController = Get.put(FollowController(), tag: mid);
|
||||
_followController =
|
||||
Get.put(FollowController(), tag: Utils.makeHeroTag(mid));
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -25,7 +25,7 @@ class HistoryController extends GetxController {
|
||||
Future queryHistoryList({type = 'init'}) async {
|
||||
int max = 0;
|
||||
int viewAt = 0;
|
||||
if (type == 'onload') {
|
||||
if (type == 'onload' && historyList.isNotEmpty) {
|
||||
max = historyList.last.history!.oid!;
|
||||
viewAt = historyList.last.viewAt!;
|
||||
}
|
||||
|
||||
@@ -307,6 +307,7 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
|
||||
_htmlRenderCtr.onReply(
|
||||
context,
|
||||
oid: _htmlRenderCtr.oid.value,
|
||||
replyType: ReplyType.values[type],
|
||||
);
|
||||
},
|
||||
tooltip: '评论动态',
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPalaX/grpc/grpc_repo.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:PiliPalaX/pages/dynamics/view.dart';
|
||||
import 'package:PiliPalaX/pages/home/view.dart';
|
||||
import 'package:PiliPalaX/pages/media/view.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:PiliPalaX/pages/dynamics/index.dart';
|
||||
import 'package:PiliPalaX/pages/home/view.dart';
|
||||
import 'package:PiliPalaX/pages/media/index.dart';
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
import '../../models/common/dynamic_badge_mode.dart';
|
||||
import '../../models/common/nav_bar_config.dart';
|
||||
|
||||
class MainController extends GetxController {
|
||||
List<Widget> pages = <Widget>[
|
||||
const HomePage(),
|
||||
// const RankPage(),
|
||||
const DynamicsPage(),
|
||||
const MediaPage(),
|
||||
];
|
||||
RxList navigationBars = defaultNavigationBars.obs;
|
||||
List<Widget> pages = <Widget>[];
|
||||
RxList navigationBars = [].obs;
|
||||
|
||||
final StreamController<bool> bottomBarStream =
|
||||
StreamController<bool>.broadcast();
|
||||
Box setting = GStorage.setting;
|
||||
@@ -33,85 +27,53 @@ class MainController extends GetxController {
|
||||
late bool checkDynamic;
|
||||
late int dynamicPeriod;
|
||||
int? _lastCheckAt;
|
||||
int? dynIndex;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
checkDynamic = GStorage.checkDynamic;
|
||||
dynamicPeriod = GStorage.dynamicPeriod;
|
||||
// if (setting.get(SettingBoxKey.autoUpdate, defaultValue: false)) {
|
||||
// Utils.checkUpdate();
|
||||
// }
|
||||
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
|
||||
int defaultHomePage =
|
||||
setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0) as int;
|
||||
selectedIndex = defaultNavigationBars
|
||||
.indexWhere((item) => item['id'] == defaultHomePage);
|
||||
dynamic userInfo = userInfoCache.get('userInfoCache');
|
||||
userLogin.value = userInfo != null;
|
||||
dynamicBadgeType = DynamicBadgeMode.values[setting.get(
|
||||
SettingBoxKey.dynamicBadgeMode,
|
||||
defaultValue: DynamicBadgeMode.number.code)];
|
||||
if (dynamicBadgeType != DynamicBadgeMode.hidden) {
|
||||
if (checkDynamic) {
|
||||
_lastCheckAt = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
getUnreadDynamic();
|
||||
}
|
||||
}
|
||||
|
||||
void onBackPressed(BuildContext context) {
|
||||
// if (_lastPressedAt == null ||
|
||||
// DateTime.now().difference(_lastPressedAt!) >
|
||||
// const Duration(seconds: 2)) {
|
||||
// 两次点击时间间隔超过2秒,重新记录时间戳
|
||||
// _lastPressedAt = DateTime.now();
|
||||
if (selectedIndex != 0) {
|
||||
pageController.jumpTo(0);
|
||||
} else {
|
||||
if (Platform.isAndroid) {
|
||||
const MethodChannel("onUserLeaveHint").invokeMethod('back');
|
||||
} else {
|
||||
SystemNavigator.pop(); // 退出应用
|
||||
setNavBarConfig();
|
||||
if (dynamicBadgeType != DynamicBadgeMode.hidden) {
|
||||
dynIndex = navigationBars.indexWhere((e) => e['id'] == 1);
|
||||
if (dynIndex != -1) {
|
||||
if (checkDynamic) {
|
||||
_lastCheckAt = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
getUnreadDynamic();
|
||||
}
|
||||
}
|
||||
// SmartDialog.showToast("再按一次退出PiliPalaX");
|
||||
// return; // 不退出应用
|
||||
// }
|
||||
// SystemNavigator.pop(); // 退出应用
|
||||
}
|
||||
|
||||
void getUnreadDynamic() async {
|
||||
if (!userLogin.value) {
|
||||
if (!userLogin.value || dynIndex == -1) {
|
||||
return;
|
||||
}
|
||||
// not needed yet
|
||||
// int dynamicItemIndex =
|
||||
// navigationBars.indexWhere((item) => item['label'] == "动态");
|
||||
// if (dynamicItemIndex == -1) return;
|
||||
// var res = await CommonHttp.unReadDynamic();
|
||||
// var data = res['data'];
|
||||
// navigationBars[1]['count'] =
|
||||
// data == null ? 0 : data.length; // 修改 count 属性为新的值
|
||||
await GrpcRepo.dynRed().then((res) {
|
||||
if (res['status']) {
|
||||
navigationBars[1]['count'] = res['data'];
|
||||
setCount(res['data']);
|
||||
}
|
||||
});
|
||||
navigationBars.refresh();
|
||||
}
|
||||
|
||||
void clearUnread() async {
|
||||
// not needed yet
|
||||
// int dynamicItemIndex =
|
||||
// navigationBars.indexWhere((item) => item['label'] == "动态");
|
||||
// if (dynamicItemIndex == -1) return;
|
||||
navigationBars[1]['count'] = 0; // 修改 count 属性为新的值
|
||||
void setCount([int count = 0]) async {
|
||||
dynIndex ??= navigationBars.indexWhere((e) => e['id'] == 1);
|
||||
if (dynIndex == -1 || navigationBars[dynIndex!]['count'] == count) return;
|
||||
navigationBars[dynIndex!]['count'] = count; // 修改 count 属性为新的值
|
||||
navigationBars.refresh();
|
||||
}
|
||||
|
||||
void checkUnreadDynamic() {
|
||||
if (!userLogin.value ||
|
||||
if (dynIndex == -1 ||
|
||||
!userLogin.value ||
|
||||
dynamicBadgeType == DynamicBadgeMode.hidden ||
|
||||
!checkDynamic) return;
|
||||
int now = DateTime.now().millisecondsSinceEpoch;
|
||||
@@ -120,4 +82,28 @@ class MainController extends GetxController {
|
||||
getUnreadDynamic();
|
||||
}
|
||||
}
|
||||
|
||||
void setNavBarConfig() async {
|
||||
List defaultNavTabs = [...defaultNavigationBars];
|
||||
List navBarSort =
|
||||
setting.get(SettingBoxKey.navBarSort, defaultValue: [0, 1, 2]);
|
||||
defaultNavTabs.retainWhere((item) => navBarSort.contains(item['id']));
|
||||
defaultNavTabs.sort((a, b) =>
|
||||
navBarSort.indexOf(a['id']).compareTo(navBarSort.indexOf(b['id'])));
|
||||
navigationBars.value = defaultNavTabs;
|
||||
int defaultHomePage =
|
||||
setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0) as int;
|
||||
int defaultIndex =
|
||||
navigationBars.indexWhere((item) => item['id'] == defaultHomePage);
|
||||
// 如果找不到匹配项,默认索引设置为0或其他合适的值
|
||||
selectedIndex = defaultIndex != -1 ? defaultIndex : 0;
|
||||
pages = navigationBars
|
||||
.map<Widget>((e) => switch (e['id']) {
|
||||
0 => HomePage(),
|
||||
1 => DynamicsPage(),
|
||||
2 => MediaPage(),
|
||||
_ => throw UnimplementedError(),
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPalaX/grpc/grpc_client.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:PiliPalaX/models/common/dynamic_badge_mode.dart';
|
||||
@@ -108,7 +110,7 @@ class _MainAppState extends State<MainApp>
|
||||
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
_dynamicController.flag = true;
|
||||
_mainController.clearUnread();
|
||||
_mainController.setCount();
|
||||
} else {
|
||||
_dynamicController.flag = false;
|
||||
}
|
||||
@@ -129,12 +131,15 @@ class _MainAppState extends State<MainApp>
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (bool didPop, Object? result) async {
|
||||
_mainController.onBackPressed(context);
|
||||
if (_dynamicController.flag) {
|
||||
_dynamicController.flag = false;
|
||||
}
|
||||
if (!_homeController.flag) {
|
||||
_homeController.flag = true;
|
||||
if (_mainController.selectedIndex != 0) {
|
||||
setIndex(0);
|
||||
_mainController.bottomBarStream.add(true);
|
||||
} else {
|
||||
if (Platform.isAndroid) {
|
||||
const MethodChannel("onUserLeaveHint").invokeMethod('back');
|
||||
} else {
|
||||
SystemNavigator.pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: LayoutBuilder(
|
||||
@@ -152,58 +157,71 @@ class _MainAppState extends State<MainApp>
|
||||
36.801 +
|
||||
MediaQuery.of(context).padding.left,
|
||||
child: Obx(
|
||||
() => NavigationRail(
|
||||
groupAlignment: 1,
|
||||
minWidth: context.width * 0.0286 + 28.56,
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedIndex: _mainController.selectedIndex,
|
||||
onDestinationSelected: (value) => setIndex(value),
|
||||
labelType: NavigationRailLabelType.none,
|
||||
leading: UserAndSearchVertical(ctr: _homeController),
|
||||
destinations: _mainController.navigationBars
|
||||
.map((e) => NavigationRailDestination(
|
||||
icon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['icon'],
|
||||
),
|
||||
selectedIcon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['selectIcon'],
|
||||
),
|
||||
label: Text(e['label']),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 0.01 * context.height),
|
||||
))
|
||||
.toList(),
|
||||
trailing: SizedBox(height: 0.1 * context.height),
|
||||
),
|
||||
() => _mainController.navigationBars.length > 1
|
||||
? NavigationRail(
|
||||
groupAlignment: 1,
|
||||
minWidth: context.width * 0.0286 + 28.56,
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedIndex: _mainController.selectedIndex,
|
||||
onDestinationSelected: setIndex,
|
||||
labelType: NavigationRailLabelType.none,
|
||||
leading:
|
||||
UserAndSearchVertical(ctr: _homeController),
|
||||
destinations: _mainController.navigationBars
|
||||
.map((e) => NavigationRailDestination(
|
||||
icon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['icon'],
|
||||
),
|
||||
selectedIcon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['selectIcon'],
|
||||
),
|
||||
label: Text(e['label']),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 0.01 * context.height),
|
||||
))
|
||||
.toList(),
|
||||
trailing: SizedBox(height: 0.1 * context.height),
|
||||
)
|
||||
: Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.paddingOf(context).top + 10),
|
||||
constraints: BoxConstraints(
|
||||
minWidth: context.width * 0.0286 + 28.56,
|
||||
),
|
||||
child:
|
||||
UserAndSearchVertical(ctr: _homeController),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if (!isPortait)
|
||||
Obx(
|
||||
() => NavigationRail(
|
||||
onDestinationSelected: (value) => setIndex(value),
|
||||
selectedIndex: _mainController.selectedIndex,
|
||||
destinations: _mainController.navigationBars
|
||||
.map(
|
||||
(e) => NavigationRailDestination(
|
||||
icon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['icon'],
|
||||
),
|
||||
selectedIcon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['selectIcon'],
|
||||
),
|
||||
label: Text(e['label']),
|
||||
),
|
||||
() => _mainController.navigationBars.length > 1
|
||||
? NavigationRail(
|
||||
onDestinationSelected: setIndex,
|
||||
selectedIndex: _mainController.selectedIndex,
|
||||
destinations: _mainController.navigationBars
|
||||
.map(
|
||||
(e) => NavigationRailDestination(
|
||||
icon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['icon'],
|
||||
),
|
||||
selectedIcon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['selectIcon'],
|
||||
),
|
||||
label: Text(e['label']),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
@@ -240,60 +258,65 @@ class _MainAppState extends State<MainApp>
|
||||
offset: Offset(0, snapshot.data ? 0 : 1),
|
||||
child: enableMYBar
|
||||
? Obx(
|
||||
() => NavigationBar(
|
||||
onDestinationSelected: (value) =>
|
||||
setIndex(value),
|
||||
selectedIndex: _mainController.selectedIndex,
|
||||
destinations:
|
||||
_mainController.navigationBars.map(
|
||||
(e) {
|
||||
return NavigationDestination(
|
||||
icon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['icon'],
|
||||
),
|
||||
selectedIcon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['selectIcon'],
|
||||
),
|
||||
label: e['label'],
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
() => _mainController.navigationBars.length > 1
|
||||
? NavigationBar(
|
||||
onDestinationSelected: setIndex,
|
||||
selectedIndex:
|
||||
_mainController.selectedIndex,
|
||||
destinations:
|
||||
_mainController.navigationBars.map(
|
||||
(e) {
|
||||
return NavigationDestination(
|
||||
icon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['icon'],
|
||||
),
|
||||
selectedIcon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['selectIcon'],
|
||||
),
|
||||
label: e['label'],
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
)
|
||||
: Obx(
|
||||
() => BottomNavigationBar(
|
||||
currentIndex: _mainController.selectedIndex,
|
||||
onTap: (value) => setIndex(value),
|
||||
iconSize: 16,
|
||||
selectedFontSize: 12,
|
||||
unselectedFontSize: 12,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
// selectedItemColor:
|
||||
// Theme.of(context).colorScheme.primary, // 选中项的颜色
|
||||
// unselectedItemColor:
|
||||
// Theme.of(context).colorScheme.onSurface,
|
||||
items: _mainController.navigationBars
|
||||
.map(
|
||||
(e) => BottomNavigationBarItem(
|
||||
icon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['icon'],
|
||||
),
|
||||
activeIcon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['selectIcon'],
|
||||
),
|
||||
label: e['label'],
|
||||
),
|
||||
() => _mainController.navigationBars.length > 1
|
||||
? BottomNavigationBar(
|
||||
currentIndex:
|
||||
_mainController.selectedIndex,
|
||||
onTap: setIndex,
|
||||
iconSize: 16,
|
||||
selectedFontSize: 12,
|
||||
unselectedFontSize: 12,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
// selectedItemColor:
|
||||
// Theme.of(context).colorScheme.primary, // 选中项的颜色
|
||||
// unselectedItemColor:
|
||||
// Theme.of(context).colorScheme.onSurface,
|
||||
items: _mainController.navigationBars
|
||||
.map(
|
||||
(e) => BottomNavigationBarItem(
|
||||
icon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['icon'],
|
||||
),
|
||||
activeIcon: _buildIcon(
|
||||
id: e['id'],
|
||||
count: e['count'],
|
||||
icon: e['selectIcon'],
|
||||
),
|
||||
label: e['label'],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -214,6 +214,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
||||
'https://account.bilibili.com/official/mobile/home'),
|
||||
),
|
||||
_divider,
|
||||
SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -30,52 +30,59 @@ class _RankPageState extends State<RankPage>
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 18,
|
||||
child: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(
|
||||
parent: BouncingScrollPhysics(),
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
itemCount: _rankController.tabs.length,
|
||||
itemBuilder: (context, index) => IntrinsicHeight(
|
||||
child: Obx(
|
||||
() => InkWell(
|
||||
onTap: () {
|
||||
_rankController.initialIndex.value = index;
|
||||
_rankController.tabController.animateTo(index);
|
||||
},
|
||||
child: Container(
|
||||
color: index == _rankController.initialIndex.value
|
||||
? Theme.of(context).colorScheme.onInverseSurface
|
||||
: Colors.transparent,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: double.infinity,
|
||||
width: 3,
|
||||
color: index == _rankController.initialIndex.value
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.transparent,
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Text(
|
||||
_rankController.tabs[index]['label'],
|
||||
style: TextStyle(
|
||||
color: index ==
|
||||
_rankController.initialIndex.value
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 15,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Column(
|
||||
children: List.generate(
|
||||
_rankController.tabs.length,
|
||||
(index) => Obx(
|
||||
() => IntrinsicHeight(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_rankController.initialIndex.value = index;
|
||||
_rankController.tabController.animateTo(index);
|
||||
},
|
||||
child: ColoredBox(
|
||||
color: index == _rankController.initialIndex.value
|
||||
? Theme.of(context).colorScheme.onInverseSurface
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: double.infinity,
|
||||
width: 3,
|
||||
color: index == _rankController.initialIndex.value
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Text(
|
||||
_rankController.tabs[index]['label'],
|
||||
style: TextStyle(
|
||||
color: index ==
|
||||
_rankController.initialIndex.value
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
fontSize: 15,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPalaX/models/home/rcmd/result.dart';
|
||||
import 'package:PiliPalaX/pages/common/popup_controller.dart';
|
||||
import 'package:PiliPalaX/http/video.dart';
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
@@ -35,9 +34,11 @@ class RcmdController extends PopupController {
|
||||
|
||||
@override
|
||||
List? handleListResponse(List currentList, List dataList) {
|
||||
return currentPage == 0 && enableSaveLastData && currentList.length < 500
|
||||
? dataList +
|
||||
(currentList.isEmpty ? <RecVideoItemAppModel>[] : currentList)
|
||||
return currentPage == 0 &&
|
||||
enableSaveLastData &&
|
||||
currentList.isNotEmpty &&
|
||||
currentList.length < 500
|
||||
? dataList + currentList
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:PiliPalaX/http/search.dart';
|
||||
import 'package:PiliPalaX/models/search/hot.dart';
|
||||
import 'package:PiliPalaX/models/search/suggest.dart';
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
|
||||
class SSearchController extends GetxController {
|
||||
final FocusNode searchFocusNode = FocusNode();
|
||||
RxString searchKeyWord = ''.obs;
|
||||
Rx<TextEditingController> controller = TextEditingController().obs;
|
||||
RxList<HotSearchItem> hotSearchList = <HotSearchItem>[].obs;
|
||||
Box historyWord = GStorage.historyWord;
|
||||
List<String> historyCacheList = [];
|
||||
final searchFocusNode = FocusNode();
|
||||
final controller = TextEditingController();
|
||||
|
||||
RxList<String> historyList = <String>[].obs;
|
||||
|
||||
RxList<SearchSuggestItem> searchSuggestList = <SearchSuggestItem>[].obs;
|
||||
final _debouncer =
|
||||
Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间
|
||||
|
||||
String hintText = '搜索';
|
||||
RxString defaultSearch = ''.obs;
|
||||
Box setting = GStorage.setting;
|
||||
bool enableHotKey = true;
|
||||
|
||||
late bool enableHotKey;
|
||||
|
||||
Rx<LoadingState> loadingState = LoadingState.loading().obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@@ -33,28 +29,31 @@ class SSearchController extends GetxController {
|
||||
}
|
||||
if (Get.parameters['hintText'] != null) {
|
||||
hintText = Get.parameters['hintText']!;
|
||||
searchKeyWord.value = hintText;
|
||||
}
|
||||
}
|
||||
historyCacheList = List<String>.from(historyWord.get('cacheList') ?? []);
|
||||
historyList.value = historyCacheList;
|
||||
enableHotKey = setting.get(SettingBoxKey.enableHotKey, defaultValue: true);
|
||||
|
||||
historyList.value = List.from(GStorage.historyWord.get('cacheList') ?? []);
|
||||
|
||||
enableHotKey =
|
||||
GStorage.setting.get(SettingBoxKey.enableHotKey, defaultValue: true);
|
||||
|
||||
if (enableHotKey) {
|
||||
queryHotSearchList();
|
||||
}
|
||||
}
|
||||
|
||||
void onChange(value) {
|
||||
searchKeyWord.value = value;
|
||||
if (value == '') {
|
||||
searchSuggestList.value = [];
|
||||
return;
|
||||
void onChange(String value) {
|
||||
if (value.isEmpty) {
|
||||
searchSuggestList.clear();
|
||||
} else {
|
||||
querySearchSuggest(value);
|
||||
}
|
||||
_debouncer.call(() => querySearchSuggest(value));
|
||||
}
|
||||
|
||||
void onClear() {
|
||||
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
|
||||
controller.value.clear();
|
||||
searchKeyWord.value = '';
|
||||
searchSuggestList.value = [];
|
||||
if (controller.value.text != '') {
|
||||
controller.clear();
|
||||
searchSuggestList.clear();
|
||||
searchFocusNode.requestFocus();
|
||||
} else {
|
||||
Get.back();
|
||||
@@ -62,44 +61,38 @@ class SSearchController extends GetxController {
|
||||
}
|
||||
|
||||
// 搜索
|
||||
void submit() {
|
||||
// ignore: unrelated_type_equality_checks
|
||||
if (searchKeyWord == '') {
|
||||
if (hintText == '') {
|
||||
void submit() async {
|
||||
if (controller.text.isEmpty) {
|
||||
if (hintText.isEmpty) {
|
||||
return;
|
||||
}
|
||||
searchKeyWord.value = hintText;
|
||||
controller.text = hintText;
|
||||
}
|
||||
List<String> arr =
|
||||
historyCacheList.where((e) => e != searchKeyWord.value).toList();
|
||||
arr.insert(0, searchKeyWord.value);
|
||||
historyCacheList = arr;
|
||||
|
||||
historyList.value = historyCacheList;
|
||||
// 手动刷新
|
||||
historyList.refresh();
|
||||
historyWord.put('cacheList', historyCacheList);
|
||||
historyList.remove(controller.text);
|
||||
historyList.insert(0, controller.text);
|
||||
GStorage.historyWord.put('cacheList', historyList);
|
||||
searchFocusNode.unfocus();
|
||||
Get.toNamed('/searchResult', parameters: {'keyword': searchKeyWord.value});
|
||||
|
||||
await Get.toNamed('/searchResult', parameters: {
|
||||
'keyword': controller.text,
|
||||
});
|
||||
searchFocusNode.requestFocus();
|
||||
}
|
||||
|
||||
// 获取热搜关键词
|
||||
Future queryHotSearchList() async {
|
||||
var result = await SearchHttp.hotSearchList();
|
||||
dynamic result = await SearchHttp.hotSearchList();
|
||||
if (result['status']) {
|
||||
hotSearchList.value = result['data'].list;
|
||||
loadingState.value = LoadingState.success(result['data'].list);
|
||||
} else {
|
||||
loadingState.value = LoadingState.error(result['msg']);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 点击热搜关键词
|
||||
void onClickKeyword(String keyword) {
|
||||
searchKeyWord.value = keyword;
|
||||
controller.value.text = keyword;
|
||||
// 移动光标
|
||||
controller.value.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: controller.value.text.length),
|
||||
);
|
||||
controller.text = keyword;
|
||||
searchSuggestList.clear();
|
||||
submit();
|
||||
}
|
||||
|
||||
@@ -112,28 +105,20 @@ class SSearchController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
onSelect(word) {
|
||||
searchKeyWord.value = word;
|
||||
controller.value.text = word;
|
||||
submit();
|
||||
}
|
||||
|
||||
onLongSelect(word) {
|
||||
int index = historyList.indexOf(word);
|
||||
historyList.removeAt(index);
|
||||
historyWord.put('cacheList', historyList);
|
||||
historyList.remove(word);
|
||||
GStorage.historyWord.put('cacheList', historyList);
|
||||
}
|
||||
|
||||
onClearHis() {
|
||||
historyList.value = [];
|
||||
historyCacheList = [];
|
||||
historyList.refresh();
|
||||
historyWord.put('cacheList', []);
|
||||
onClearHistory() {
|
||||
historyList.clear();
|
||||
GStorage.historyWord.put('cacheList', []);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
searchFocusNode.dispose();
|
||||
controller.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:PiliPalaX/common/widgets/loading_widget.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:PiliPalaX/common/widgets/http_error.dart';
|
||||
import 'controller.dart';
|
||||
import 'widgets/hot_keyword.dart';
|
||||
import 'widgets/search_text.dart';
|
||||
@@ -10,39 +11,10 @@ class SearchPage extends StatefulWidget {
|
||||
|
||||
@override
|
||||
State<SearchPage> createState() => _SearchPageState();
|
||||
static final RouteObserver<PageRoute> routeObserver =
|
||||
RouteObserver<PageRoute>();
|
||||
}
|
||||
|
||||
class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
final SSearchController _searchController = Get.put(SSearchController());
|
||||
late Future? _futureBuilderFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureBuilderFuture = _searchController.queryHotSearchList();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
SearchPage.routeObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
// 返回当前页面时
|
||||
void didPopNext() async {
|
||||
_searchController.searchFocusNode.requestFocus();
|
||||
super.didPopNext();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
SearchPage.routeObserver
|
||||
.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -63,38 +35,33 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
),
|
||||
const SizedBox(width: 10)
|
||||
],
|
||||
title: Obx(
|
||||
() => TextField(
|
||||
autofocus: true,
|
||||
focusNode: _searchController.searchFocusNode,
|
||||
controller: _searchController.controller.value,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: (value) => _searchController.onChange(value),
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
hintText: _searchController.hintText,
|
||||
border: InputBorder.none,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: '清空',
|
||||
icon: const Icon(Icons.clear, size: 22),
|
||||
onPressed: () => _searchController.onClear(),
|
||||
),
|
||||
title: TextField(
|
||||
autofocus: true,
|
||||
focusNode: _searchController.searchFocusNode,
|
||||
controller: _searchController.controller,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: _searchController.onChange,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
hintText: _searchController.hintText,
|
||||
border: InputBorder.none,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: '清空',
|
||||
icon: const Icon(Icons.clear, size: 22),
|
||||
onPressed: _searchController.onClear,
|
||||
),
|
||||
onSubmitted: (String value) => _searchController.submit(),
|
||||
),
|
||||
onSubmitted: (value) => _searchController.submit(),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
// 搜索建议
|
||||
_searchSuggest(),
|
||||
// 热搜
|
||||
Visibility(
|
||||
visible: _searchController.enableHotKey,
|
||||
child: hotSearch(_searchController),
|
||||
),
|
||||
if (_searchController.enableHotKey)
|
||||
// 热搜
|
||||
hotSearch(),
|
||||
// 搜索历史
|
||||
_history()
|
||||
],
|
||||
@@ -107,32 +74,37 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
return Obx(
|
||||
() => _searchController.searchSuggestList.isNotEmpty &&
|
||||
_searchController.searchSuggestList.first.term != null &&
|
||||
_searchController.controller.value.text != ''
|
||||
? ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: _searchController.searchSuggestList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
onTap: () => _searchController.onClickKeyword(
|
||||
_searchController.searchSuggestList[index].term!),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 20, top: 9, bottom: 9),
|
||||
child: _searchController.searchSuggestList[index].textRich,
|
||||
),
|
||||
);
|
||||
},
|
||||
_searchController.controller.text != ''
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _searchController.searchSuggestList
|
||||
.map(
|
||||
(item) => InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
onTap: () => _searchController.onClickKeyword(item.term!),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 20,
|
||||
top: 9,
|
||||
bottom: 9,
|
||||
),
|
||||
child: item.textRich,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
: const SizedBox(),
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget hotSearch(ctr) {
|
||||
Widget hotSearch() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 14, 4, 0),
|
||||
padding: const EdgeInsets.fromLTRB(10, 25, 4, 25),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -152,10 +124,12 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
height: 34,
|
||||
child: TextButton.icon(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(const EdgeInsets.only(
|
||||
left: 10, top: 6, bottom: 6, right: 10)),
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.only(
|
||||
left: 10, top: 6, bottom: 6, right: 10),
|
||||
),
|
||||
),
|
||||
onPressed: () => ctr.queryHotSearchList(),
|
||||
onPressed: _searchController.queryHotSearchList,
|
||||
icon: const Icon(Icons.refresh_outlined, size: 18),
|
||||
label: const Text('刷新'),
|
||||
),
|
||||
@@ -163,60 +137,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
],
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
final double width = boxConstraints.maxWidth;
|
||||
return FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => HotKeyword(
|
||||
width: width,
|
||||
// ignore: invalid_use_of_protected_member
|
||||
hotSearchList: _searchController.hotSearchList.value,
|
||||
onClick: (keyword) async {
|
||||
_searchController.searchFocusNode.unfocus();
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 150));
|
||||
_searchController.onClickKeyword(keyword);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
HttpError(
|
||||
errMsg: data['msg'],
|
||||
callback: () => setState(() {
|
||||
_futureBuilderFuture =
|
||||
_searchController.queryHotSearchList();
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 缓存数据
|
||||
if (_searchController.hotSearchList.isNotEmpty) {
|
||||
return HotKeyword(
|
||||
width: width,
|
||||
hotSearchList: _searchController.hotSearchList,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Obx(() => _buildHotKey(_searchController.loadingState.value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -227,13 +148,17 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
() => Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
10, 25, 6, MediaQuery.of(context).padding.bottom + 50),
|
||||
10,
|
||||
_searchController.enableHotKey ? 0 : 6,
|
||||
6,
|
||||
MediaQuery.of(context).padding.bottom + 50,
|
||||
),
|
||||
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, 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -244,9 +169,23 @@ 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: WidgetStateProperty.all(
|
||||
const EdgeInsets.only(
|
||||
left: 10,
|
||||
top: 6,
|
||||
bottom: 6,
|
||||
right: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: _searchController.onClearHistory,
|
||||
icon: const Icon(Icons.clear_all_outlined, size: 18),
|
||||
label: const Text('清空'),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -257,16 +196,15 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
runSpacing: 8,
|
||||
direction: Axis.horizontal,
|
||||
textDirection: TextDirection.ltr,
|
||||
children: [
|
||||
for (int i = 0; i < _searchController.historyList.length; i++)
|
||||
SearchText(
|
||||
searchText: _searchController.historyList[i],
|
||||
searchTextIdx: i,
|
||||
onSelect: (value) => _searchController.onSelect(value),
|
||||
onLongSelect: (value) =>
|
||||
_searchController.onLongSelect(value),
|
||||
children: _searchController.historyList
|
||||
.map(
|
||||
(item) => SearchText(
|
||||
searchText: item,
|
||||
onSelect: _searchController.onClickKeyword,
|
||||
onLongSelect: _searchController.onLongSelect,
|
||||
),
|
||||
)
|
||||
],
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -274,4 +212,23 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHotKey(LoadingState loadingState) {
|
||||
return switch (loadingState) {
|
||||
Success() => (loadingState.response as List?)?.isNotEmpty == true
|
||||
? LayoutBuilder(
|
||||
builder: (_, constraints) => HotKeyword(
|
||||
width: constraints.maxWidth,
|
||||
hotSearchList: loadingState.response,
|
||||
onClick: _searchController.onClickKeyword,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Error() => errorWidget(
|
||||
errMsg: loadingState.errMsg,
|
||||
callback: _searchController.queryHotSearchList,
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||
class SearchText extends StatelessWidget {
|
||||
final String? searchText;
|
||||
final Function? onSelect;
|
||||
final int? searchTextIdx;
|
||||
final Function? onLongSelect;
|
||||
final double? fontSize;
|
||||
final Color? bgColor;
|
||||
@@ -13,7 +12,6 @@ class SearchText extends StatelessWidget {
|
||||
super.key,
|
||||
this.searchText,
|
||||
this.onSelect,
|
||||
this.searchTextIdx,
|
||||
this.onLongSelect,
|
||||
this.fontSize,
|
||||
this.bgColor,
|
||||
|
||||
@@ -32,19 +32,13 @@ Widget searchBangumiPanel(BuildContext context, ctr, loadingState) {
|
||||
var i = loadingState.response[index];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
/// TODO 番剧详情页面
|
||||
// Get.toNamed('/video?bvid=${i.bvid}&cid=${i.cid}', arguments: {
|
||||
// 'videoItem': i,
|
||||
// 'heroTag': Utils.makeHeroTag(i.id),
|
||||
// 'videoType': SearchType.media_bangumi
|
||||
// });
|
||||
Utils.viewBangumi(seasonId: i.seasonId);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace,
|
||||
StyleString.safeSpace,
|
||||
StyleString.safeSpace,
|
||||
2),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
vertical: StyleString.cardSpace,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -127,57 +121,6 @@ Widget searchBangumiPanel(BuildContext context, ctr, loadingState) {
|
||||
Text(i.indexShow, style: style),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
Utils.viewBangumi(seasonId: i.seasonId);
|
||||
// SmartDialog.showLoading(msg: '获取中...');
|
||||
// var res = await SearchHttp.bangumiInfo(
|
||||
// seasonId: i.seasonId);
|
||||
// SmartDialog.dismiss().then((value) {
|
||||
// if (res['status']) {
|
||||
// EpisodeItem episode =
|
||||
// res['data'].episodes.first;
|
||||
// int? epId = res['data']
|
||||
// .userStatus
|
||||
// ?.progress
|
||||
// ?.lastEpId;
|
||||
// if (epId == null) {
|
||||
// epId = episode.epId;
|
||||
// } else {
|
||||
// for (var item
|
||||
// in res['data'].episodes) {
|
||||
// if (item.epId == epId) {
|
||||
// episode = item;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// String bvid = episode.bvid!;
|
||||
// int cid = episode.cid!;
|
||||
// String pic = episode.cover!;
|
||||
// String heroTag =
|
||||
// Utils.makeHeroTag(cid);
|
||||
// Get.toNamed(
|
||||
// '/video?bvid=$bvid&cid=$cid&seasonId=${i.seasonId}&epId=$epId',
|
||||
// arguments: {
|
||||
// 'pic': pic,
|
||||
// 'heroTag': heroTag,
|
||||
// 'videoType':
|
||||
// SearchType.media_bangumi,
|
||||
// 'bangumiItem': res['data'],
|
||||
// },
|
||||
// );
|
||||
// } else {
|
||||
// SmartDialog.showToast(res['msg']);
|
||||
// }
|
||||
// });
|
||||
},
|
||||
child: const Text('观看'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
100
lib/pages/setting/navigation_bar_set.dart
Normal file
100
lib/pages/setting/navigation_bar_set.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'package:PiliPalaX/utils/storage.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
|
||||
import '../../../models/common/nav_bar_config.dart';
|
||||
|
||||
class NavigationBarSetPage extends StatefulWidget {
|
||||
const NavigationBarSetPage({super.key});
|
||||
|
||||
@override
|
||||
State<NavigationBarSetPage> createState() => _NavigationbarSetPageState();
|
||||
}
|
||||
|
||||
class _NavigationbarSetPageState extends State<NavigationBarSetPage> {
|
||||
late List defaultNavTabs;
|
||||
late List<int> navBarSort;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
defaultNavTabs = defaultNavigationBars;
|
||||
navBarSort =
|
||||
GStorage.setting.get(SettingBoxKey.navBarSort, defaultValue: [0, 1, 2]);
|
||||
// 对 tabData 进行排序
|
||||
defaultNavTabs.sort((a, b) {
|
||||
int indexA = navBarSort.indexOf(a['id']);
|
||||
int indexB = navBarSort.indexOf(b['id']);
|
||||
|
||||
// 如果类型在 sortOrder 中不存在,则放在末尾
|
||||
if (indexA == -1) indexA = navBarSort.length;
|
||||
if (indexB == -1) indexB = navBarSort.length;
|
||||
|
||||
return indexA.compareTo(indexB);
|
||||
});
|
||||
}
|
||||
|
||||
void saveEdit() {
|
||||
List<int> sortedTabbar = defaultNavTabs
|
||||
.where((i) => navBarSort.contains(i['id']))
|
||||
.map<int>((i) => i['id'])
|
||||
.toList();
|
||||
if (sortedTabbar.isEmpty) {
|
||||
sortedTabbar = [0, 1, 2];
|
||||
}
|
||||
GStorage.setting.put(SettingBoxKey.navBarSort, sortedTabbar);
|
||||
SmartDialog.showToast('保存成功,下次启动时生效');
|
||||
}
|
||||
|
||||
void onReorder(int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final tabsItem = defaultNavTabs.removeAt(oldIndex);
|
||||
defaultNavTabs.insert(newIndex, tabsItem);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Navbar编辑'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: saveEdit,
|
||||
child: const Text('保存'),
|
||||
),
|
||||
const SizedBox(width: 12)
|
||||
],
|
||||
),
|
||||
body: ReorderableListView(
|
||||
onReorder: onReorder,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
footer: SizedBox(
|
||||
height: MediaQuery.of(context).padding.bottom + 30,
|
||||
),
|
||||
children: defaultNavTabs
|
||||
.map(
|
||||
(item) => CheckboxListTile(
|
||||
key: Key(item['label']),
|
||||
value: navBarSort.contains(item['id']),
|
||||
onChanged: (bool? newValue) {
|
||||
int tabTypeId = item['id'];
|
||||
if (!newValue!) {
|
||||
navBarSort.remove(tabTypeId);
|
||||
} else {
|
||||
navBarSort.add(tabTypeId);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
title: Text(item['label']),
|
||||
secondary: const Icon(Icons.drag_indicator_rounded),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -359,13 +359,19 @@ class _StyleSettingState extends State<StyleSetting> {
|
||||
subtitle: Text('删除或调换首页标签页', style: subTitleStyle),
|
||||
leading: const Icon(Icons.toc_outlined),
|
||||
),
|
||||
ListTile(
|
||||
dense: false,
|
||||
onTap: () => Get.toNamed('/navbarSetting'),
|
||||
title: Text('Navbar编辑', style: titleStyle),
|
||||
leading: const Icon(Icons.toc_outlined),
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
ListTile(
|
||||
dense: false,
|
||||
onTap: () => Get.toNamed('/displayModeSetting'),
|
||||
title: Text('屏幕帧率', style: titleStyle),
|
||||
leading: const Icon(Icons.autofps_select_outlined),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:PiliPalaX/common/constants.dart';
|
||||
import 'package:PiliPalaX/common/widgets/icon_button.dart';
|
||||
import 'package:PiliPalaX/common/widgets/pair.dart';
|
||||
import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart';
|
||||
import 'package:PiliPalaX/http/danmaku.dart';
|
||||
@@ -7,6 +10,7 @@ import 'package:PiliPalaX/http/init.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
@@ -115,6 +119,19 @@ class SegmentModel {
|
||||
bool hasSkipped;
|
||||
}
|
||||
|
||||
class PostSegmentModel {
|
||||
PostSegmentModel({
|
||||
required this.segment,
|
||||
required this.category,
|
||||
required this.actionType,
|
||||
});
|
||||
Pair<int, int> segment;
|
||||
SegmentType category;
|
||||
ActionType actionType;
|
||||
}
|
||||
|
||||
enum ActionType { skip, mute, full, poi }
|
||||
|
||||
class VideoDetailController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
/// 路由传参
|
||||
@@ -159,6 +176,8 @@ class VideoDetailController extends GetxController
|
||||
RxInt oid = 0.obs;
|
||||
|
||||
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final childKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
RxString bgCover = ''.obs;
|
||||
PlPlayerController plPlayerController = PlPlayerController.getInstance()
|
||||
..setCurrBrightness(-1.0);
|
||||
@@ -183,7 +202,7 @@ class VideoDetailController extends GetxController
|
||||
late String cacheSecondDecode;
|
||||
late int cacheAudioQa;
|
||||
|
||||
late final bool _enableSponsorBlock;
|
||||
late final bool enableSponsorBlock;
|
||||
PlayerStatus? playerStatus;
|
||||
StreamSubscription<Duration>? positionSubscription;
|
||||
|
||||
@@ -244,9 +263,9 @@ class VideoDetailController extends GetxController
|
||||
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
||||
defaultValue: AudioQuality.hiRes.code);
|
||||
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
|
||||
_enableSponsorBlock =
|
||||
enableSponsorBlock =
|
||||
setting.get(SettingBoxKey.enableSponsorBlock, defaultValue: false);
|
||||
if (_enableSponsorBlock) {
|
||||
if (enableSponsorBlock) {
|
||||
_blockLimit = GStorage.blockLimit;
|
||||
_blockSettings = GStorage.blockSettings;
|
||||
_blockColor = GStorage.blockColor;
|
||||
@@ -263,14 +282,17 @@ class VideoDetailController extends GetxController
|
||||
_blockColor?[segment.index] ?? segment.color;
|
||||
|
||||
Future _vote(String uuid, int type) async {
|
||||
Request().post(
|
||||
Request()
|
||||
.post(
|
||||
'${GStorage.blockServer}/api/voteOnSponsorTime',
|
||||
queryParameters: {
|
||||
'UUID': uuid,
|
||||
'userID': GStorage.blockUserID,
|
||||
'type': type,
|
||||
},
|
||||
).then((res) {
|
||||
options: _options,
|
||||
)
|
||||
.then((res) {
|
||||
SmartDialog.showToast(res.statusCode == 200 ? '投票成功' : '投票失败');
|
||||
});
|
||||
}
|
||||
@@ -289,14 +311,17 @@ class VideoDetailController extends GetxController
|
||||
dense: true,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
Request().post(
|
||||
Request()
|
||||
.post(
|
||||
'${GStorage.blockServer}/api/voteOnSponsorTime',
|
||||
queryParameters: {
|
||||
'UUID': segment.UUID,
|
||||
'userID': GStorage.blockUserID,
|
||||
'category': item.name,
|
||||
},
|
||||
).then((res) {
|
||||
options: _options,
|
||||
)
|
||||
.then((res) {
|
||||
SmartDialog.showToast(
|
||||
'类别更改${res.statusCode == 200 ? '成功' : '失败'}');
|
||||
});
|
||||
@@ -387,7 +412,7 @@ class VideoDetailController extends GetxController
|
||||
);
|
||||
}
|
||||
|
||||
void showSponsorBlock(BuildContext context) {
|
||||
void showSBDetail(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
@@ -502,14 +527,7 @@ class VideoDetailController extends GetxController
|
||||
);
|
||||
}
|
||||
|
||||
Future _sponsorBlock() async {
|
||||
dynamic result = await Request().get(
|
||||
'${GStorage.blockServer}/api/skipSegments',
|
||||
data: {
|
||||
'videoID': bvid,
|
||||
// 'cid': cid.value,
|
||||
},
|
||||
options: Options(
|
||||
Options get _options => Options(
|
||||
headers: {
|
||||
'env': '',
|
||||
'app-key': '',
|
||||
@@ -519,7 +537,16 @@ class VideoDetailController extends GetxController
|
||||
HttpHeaders.cookieHeader:
|
||||
'buvid3= ; SESSDATA= ; bili_jct= ; DedeUserID= ; DedeUserID__ckMd5= ; sid= ',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Future _sponsorBlock() async {
|
||||
dynamic result = await Request().get(
|
||||
'${GStorage.blockServer}/api/skipSegments',
|
||||
data: {
|
||||
'videoID': bvid,
|
||||
'cid': cid.value,
|
||||
},
|
||||
options: _options,
|
||||
);
|
||||
if (result.data is List && result.data.isNotEmpty) {
|
||||
try {
|
||||
@@ -608,6 +635,7 @@ class VideoDetailController extends GetxController
|
||||
Request().post(
|
||||
'${GStorage.blockServer}/api/viewedVideoSponsorTime',
|
||||
queryParameters: {'UUID': item.UUID},
|
||||
options: _options,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -832,7 +860,7 @@ class VideoDetailController extends GetxController
|
||||
var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
|
||||
if (result['status']) {
|
||||
data = result['data'];
|
||||
if (_enableSponsorBlock) {
|
||||
if (enableSponsorBlock) {
|
||||
await _sponsorBlock();
|
||||
}
|
||||
if (data.acceptDesc!.isNotEmpty && data.acceptDesc!.contains('试看')) {
|
||||
@@ -974,4 +1002,371 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<PostSegmentModel>? list;
|
||||
|
||||
void onBlock(BuildContext context) {
|
||||
PersistentBottomSheetController? ctr;
|
||||
list ??= <PostSegmentModel>[];
|
||||
if (list!.isEmpty) {
|
||||
list!.add(
|
||||
PostSegmentModel(
|
||||
segment: Pair(first: 0, second: 0),
|
||||
category: SegmentType.sponsor,
|
||||
actionType: ActionType.skip,
|
||||
),
|
||||
);
|
||||
}
|
||||
ctr = plPlayerController.isFullScreen.value
|
||||
? scaffoldKey.currentState?.showBottomSheet(
|
||||
enableDrag: false,
|
||||
(context) => _postPanel(ctr?.close),
|
||||
)
|
||||
: childKey.currentState?.showBottomSheet(
|
||||
enableDrag: false,
|
||||
(context) => _postPanel(ctr?.close),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _postPanel(onClose) => StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
List<Widget> segmentWidget({
|
||||
required int index,
|
||||
required bool isFirst,
|
||||
}) {
|
||||
String value = Utils.timeFormat(isFirst
|
||||
? list![index].segment.first
|
||||
: list![index].segment.second);
|
||||
return [
|
||||
Text(
|
||||
'${isFirst ? '开始' : '结束'}: $value',
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '使用当前位置',
|
||||
icon: Icons.my_location,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (isFirst) {
|
||||
list![index].segment.first =
|
||||
plPlayerController.positionSeconds.value;
|
||||
} else {
|
||||
list![index].segment.second =
|
||||
plPlayerController.positionSeconds.value;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
tooltip: '编辑',
|
||||
icon: Icons.edit,
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
String initV = value;
|
||||
return AlertDialog(
|
||||
content: TextFormField(
|
||||
initialValue: value,
|
||||
autofocus: true,
|
||||
onChanged: (value) {
|
||||
initV = value;
|
||||
},
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'[\d:]+'),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: initV),
|
||||
child: Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
).then((res) {
|
||||
if (res != null) {
|
||||
try {
|
||||
List<int> split = (res as String)
|
||||
.split(':')
|
||||
.toList()
|
||||
.reversed
|
||||
.toList()
|
||||
.map((e) => int.parse(e))
|
||||
.toList();
|
||||
int duration = 0;
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
duration += split[i] * pow(60, i).toInt();
|
||||
}
|
||||
if (duration <= (data.timeLength ?? 0) / 1000) {
|
||||
setState(() {
|
||||
if (isFirst) {
|
||||
list![index].segment.first = duration;
|
||||
} else {
|
||||
list![index].segment.second = duration;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 16,
|
||||
title: const Text('提交片段'),
|
||||
actions: [
|
||||
iconButton(
|
||||
context: context,
|
||||
tooltip: '添加片段',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
list?.insert(
|
||||
0,
|
||||
PostSegmentModel(
|
||||
segment: Pair(first: 0, second: 0),
|
||||
category: SegmentType.sponsor,
|
||||
actionType: ActionType.skip,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
icon: Icons.add,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
iconButton(
|
||||
context: context,
|
||||
tooltip: '关闭',
|
||||
onPressed: onClose,
|
||||
icon: Icons.close,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
body: list?.isNotEmpty == true
|
||||
? Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
...List.generate(
|
||||
list!.length,
|
||||
(index) => Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 5,
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
...segmentWidget(
|
||||
isFirst: true,
|
||||
index: index,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
...segmentWidget(
|
||||
isFirst: false,
|
||||
index: index,
|
||||
),
|
||||
const Spacer(),
|
||||
iconButton(
|
||||
context: context,
|
||||
size: 26,
|
||||
icon: Icons.clear,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
list!.removeAt(index);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Text('分类: '),
|
||||
PopupMenuButton(
|
||||
initialValue: list![index].category,
|
||||
onSelected: (item) async {
|
||||
setState(() {
|
||||
list![index].category = item;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context) => SegmentType
|
||||
.values
|
||||
.map((item) =>
|
||||
PopupMenuItem<SegmentType>(
|
||||
value: item,
|
||||
child: Text(item.title),
|
||||
))
|
||||
.toList(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
list![index].category.title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
size: 20,
|
||||
Icons.keyboard_arrow_right,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Text('ActionType: '),
|
||||
PopupMenuButton(
|
||||
initialValue: list![index].actionType,
|
||||
onSelected: (item) async {
|
||||
setState(() {
|
||||
list![index].actionType = item;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context) => ActionType
|
||||
.values
|
||||
.map((item) =>
|
||||
PopupMenuItem<ActionType>(
|
||||
value: item,
|
||||
child: Text(item.name),
|
||||
))
|
||||
.toList(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
list![index].actionType.name,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
size: 20,
|
||||
Icons.keyboard_arrow_right,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 88 + MediaQuery.paddingOf(context).bottom,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 16,
|
||||
bottom: 16 + MediaQuery.paddingOf(context).bottom,
|
||||
child: FloatingActionButton(
|
||||
tooltip: '提交',
|
||||
onPressed: () {
|
||||
Request()
|
||||
.post(
|
||||
'${GStorage.blockServer}/api/skipSegments',
|
||||
queryParameters: {
|
||||
'videoID': bvid,
|
||||
'cid': cid.value,
|
||||
'userID': GStorage.blockUserID,
|
||||
'userAgent': Constants.userAgent,
|
||||
'videoDuration': (data.timeLength ?? 0 / 1000),
|
||||
},
|
||||
data: {
|
||||
'segments': list!
|
||||
.map(
|
||||
(item) => {
|
||||
'segment': [
|
||||
item.segment.first,
|
||||
item.segment.second,
|
||||
],
|
||||
'category': item.category.name,
|
||||
'actionType': item.actionType.name,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
},
|
||||
options: _options,
|
||||
)
|
||||
.then(
|
||||
(res) {
|
||||
if (res.statusCode == 200) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('提交成功');
|
||||
list?.clear();
|
||||
} else {
|
||||
SmartDialog.showToast(
|
||||
'提交失败: ${{
|
||||
400: '参数错误',
|
||||
403: '被自动审核机制拒绝',
|
||||
429: '重复提交太快',
|
||||
409: '重复提交'
|
||||
}[res.statusCode]}',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Icon(Icons.check),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -187,7 +187,11 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
feedBack();
|
||||
dynamic oid = _videoReplyController.aid ??
|
||||
IdUtils.bv2av(Get.parameters['bvid']!);
|
||||
_videoReplyController.onReply(context, oid: oid);
|
||||
_videoReplyController.onReply(
|
||||
context,
|
||||
oid: oid,
|
||||
replyType: ReplyType.video,
|
||||
);
|
||||
},
|
||||
tooltip: '发表评论',
|
||||
child: const Icon(Icons.reply),
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:get/get.dart';
|
||||
import 'package:PiliPalaX/http/video.dart';
|
||||
import 'package:PiliPalaX/models/common/reply_type.dart';
|
||||
import 'package:PiliPalaX/models/video/reply/emote.dart';
|
||||
import 'package:PiliPalaX/models/video/reply/item.dart';
|
||||
import 'package:PiliPalaX/pages/emote/index.dart';
|
||||
import 'package:PiliPalaX/utils/feed_back.dart';
|
||||
import 'package:PiliPalaX/pages/emote/view.dart';
|
||||
@@ -517,9 +516,7 @@ class _ReplyPageState extends State<ReplyPage>
|
||||
);
|
||||
if (result['status']) {
|
||||
SmartDialog.showToast(result['data']['success_toast']);
|
||||
Get.back(result: {
|
||||
'data': ReplyItemModel.fromJson(result['data']['reply'], ''),
|
||||
});
|
||||
Get.back(result: result['data']['reply']);
|
||||
} else {
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:PiliPalaX/grpc/app/main/community/reply/v1/reply.pb.dart';
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPalaX/pages/video/detail/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPalaX/pages/video/detail/reply_new/reply_page.dart';
|
||||
import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:PiliPalaX/common/skeleton/video_reply.dart';
|
||||
@@ -158,7 +159,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
replyType: widget.replyType,
|
||||
needDivider: false,
|
||||
onReply: () {
|
||||
_onReply(firstFloor!);
|
||||
_onReply(firstFloor!, -1);
|
||||
},
|
||||
upMid: _videoReplyReplyController.upMid,
|
||||
);
|
||||
@@ -237,7 +238,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
),
|
||||
);
|
||||
|
||||
void _onReply(ReplyInfo? item) {
|
||||
void _onReply(ReplyInfo? item, int index) {
|
||||
dynamic oid = item?.oid.toInt();
|
||||
dynamic root = item?.id.toInt();
|
||||
dynamic parent = item?.id.toInt();
|
||||
@@ -250,7 +251,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
oid: oid,
|
||||
root: root,
|
||||
parent: parent,
|
||||
replyType: ReplyType.video,
|
||||
replyType: widget.replyType,
|
||||
replyItem: item,
|
||||
savedReply: _savedReplies[key],
|
||||
onSaveReply: (reply) {
|
||||
@@ -274,20 +275,16 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
},
|
||||
),
|
||||
)
|
||||
.then((value) {
|
||||
// 完成评论,数据添加 // TODO: data cast
|
||||
if (value != null && value['data'] != null) {
|
||||
.then((res) {
|
||||
if (res != null) {
|
||||
_savedReplies[key] = null;
|
||||
if (value['data'] is ReplyInfo) {
|
||||
List<ReplyInfo> list =
|
||||
_videoReplyReplyController.loadingState.value is Success
|
||||
? (_videoReplyReplyController.loadingState.value as Success)
|
||||
.response
|
||||
: <ReplyInfo>[];
|
||||
list.add(value['data']);
|
||||
_videoReplyReplyController.loadingState.value =
|
||||
LoadingState.success(list);
|
||||
}
|
||||
ReplyInfo replyInfo = Utils.replyCast(res);
|
||||
List list = (_videoReplyReplyController.loadingState.value as Success?)
|
||||
?.response ??
|
||||
<ReplyInfo>[];
|
||||
list.insert(index + 1, replyInfo);
|
||||
_videoReplyReplyController.loadingState.value =
|
||||
LoadingState.success(list);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -320,11 +317,11 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
return ColoredBox(
|
||||
color: _videoReplyReplyController.colorAnimation?.value ??
|
||||
Theme.of(Get.context!).colorScheme.onInverseSurface,
|
||||
child: _replyItem(loadingState.response[index]),
|
||||
child: _replyItem(loadingState.response[index], index),
|
||||
);
|
||||
},
|
||||
)
|
||||
: _replyItem(loadingState.response[index]);
|
||||
: _replyItem(loadingState.response[index], index);
|
||||
}
|
||||
} else if (loadingState is Error) {
|
||||
return CustomScrollView(
|
||||
@@ -355,14 +352,14 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _replyItem(replyItem) {
|
||||
Widget _replyItem(replyItem, index) {
|
||||
return ReplyItemGrpc(
|
||||
replyItem: replyItem,
|
||||
replyLevel: widget.isDialogue ? '3' : '2',
|
||||
showReplyRow: false,
|
||||
replyType: widget.replyType,
|
||||
onReply: () {
|
||||
_onReply(replyItem);
|
||||
_onReply(replyItem, index);
|
||||
},
|
||||
onDelete: (rpid, frpid) {
|
||||
List list =
|
||||
|
||||
@@ -85,8 +85,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
// StreamSubscription<Duration>? _bufferedListener;
|
||||
bool get isFullScreen => plPlayerController?.isFullScreen.value ?? false;
|
||||
|
||||
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -520,7 +518,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
),
|
||||
Expanded(
|
||||
child: Scaffold(
|
||||
key: scaffoldKey,
|
||||
key: videoDetailController.childKey,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Column(
|
||||
children: [
|
||||
@@ -578,7 +576,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
),
|
||||
Expanded(
|
||||
child: Scaffold(
|
||||
key: scaffoldKey,
|
||||
key: videoDetailController.childKey,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Column(
|
||||
children: [
|
||||
@@ -630,7 +628,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
),
|
||||
Expanded(
|
||||
child: Scaffold(
|
||||
key: scaffoldKey,
|
||||
key: videoDetailController.childKey,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Column(
|
||||
children: [
|
||||
@@ -685,7 +683,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
Expanded(
|
||||
child: Expanded(
|
||||
child: Scaffold(
|
||||
key: scaffoldKey,
|
||||
key: videoDetailController.childKey,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Column(
|
||||
children: [
|
||||
@@ -785,7 +783,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
height: context.height -
|
||||
(removeSafeArea ? 0 : MediaQuery.of(context).padding.top),
|
||||
child: Scaffold(
|
||||
key: scaffoldKey,
|
||||
key: videoDetailController.childKey,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Column(
|
||||
children: [
|
||||
@@ -1289,7 +1287,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
|
||||
// 展示二级回复
|
||||
void replyReply(replyItem, id) {
|
||||
scaffoldKey.currentState?.showBottomSheet(
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
(context) => VideoReplyReplyPanel(
|
||||
id: id,
|
||||
// rcount: replyItem.rcount,
|
||||
@@ -1304,14 +1302,14 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
|
||||
// ai总结
|
||||
showAiBottomSheet() {
|
||||
scaffoldKey.currentState?.showBottomSheet(
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
enableDrag: true,
|
||||
(context) => AiDetail(modelResult: videoIntroController.modelResult),
|
||||
);
|
||||
}
|
||||
|
||||
showIntroDetail(videoDetail, videoTags) {
|
||||
scaffoldKey.currentState?.showBottomSheet(
|
||||
videoDetailController.childKey.currentState?.showBottomSheet(
|
||||
enableDrag: true,
|
||||
(context) => videoDetail is BangumiInfoModel
|
||||
? bangumi.IntroDetail(
|
||||
@@ -1339,7 +1337,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
context: context,
|
||||
scaffoldState: isFullScreen
|
||||
? videoDetailController.scaffoldKey.currentState
|
||||
: scaffoldKey.currentState,
|
||||
: videoDetailController.childKey.currentState,
|
||||
).buildShowBottomSheet();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
RxString now = ''.obs;
|
||||
Timer? clock;
|
||||
late String defaultCDNService;
|
||||
|
||||
bool get isFullScreen => widget.controller!.isFullScreen.value;
|
||||
|
||||
@override
|
||||
@@ -1450,20 +1449,37 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
// ),
|
||||
// fuc: () => _.screenshot(),
|
||||
// ),
|
||||
if (widget.videoDetailCtr?.enableSponsorBlock == true)
|
||||
SizedBox(
|
||||
width: 42,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
tooltip: '提交片段',
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () => widget.videoDetailCtr?.onBlock(context),
|
||||
icon: const Icon(
|
||||
Icons.block,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => widget.videoDetailCtr?.segmentList.isNotEmpty == true
|
||||
? SizedBox(
|
||||
width: 42,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
tooltip: 'SponsorBlock',
|
||||
tooltip: '片段信息',
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () =>
|
||||
widget.videoDetailCtr?.showSponsorBlock(context),
|
||||
icon: const Icon(
|
||||
Icons.block,
|
||||
widget.videoDetailCtr?.showSBDetail(context),
|
||||
icon: Icon(
|
||||
MdiIcons.advertisements,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
|
||||
@@ -1241,7 +1241,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
Obx(() {
|
||||
if (plPlayerController.buffered.value ==
|
||||
Duration.zero) {
|
||||
return const Text('Buffering...',
|
||||
return const Text('加载中...',
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 12));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import 'package:PiliPalaX/pages/member/new/member_page.dart';
|
||||
import 'package:PiliPalaX/pages/member/new/widget/edit_profile_page.dart';
|
||||
import 'package:PiliPalaX/pages/setting/navigation_bar_set.dart';
|
||||
import 'package:PiliPalaX/pages/setting/sponsor_block_page.dart';
|
||||
import 'package:PiliPalaX/pages/video/detail/introduction/widgets/create_fav_page.dart';
|
||||
import 'package:PiliPalaX/pages/webview/webview_page.dart';
|
||||
@@ -186,6 +187,9 @@ class Routes {
|
||||
CustomGetPage(name: '/sponsorBlock', page: () => const SponsorBlockPage()),
|
||||
CustomGetPage(name: '/createFav', page: () => const CreateFavPage()),
|
||||
CustomGetPage(name: '/editProfile', page: () => const EditProfilePage()),
|
||||
// navigation bar
|
||||
CustomGetPage(
|
||||
name: '/navbarSetting', page: () => const NavigationBarSetPage()),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class GStorage {
|
||||
static late final Box<dynamic> video;
|
||||
|
||||
static List<Pair<SegmentType, SkipType>> get blockSettings {
|
||||
List<int> list = setting.get(
|
||||
List list = setting.get(
|
||||
SettingBoxKey.blockSettings,
|
||||
defaultValue: List.generate(SegmentType.values.length, (_) => 1),
|
||||
);
|
||||
@@ -37,7 +37,7 @@ class GStorage {
|
||||
}
|
||||
|
||||
static List<Color> get blockColor {
|
||||
List<String> list = setting.get(
|
||||
List list = setting.get(
|
||||
SettingBoxKey.blockColor,
|
||||
defaultValue: List.generate(SegmentType.values.length, (_) => ''),
|
||||
);
|
||||
@@ -255,6 +255,12 @@ class SettingBoxKey {
|
||||
enableAi = 'enableAi',
|
||||
disableLikeMsg = 'disableLikeMsg',
|
||||
defaultHomePage = 'defaultHomePage',
|
||||
previewQuality = 'previewQuality',
|
||||
checkDynamic = 'checkDynamic',
|
||||
dynamicPeriod = 'dynamicPeriod',
|
||||
schemeVariant = 'schemeVariant',
|
||||
|
||||
// Sponsor Block
|
||||
enableSponsorBlock = 'enableSponsorBlock',
|
||||
blockSettings = 'blockSettings',
|
||||
blockLimit = 'blockLimit',
|
||||
@@ -263,10 +269,6 @@ class SettingBoxKey {
|
||||
blockToast = 'blockToast',
|
||||
blockServer = 'blockServer',
|
||||
blockTrack = 'blockTrack',
|
||||
previewQuality = 'previewQuality',
|
||||
checkDynamic = 'checkDynamic',
|
||||
dynamicPeriod = 'dynamicPeriod',
|
||||
schemeVariant = 'schemeVariant',
|
||||
|
||||
// 弹幕相关设置 权重(云屏蔽) 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 字体粗细
|
||||
danmakuWeight = 'danmakuWeight',
|
||||
@@ -304,7 +306,8 @@ class SettingBoxKey {
|
||||
tabbarSort = 'tabbarSort', // 首页tabbar
|
||||
dynamicBadgeMode = 'dynamicBadgeMode',
|
||||
hiddenSettingUnlocked = 'hiddenSettingUnlocked',
|
||||
enableGradientBg = 'enableGradientBg';
|
||||
enableGradientBg = 'enableGradientBg',
|
||||
navBarSort = 'navBarSort';
|
||||
}
|
||||
|
||||
class LocalCacheKey {
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:PiliPalaX/grpc/app/main/community/reply/v1/reply.pb.dart';
|
||||
import 'package:PiliPalaX/http/member.dart';
|
||||
import 'package:PiliPalaX/http/search.dart';
|
||||
import 'package:PiliPalaX/http/video.dart';
|
||||
@@ -25,6 +26,27 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
class Utils {
|
||||
static final Random random = Random();
|
||||
|
||||
static ReplyInfo replyCast(res) {
|
||||
Map? emote = res['content']['emote'];
|
||||
emote?.forEach((key, value) {
|
||||
value['size'] = value['meta']['size'];
|
||||
});
|
||||
return ReplyInfo.create()
|
||||
..mergeFromProto3Json(
|
||||
res
|
||||
..['id'] = res['rpid']
|
||||
..['member']['name'] = res['member']['uname']
|
||||
..['member']['face'] = res['member']['avatar']
|
||||
..['member']['level'] = res['member']['level_info']['current_level']
|
||||
..['member']['vipStatus'] = res['member']['vip']['vipStatus']
|
||||
..['member']['vipType'] = res['member']['vip']['vipType']
|
||||
..['member']['officialVerifyType'] =
|
||||
res['member']['official_verify']['type']
|
||||
..['content']['emote'] = emote,
|
||||
ignoreUnknownFields: true,
|
||||
);
|
||||
}
|
||||
|
||||
static bool isDefault(int attr) {
|
||||
return (attr & 2) == 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user