Compare commits
44 Commits
v1.0.18.01
...
feature-ho
| Author | SHA1 | Date | |
|---|---|---|---|
| 41c40dfbc4 | |||
| 5d9ecab1b0 | |||
| 3de009ac43 | |||
| b29256f598 | |||
| e7cf472a0f | |||
| 03c59d23b8 | |||
| b6f805f0e4 | |||
| e23c2469ed | |||
| 387c799de1 | |||
| 230dd81342 | |||
| 47bdfec8c2 | |||
| 6a844da259 | |||
| 18bb58d293 | |||
| 045186b3c8 | |||
| b531599893 | |||
| 1da84508d8 | |||
| 4c44fab217 | |||
| 5c3d438a7e | |||
| 92a8efdee1 | |||
| eb1e2ca5f4 | |||
| 5b1022628c | |||
| 33f61ac0fa | |||
| 0b349e102e | |||
| 81371c5a31 | |||
| 85a59e11b9 | |||
| e24ccc16fa | |||
| 89a43b1285 | |||
| ea8af28828 | |||
| 8a2c023343 | |||
| a86fe76e59 | |||
| d703e38c3f | |||
| 9e93b50860 | |||
| 9907967a0a | |||
| 331969cc8d | |||
| e603942b5f | |||
| 0c4bad406e | |||
| 9663278916 | |||
| 545def36e6 | |||
| aaeecc9e53 | |||
| 16895b5c32 | |||
| a68c04001b | |||
| 1dd70f482f | |||
| 103423abf7 | |||
| 569184a507 |
15
change_log/1.0.19.0131.md
Normal file
15
change_log/1.0.19.0131.md
Normal file
@ -0,0 +1,15 @@
|
||||
## 1.0.19
|
||||
|
||||
|
||||
### 修复
|
||||
+ 视频404、评论加载错误
|
||||
+ bvav转换
|
||||
|
||||
### 优化
|
||||
+ 视频详情页内存占用
|
||||
|
||||
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@ -4,7 +4,7 @@ import 'init.dart';
|
||||
|
||||
class ReplyHttp {
|
||||
static Future replyList({
|
||||
required dynamic oid,
|
||||
required int oid,
|
||||
required int pageNum,
|
||||
required int type,
|
||||
int? ps,
|
||||
@ -76,7 +76,7 @@ class ReplyHttp {
|
||||
// 评论点赞
|
||||
static Future likeReply({
|
||||
required int type,
|
||||
required dynamic oid,
|
||||
required int oid,
|
||||
required int rpid,
|
||||
required int action,
|
||||
}) async {
|
||||
|
||||
@ -224,10 +224,11 @@ class VideoHttp {
|
||||
// 获取投币状态
|
||||
static Future hasCoinVideo({required String bvid}) async {
|
||||
var res = await Request().get(Api.hasCoinVideo, data: {'bvid': bvid});
|
||||
print('res: $res');
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
return {'status': true, 'data': []};
|
||||
return {'status': false, 'data': []};
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,7 +332,7 @@ class VideoHttp {
|
||||
// plat num 发送平台标识 非必要 1:web端 2:安卓客户端 3:ios客户端 4:wp客户端
|
||||
static Future replyAdd({
|
||||
required ReplyType type,
|
||||
required dynamic oid,
|
||||
required int oid,
|
||||
required String message,
|
||||
int? root,
|
||||
int? parent,
|
||||
@ -361,7 +362,7 @@ class VideoHttp {
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
return {'status': true, 'data': []};
|
||||
return {'status': false, 'data': []};
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,7 +378,7 @@ class VideoHttp {
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
return {'status': true, 'data': []};
|
||||
return {'status': false, 'data': []};
|
||||
}
|
||||
}
|
||||
|
||||
@ -433,6 +434,8 @@ class VideoHttp {
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
return {'status': false, 'data': null, 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
|
||||
@ -453,10 +456,7 @@ class VideoHttp {
|
||||
'data': AiConclusionModel.fromJson(res.data['data']),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'data': []
|
||||
};
|
||||
return {'status': false, 'data': []};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
lib/models/common/dynamic_badge_mode.dart
Normal file
9
lib/models/common/dynamic_badge_mode.dart
Normal file
@ -0,0 +1,9 @@
|
||||
enum DynamicBadgeMode { hidden, point, number }
|
||||
|
||||
extension DynamicBadgeModeDesc on DynamicBadgeMode {
|
||||
String get description => ['隐藏', '红点', '数字'][index];
|
||||
}
|
||||
|
||||
extension DynamicBadgeModeCode on DynamicBadgeMode {
|
||||
int get code => [0, 1, 2][index];
|
||||
}
|
||||
@ -17,8 +17,9 @@ class LatestDataModel {
|
||||
url = json['url'];
|
||||
tagName = json['tag_name'];
|
||||
createdAt = json['created_at'];
|
||||
assets =
|
||||
json['assets'].map<AssetItem>((e) => AssetItem.fromJson(e)).toList();
|
||||
assets = json['assets'] != null
|
||||
? json['assets'].map<AssetItem>((e) => AssetItem.fromJson(e)).toList()
|
||||
: [];
|
||||
body = json['body'];
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,7 +166,7 @@ class SessionMsgDataModel {
|
||||
int? hasMore;
|
||||
int? minSeqno;
|
||||
int? maxSeqno;
|
||||
List? eInfos;
|
||||
List<dynamic>? eInfos;
|
||||
|
||||
SessionMsgDataModel.fromJson(Map<String, dynamic> json) {
|
||||
messages = json['messages']
|
||||
|
||||
@ -1,14 +1,8 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'hot.g.dart';
|
||||
|
||||
@HiveType(typeId: 6)
|
||||
class HotSearchModel {
|
||||
HotSearchModel({
|
||||
this.list,
|
||||
});
|
||||
|
||||
@HiveField(0)
|
||||
List<HotSearchItem>? list;
|
||||
|
||||
HotSearchModel.fromJson(Map<String, dynamic> json) {
|
||||
@ -18,7 +12,6 @@ class HotSearchModel {
|
||||
}
|
||||
}
|
||||
|
||||
@HiveType(typeId: 7)
|
||||
class HotSearchItem {
|
||||
HotSearchItem({
|
||||
this.keyword,
|
||||
@ -27,20 +20,19 @@ class HotSearchItem {
|
||||
this.icon,
|
||||
});
|
||||
|
||||
@HiveField(0)
|
||||
String? keyword;
|
||||
@HiveField(1)
|
||||
String? showName;
|
||||
// 4/5热 11话题 8普通 7直播
|
||||
@HiveField(2)
|
||||
int? wordType;
|
||||
@HiveField(3)
|
||||
String? icon;
|
||||
List? liveId;
|
||||
|
||||
HotSearchItem.fromJson(Map<String, dynamic> json) {
|
||||
keyword = json['keyword'];
|
||||
showName = json['show_name'];
|
||||
wordType = json['word_type'];
|
||||
icon = json['icon'];
|
||||
liveId = json['live_id'];
|
||||
liveId = json['live_id'];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'hot.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class HotSearchModelAdapter extends TypeAdapter<HotSearchModel> {
|
||||
@override
|
||||
final int typeId = 6;
|
||||
|
||||
@override
|
||||
HotSearchModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return HotSearchModel(
|
||||
list: (fields[0] as List?)?.cast<HotSearchItem>(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, HotSearchModel obj) {
|
||||
writer
|
||||
..writeByte(1)
|
||||
..writeByte(0)
|
||||
..write(obj.list);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is HotSearchModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class HotSearchItemAdapter extends TypeAdapter<HotSearchItem> {
|
||||
@override
|
||||
final int typeId = 7;
|
||||
|
||||
@override
|
||||
HotSearchItem read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return HotSearchItem(
|
||||
keyword: fields[0] as String?,
|
||||
showName: fields[1] as String?,
|
||||
wordType: fields[2] as int?,
|
||||
icon: fields[3] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, HotSearchItem obj) {
|
||||
writer
|
||||
..writeByte(4)
|
||||
..writeByte(0)
|
||||
..write(obj.keyword)
|
||||
..writeByte(1)
|
||||
..write(obj.showName)
|
||||
..writeByte(2)
|
||||
..write(obj.wordType)
|
||||
..writeByte(3)
|
||||
..write(obj.icon);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is HotSearchItemAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@ -266,7 +266,7 @@ class BangumiIntroController extends GetxController {
|
||||
/// 未渲染回复组件时可能异常
|
||||
VideoReplyController videoReplyCtr =
|
||||
Get.find<VideoReplyController>(tag: Get.arguments['heroTag']);
|
||||
videoReplyCtr.oid = bvid;
|
||||
videoReplyCtr.aid = aid;
|
||||
videoReplyCtr.queryReplyList(type: 'init');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@ -385,8 +385,8 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return VideoReplyNewDialog(
|
||||
oid: _dynamicDetailController.oid?.toString() ??
|
||||
Get.parameters['bvid'],
|
||||
oid: _dynamicDetailController.oid ??
|
||||
IdUtils.bv2av(Get.parameters['bvid']!),
|
||||
root: 0,
|
||||
parent: 0,
|
||||
replyType: ReplyType.values[replyType],
|
||||
|
||||
@ -13,7 +13,6 @@ import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply_new/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply_reply/index.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
@ -428,7 +427,7 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return VideoReplyNewDialog(
|
||||
oid: IdUtils.av2bv(_htmlRenderCtr.oid.value),
|
||||
oid: _htmlRenderCtr.oid.value,
|
||||
root: 0,
|
||||
parent: 0,
|
||||
replyType: ReplyType.values[type],
|
||||
|
||||
@ -11,6 +11,7 @@ import 'package:pilipala/pages/home/view.dart';
|
||||
import 'package:pilipala/pages/media/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import '../../models/common/dynamic_badge_mode.dart';
|
||||
|
||||
class MainController extends GetxController {
|
||||
List<Widget> pages = <Widget>[
|
||||
@ -65,6 +66,7 @@ class MainController extends GetxController {
|
||||
int selectedIndex = 0;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
RxBool userLogin = false.obs;
|
||||
late Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -75,7 +77,12 @@ class MainController extends GetxController {
|
||||
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
|
||||
var userInfo = userInfoCache.get('userInfoCache');
|
||||
userLogin.value = userInfo != null;
|
||||
getUnreadDynamic();
|
||||
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
|
||||
SettingBoxKey.dynamicBadgeMode,
|
||||
defaultValue: DynamicBadgeMode.number.code)];
|
||||
if (dynamicBadgeType.value != DynamicBadgeMode.hidden) {
|
||||
getUnreadDynamic();
|
||||
}
|
||||
}
|
||||
|
||||
void onBackPressed(BuildContext context) {
|
||||
|
||||
@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/models/common/dynamic_badge_mode.dart';
|
||||
import 'package:pilipala/pages/dynamics/index.dart';
|
||||
import 'package:pilipala/pages/home/index.dart';
|
||||
import 'package:pilipala/pages/media/index.dart';
|
||||
@ -127,11 +128,21 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
||||
destinations: <Widget>[
|
||||
..._mainController.navigationBars.map((e) {
|
||||
return NavigationDestination(
|
||||
icon: Badge(
|
||||
label: Text(e['count'].toString()),
|
||||
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
|
||||
isLabelVisible: e['count'] > 0,
|
||||
child: e['icon'],
|
||||
icon: Obx(
|
||||
() => Badge(
|
||||
label:
|
||||
_mainController.dynamicBadgeType.value ==
|
||||
DynamicBadgeMode.number
|
||||
? Text(e['count'].toString())
|
||||
: null,
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(6, 0, 6, 0),
|
||||
isLabelVisible:
|
||||
_mainController.dynamicBadgeType.value !=
|
||||
DynamicBadgeMode.hidden &&
|
||||
e['count'] > 0,
|
||||
child: e['icon'],
|
||||
),
|
||||
),
|
||||
selectedIcon: e['selectIcon'],
|
||||
label: e['label'],
|
||||
@ -148,11 +159,21 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
||||
items: [
|
||||
..._mainController.navigationBars.map((e) {
|
||||
return BottomNavigationBarItem(
|
||||
icon: Badge(
|
||||
label: Text(e['count'].toString()),
|
||||
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
|
||||
isLabelVisible: e['count'] > 0,
|
||||
child: e['icon'],
|
||||
icon: Obx(
|
||||
() => Badge(
|
||||
label:
|
||||
_mainController.dynamicBadgeType.value ==
|
||||
DynamicBadgeMode.number
|
||||
? Text(e['count'].toString())
|
||||
: null,
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(6, 0, 6, 0),
|
||||
isLabelVisible:
|
||||
_mainController.dynamicBadgeType.value !=
|
||||
DynamicBadgeMode.hidden &&
|
||||
e['count'] > 0,
|
||||
child: e['icon'],
|
||||
),
|
||||
),
|
||||
activeIcon: e['selectIcon'],
|
||||
label: e['label'],
|
||||
|
||||
@ -64,7 +64,7 @@ class _MinePageState extends State<MinePage> {
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Get.toNamed('/setting'),
|
||||
onPressed: () => Get.toNamed('/setting', preventDuplicates: false),
|
||||
icon: const Icon(
|
||||
CupertinoIcons.slider_horizontal_3,
|
||||
),
|
||||
|
||||
@ -29,7 +29,7 @@ class SSearchController extends GetxController {
|
||||
// 其他页面跳转过来
|
||||
if (Get.parameters.keys.isNotEmpty) {
|
||||
if (Get.parameters['keyword'] != null) {
|
||||
onClickKeyword(Get.parameters['keyword']!);
|
||||
onClickKeyword(Get.parameters['keyword']!, null);
|
||||
}
|
||||
if (Get.parameters['hintText'] != null) {
|
||||
hintText = Get.parameters['hintText']!;
|
||||
@ -88,7 +88,12 @@ class SSearchController extends GetxController {
|
||||
}
|
||||
|
||||
// 点击热搜关键词
|
||||
void onClickKeyword(String keyword) {
|
||||
void onClickKeyword(String keyword, item) {
|
||||
if (item != null && item.wordType == 7) {
|
||||
Get.toNamed('/liveRoom?roomid=${item.liveId.first}',
|
||||
arguments: {'liveItem': null, 'heroTag': '${item.liveId.first}'});
|
||||
return;
|
||||
}
|
||||
searchKeyWord.value = keyword;
|
||||
controller.value.text = keyword;
|
||||
// 移动光标
|
||||
|
||||
@ -115,8 +115,8 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
onTap: () => ssCtr
|
||||
.onClickKeyword(ssCtr.searchSuggestList[index].term!),
|
||||
onTap: () => ssCtr.onClickKeyword(
|
||||
ssCtr.searchSuggestList[index].term!, null),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 20, top: 9, bottom: 9),
|
||||
child: ssCtr.searchSuggestList[index].textRich,
|
||||
@ -178,11 +178,11 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
width: width,
|
||||
// ignore: invalid_use_of_protected_member
|
||||
hotSearchList: _searchController.hotSearchList.value,
|
||||
onClick: (keyword) async {
|
||||
onClick: (keyword, item) async {
|
||||
_searchController.searchFocusNode.unfocus();
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 150));
|
||||
_searchController.onClickKeyword(keyword);
|
||||
_searchController.onClickKeyword(keyword, item);
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -193,15 +193,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 缓存数据
|
||||
if (_searchController.hotSearchList.isNotEmpty) {
|
||||
return HotKeyword(
|
||||
width: width,
|
||||
hotSearchList: _searchController.hotSearchList,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -26,7 +26,7 @@ class HotKeyword extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: InkWell(
|
||||
onTap: () => onClick!(i.keyword),
|
||||
onTap: () => onClick!(i.keyword, i),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 2,
|
||||
|
||||
@ -25,16 +25,17 @@ Widget searchArticlePanel(BuildContext context, ctr, list) {
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
|
||||
child: LayoutBuilder(builder: (context, boxConstraints) {
|
||||
double width = (boxConstraints.maxWidth -
|
||||
StyleString.cardSpace *
|
||||
6 /
|
||||
MediaQuery.textScalerOf(context).scale(2.0));
|
||||
final double width = (boxConstraints.maxWidth -
|
||||
StyleString.cardSpace *
|
||||
6 /
|
||||
MediaQuery.textScalerOf(context).scale(1.0)) /
|
||||
2;
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 88),
|
||||
height: width / StyleString.aspectRatio,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
if (list[index].imageUrls != null &&
|
||||
list[index].imageUrls.isNotEmpty)
|
||||
AspectRatio(
|
||||
|
||||
@ -7,6 +7,9 @@ import 'package:pilipala/models/common/theme_type.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/login.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import '../../models/common/dynamic_badge_mode.dart';
|
||||
import '../main/index.dart';
|
||||
import 'widgets/select_dialog.dart';
|
||||
|
||||
class SettingController extends GetxController {
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
@ -19,6 +22,7 @@ class SettingController extends GetxController {
|
||||
RxInt picQuality = 10.obs;
|
||||
Rx<ThemeType> themeType = ThemeType.system.obs;
|
||||
var userInfo;
|
||||
Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -33,6 +37,9 @@ class SettingController extends GetxController {
|
||||
setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
|
||||
themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,
|
||||
defaultValue: ThemeType.system.code)];
|
||||
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
|
||||
SettingBoxKey.dynamicBadgeMode,
|
||||
defaultValue: DynamicBadgeMode.number.code)];
|
||||
}
|
||||
|
||||
loginOut() async {
|
||||
@ -76,4 +83,31 @@ class SettingController extends GetxController {
|
||||
feedBackEnable.value = !feedBackEnable.value;
|
||||
setting.put(SettingBoxKey.feedBackEnable, feedBackEnable.value);
|
||||
}
|
||||
|
||||
// 设置动态未读标记
|
||||
setDynamicBadgeMode(BuildContext context) async {
|
||||
DynamicBadgeMode? result = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SelectDialog<DynamicBadgeMode>(
|
||||
title: '动态未读标记',
|
||||
value: dynamicBadgeType.value,
|
||||
values: DynamicBadgeMode.values.map((e) {
|
||||
return {'title': e.description, 'value': e};
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (result != null) {
|
||||
dynamicBadgeType.value = result;
|
||||
setting.put(SettingBoxKey.dynamicBadgeMode, result.code);
|
||||
MainController mainController = Get.put(MainController());
|
||||
mainController.dynamicBadgeType.value =
|
||||
DynamicBadgeMode.values[result.code];
|
||||
if (mainController.dynamicBadgeType.value != DynamicBadgeMode.hidden) {
|
||||
mainController.getUnreadDynamic();
|
||||
}
|
||||
SmartDialog.showToast('设置成功');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
|
||||
import 'package:pilipala/pages/setting/widgets/slide_dialog.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
import '../../models/common/dynamic_badge_mode.dart';
|
||||
import 'controller.dart';
|
||||
import 'widgets/switch_item.dart';
|
||||
|
||||
@ -241,6 +242,14 @@ class _StyleSettingState extends State<StyleSetting> {
|
||||
'当前模式:${settingController.themeType.value.description}',
|
||||
style: subTitleStyle)),
|
||||
),
|
||||
ListTile(
|
||||
dense: false,
|
||||
onTap: () => settingController.setDynamicBadgeMode(context),
|
||||
title: Text('动态未读标记', style: titleStyle),
|
||||
subtitle: Obx(() => Text(
|
||||
'当前标记样式:${settingController.dynamicBadgeType.value.description}',
|
||||
style: subTitleStyle)),
|
||||
),
|
||||
ListTile(
|
||||
dense: false,
|
||||
onTap: () => Get.toNamed('/colorSetting'),
|
||||
|
||||
@ -148,7 +148,9 @@ class VideoIntroController extends GetxController {
|
||||
// 获取投币状态
|
||||
Future queryHasCoinVideo() async {
|
||||
var result = await VideoHttp.hasCoinVideo(bvid: bvid);
|
||||
hasCoin.value = result["data"]['multiply'] == 0 ? false : true;
|
||||
if (result['status']) {
|
||||
hasCoin.value = result["data"]['multiply'] == 0 ? false : true;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取收藏状态
|
||||
@ -208,6 +210,10 @@ class VideoIntroController extends GetxController {
|
||||
|
||||
// (取消)点赞
|
||||
Future actionLikeVideo() async {
|
||||
if (userInfo == null) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);
|
||||
if (result['status']) {
|
||||
// hasLike.value = result["data"] == 1 ? true : false;
|
||||
@ -478,7 +484,7 @@ class VideoIntroController extends GetxController {
|
||||
/// 未渲染回复组件时可能异常
|
||||
final VideoReplyController videoReplyCtr =
|
||||
Get.find<VideoReplyController>(tag: heroTag);
|
||||
videoReplyCtr.oid = bvid;
|
||||
videoReplyCtr.aid = aid;
|
||||
videoReplyCtr.queryReplyList(type: 'init');
|
||||
} catch (_) {}
|
||||
this.bvid = bvid;
|
||||
|
||||
@ -4,7 +4,7 @@ import 'package:pilipala/http/video.dart';
|
||||
|
||||
class ReleatedController extends GetxController {
|
||||
// 视频aid
|
||||
String bvid = Get.parameters['bvid']!;
|
||||
String bvid = Get.parameters['bvid'] ?? "";
|
||||
// 推荐视频列表
|
||||
List relatedVideoList = [];
|
||||
|
||||
|
||||
@ -11,13 +11,13 @@ import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class VideoReplyController extends GetxController {
|
||||
VideoReplyController(
|
||||
this.oid,
|
||||
this.aid,
|
||||
this.rpid,
|
||||
this.replyLevel,
|
||||
);
|
||||
final ScrollController scrollController = ScrollController();
|
||||
// 视频aid 请求时使用的oid
|
||||
String? oid;
|
||||
int? aid;
|
||||
// 层级 2为楼中楼
|
||||
String? replyLevel;
|
||||
// rpid 请求楼中楼回复
|
||||
@ -57,7 +57,7 @@ class VideoReplyController extends GetxController {
|
||||
return;
|
||||
}
|
||||
final res = await ReplyHttp.replyList(
|
||||
oid: oid!,
|
||||
oid: aid!,
|
||||
pageNum: currentPage + 1,
|
||||
ps: ps,
|
||||
type: ReplyType.video.index,
|
||||
|
||||
@ -40,7 +40,6 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
bool _isFabVisible = true;
|
||||
String replyLevel = '1';
|
||||
late String heroTag;
|
||||
late String oid;
|
||||
|
||||
// 添加页面缓存
|
||||
@override
|
||||
@ -49,7 +48,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
oid = widget.bvid != null ? widget.bvid! : '0';
|
||||
int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
replyLevel = widget.replyLevel ?? '1';
|
||||
if (replyLevel == '2') {
|
||||
@ -298,8 +297,8 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return VideoReplyNewDialog(
|
||||
oid:
|
||||
_videoReplyController.oid ?? Get.parameters['bvid'],
|
||||
oid: _videoReplyController.aid ??
|
||||
IdUtils.bv2av(Get.parameters['bvid']!),
|
||||
root: 0,
|
||||
parent: 0,
|
||||
replyType: ReplyType.video,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
@ -353,7 +354,7 @@ class ReplyItem extends StatelessWidget {
|
||||
isScrollControlled: true,
|
||||
builder: (builder) {
|
||||
return VideoReplyNewDialog(
|
||||
oid: IdUtils.av2bv(replyItem!.oid!),
|
||||
oid: replyItem!.oid,
|
||||
root: replyItem!.rpid,
|
||||
parent: replyItem!.rpid,
|
||||
replyType: replyType,
|
||||
@ -539,18 +540,6 @@ InlineSpan buildContent(
|
||||
// replyReply 查看二楼回复(回复详情)回调
|
||||
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
|
||||
final content = replyItem.content;
|
||||
if (content.emote.isEmpty &&
|
||||
content.atNameToMid.isEmpty &&
|
||||
content.jumpUrl.isEmpty &&
|
||||
content.vote.isEmpty &&
|
||||
content.pictures.isEmpty) {
|
||||
return TextSpan(
|
||||
text: content.message,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap =
|
||||
() => replyReply(replyItem.root == 0 ? replyItem : fReplyItem),
|
||||
);
|
||||
}
|
||||
final List<InlineSpan> spanChilds = <InlineSpan>[];
|
||||
bool hasMatchMember = false;
|
||||
|
||||
@ -582,258 +571,171 @@ InlineSpan buildContent(
|
||||
});
|
||||
}
|
||||
// content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
|
||||
if (content.message.contains('&')) {
|
||||
content.message = content.message.replaceAll('&', '&');
|
||||
content.message = content.message.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
.replaceAll(' ', ' ');
|
||||
// print("content.jumpUrl.keys:" + content.jumpUrl.keys.toString());
|
||||
// 构建正则表达式
|
||||
final List<String> specialTokens = [
|
||||
...content.emote.keys,
|
||||
...content.atNameToMid.keys.map((e) => '@$e'),
|
||||
...content.jumpUrl.keys.map((e) =>
|
||||
e.replaceAll('?', '\\?').replaceAll('+', '\\+').replaceAll('*', '\\*')),
|
||||
];
|
||||
|
||||
String patternStr =
|
||||
specialTokens.map(RegExp.escape).join('|');
|
||||
if (patternStr.isNotEmpty) {
|
||||
patternStr += "|";
|
||||
}
|
||||
// 匹配表情
|
||||
patternStr += r'(\b\d{1,2}[::]\d{2}\b)';
|
||||
final RegExp pattern = RegExp(patternStr);
|
||||
List<String> matchedStrs = [];
|
||||
void addPlainTextSpan(str){
|
||||
spanChilds.add(TextSpan(
|
||||
text: str,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () =>
|
||||
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
|
||||
}
|
||||
// 分割文本并处理每个部分
|
||||
content.message.splitMapJoin(
|
||||
RegExp(r"\[.*?\]"),
|
||||
pattern,
|
||||
onMatch: (Match match) {
|
||||
final String matchStr = match[0]!;
|
||||
if (content.emote.isNotEmpty &&
|
||||
matchStr.indexOf('[') == matchStr.lastIndexOf('[') &&
|
||||
matchStr.indexOf(']') == matchStr.lastIndexOf(']')) {
|
||||
String matchStr = match[0]!;
|
||||
if (content.emote.containsKey(matchStr)) {
|
||||
// 处理表情
|
||||
final int size = content.emote[matchStr]['meta']['size'];
|
||||
if (content.emote.keys.contains(matchStr)) {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: NetworkImgLayer(
|
||||
src: content.emote[matchStr]['url'],
|
||||
type: 'emote',
|
||||
width: size * 20,
|
||||
height: size * 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
spanChilds.add(TextSpan(
|
||||
text: matchStr,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () =>
|
||||
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
|
||||
return matchStr;
|
||||
}
|
||||
} else {
|
||||
spanChilds.add(TextSpan(
|
||||
spanChilds.add(WidgetSpan(
|
||||
child: NetworkImgLayer(
|
||||
src: content.emote[matchStr]['url'],
|
||||
type: 'emote',
|
||||
width: size * 20,
|
||||
height: size * 20,
|
||||
),
|
||||
));
|
||||
} else if (matchStr.startsWith("@") &&
|
||||
content.atNameToMid.containsKey(matchStr.substring(1))) {
|
||||
// 处理@用户
|
||||
final String userName = matchStr.substring(1);
|
||||
final int userId = content.atNameToMid[userName];
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: matchStr,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () =>
|
||||
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
|
||||
return matchStr;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String str) {
|
||||
// 匹配@用户
|
||||
String matchMember = str;
|
||||
if (content.atNameToMid.isNotEmpty) {
|
||||
final List atNameToMidKeys = content.atNameToMid.keys.toList();
|
||||
RegExp reg = RegExp(atNameToMidKeys.map((key) => key).join('|'));
|
||||
// if (!content.message.contains(':')) {
|
||||
// reg = RegExp(r"@.*( |:)");
|
||||
// }
|
||||
|
||||
// 只@用户没有内容
|
||||
if (!content.message.contains(':') ||
|
||||
(content.atNameToMid.length == 1 &&
|
||||
content.message == '@${content.members.first.uname}')) {
|
||||
reg = RegExp(r"@.*( |:|$)");
|
||||
}
|
||||
matchMember = str.splitMapJoin(
|
||||
reg,
|
||||
onMatch: (Match match) {
|
||||
if (match[0] != null) {
|
||||
hasMatchMember = true;
|
||||
content.atNameToMid.forEach((key, value) {
|
||||
if (str.contains('回复')) {
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: '回复 ',
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.titleSmall!.fontSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: '@$key',
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.titleSmall!.fontSize,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
final String heroTag = Utils.makeHeroTag(value);
|
||||
Get.toNamed(
|
||||
'/member?mid=$value',
|
||||
arguments: {'face': '', 'heroTag': heroTag},
|
||||
);
|
||||
},
|
||||
),
|
||||
..onTap = () {
|
||||
final String heroTag = Utils.makeHeroTag(userId);
|
||||
Get.toNamed(
|
||||
'/member?mid=$userId',
|
||||
arguments: {'face': '', 'heroTag': heroTag},
|
||||
);
|
||||
});
|
||||
}
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String str) {
|
||||
if (!str.contains('@')) {
|
||||
spanChilds.add(TextSpan(text: str));
|
||||
}
|
||||
print(str);
|
||||
return str;
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
} else if (RegExp(r'^\b[0-9]{1,2}[::][0-9]{2}\b$').hasMatch(matchStr)) {
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: ' $matchStr ',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// 跳转到指定位置
|
||||
try {
|
||||
matchStr = matchStr.replaceAll(':', ':');
|
||||
SmartDialog.showToast('跳转至:$matchStr');
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
|
||||
.plPlayerController
|
||||
.seekTo(
|
||||
Duration(seconds: Utils.duration(matchStr)),
|
||||
);
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('跳转失败: $e');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
matchMember = str;
|
||||
}
|
||||
|
||||
// 匹配 jumpUrl
|
||||
String matchUrl = matchMember;
|
||||
if (content.jumpUrl.isNotEmpty) {
|
||||
final List urlKeys = content.jumpUrl.keys.toList().reversed.toList();
|
||||
for (int index = 0; index < urlKeys.length; index++) {
|
||||
var i = urlKeys[index];
|
||||
if (i.contains('?')) {
|
||||
urlKeys[index] = i.replaceAll('?', '\\?');
|
||||
// print("matchStr=$matchStr");
|
||||
String appUrlSchema = '';
|
||||
final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe,
|
||||
defaultValue: false) as bool;
|
||||
if (content.jumpUrl[matchStr] != null &&
|
||||
!matchedStrs.contains(matchStr)) {
|
||||
appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
|
||||
if (appUrlSchema.startsWith('bilibili://search') && !enableWordRe) {
|
||||
addPlainTextSpan(matchStr);
|
||||
return "";
|
||||
}
|
||||
if (i.contains('+')) {
|
||||
urlKeys[index] = i.replaceAll('+', '\\+');
|
||||
}
|
||||
if (i.contains('*')) {
|
||||
urlKeys[index] = i.replaceAll('*', '\\*');
|
||||
}
|
||||
}
|
||||
if (hasMatchMember) {
|
||||
matchMember = matchMember.split('回复 @ :').length > 1
|
||||
? matchMember.split('回复 @ :')[1]
|
||||
: matchMember;
|
||||
}
|
||||
matchUrl = matchMember.splitMapJoin(
|
||||
/// RegExp.escape() 转义特殊字符
|
||||
RegExp(urlKeys.map((key) => key).join("|")),
|
||||
// RegExp('What does the fox say\\?'),
|
||||
onMatch: (Match match) {
|
||||
final String matchStr = match[0]!;
|
||||
String appUrlSchema = '';
|
||||
if (content.jumpUrl[matchStr] != null) {
|
||||
appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
|
||||
}
|
||||
// 默认不显示关键词
|
||||
final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe,
|
||||
defaultValue: false) as bool;
|
||||
if (content.jumpUrl[matchStr] != null) {
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: content.jumpUrl[matchStr]['title'],
|
||||
style: TextStyle(
|
||||
color: enableWordRe
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
if (appUrlSchema == '') {
|
||||
final String str = Uri.parse(matchStr).pathSegments[0];
|
||||
final Map matchRes = IdUtils.matchAvorBv(input: str);
|
||||
final List matchKeys = matchRes.keys.toList();
|
||||
if (matchKeys.isNotEmpty) {
|
||||
if (matchKeys.first == 'BV') {
|
||||
Get.toNamed(
|
||||
'/searchResult',
|
||||
parameters: {'keyword': matchRes['BV']},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': matchStr,
|
||||
'type': 'url',
|
||||
'pageTitle': ''
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (appUrlSchema.startsWith('bilibili://search') &&
|
||||
enableWordRe) {
|
||||
Get.toNamed('/searchResult', parameters: {
|
||||
'keyword': content.jumpUrl[matchStr]['title']
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (appUrlSchema.startsWith('bilibili://search') && enableWordRe) {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: Icon(
|
||||
FontAwesomeIcons.magnifyingGlass,
|
||||
size: 9,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
alignment: PlaceholderAlignment.top,
|
||||
),
|
||||
);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String str) {
|
||||
spanChilds.add(TextSpan(
|
||||
text: str,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => replyReply(
|
||||
replyItem.root == 0 ? replyItem : fReplyItem)));
|
||||
return str;
|
||||
},
|
||||
);
|
||||
}
|
||||
str = matchUrl.splitMapJoin(
|
||||
RegExp(r'\b\d{2}:\d{2}\b'),
|
||||
onMatch: (Match match) {
|
||||
String matchStr = match[0]!;
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: ' $matchStr ',
|
||||
text: content.jumpUrl[matchStr]['title'],
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// 跳转到指定位置
|
||||
try {
|
||||
Get.find<VideoDetailController>(
|
||||
tag: Get.arguments['heroTag'])
|
||||
.plPlayerController
|
||||
.seekTo(
|
||||
Duration(seconds: Utils.duration(matchStr)),
|
||||
if (appUrlSchema == '') {
|
||||
final String str = Uri.parse(matchStr).pathSegments[0];
|
||||
final Map matchRes = IdUtils.matchAvorBv(input: str);
|
||||
final List matchKeys = matchRes.keys.toList();
|
||||
if (matchKeys.isNotEmpty) {
|
||||
if (matchKeys.first == 'BV') {
|
||||
Get.toNamed(
|
||||
'/searchResult',
|
||||
parameters: {'keyword': matchRes['BV']},
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
} else {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': matchStr,
|
||||
'type': 'url',
|
||||
'pageTitle': ''
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (appUrlSchema.startsWith('bilibili://search')) {
|
||||
Get.toNamed('/searchResult', parameters: {
|
||||
'keyword': content.jumpUrl[matchStr]['title']
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (str) {
|
||||
return str;
|
||||
},
|
||||
);
|
||||
|
||||
if (content.atNameToMid.isEmpty && content.jumpUrl.isEmpty) {
|
||||
if (str != '') {
|
||||
spanChilds.add(TextSpan(
|
||||
text: str,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () =>
|
||||
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
|
||||
if (appUrlSchema.startsWith('bilibili://search')) {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: Icon(
|
||||
FontAwesomeIcons.magnifyingGlass,
|
||||
size: 9,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
alignment: PlaceholderAlignment.top,
|
||||
),
|
||||
);
|
||||
}
|
||||
// 只显示一次
|
||||
matchedStrs.add(matchStr);
|
||||
} else {
|
||||
addPlainTextSpan(matchStr);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String nonMatchStr) {
|
||||
addPlainTextSpan(nonMatchStr);
|
||||
return nonMatchStr;
|
||||
},
|
||||
);
|
||||
|
||||
@ -841,10 +743,10 @@ InlineSpan buildContent(
|
||||
if (content.pictures.isNotEmpty) {
|
||||
final List<String> picList = <String>[];
|
||||
final int len = content.pictures.length;
|
||||
spanChilds.add(const TextSpan(text: '\n'));
|
||||
if (len == 1) {
|
||||
Map pictureItem = content.pictures.first;
|
||||
picList.add(pictureItem['img_src']);
|
||||
spanChilds.add(const TextSpan(text: '\n'));
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: LayoutBuilder(
|
||||
|
||||
@ -8,7 +8,7 @@ import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
|
||||
class VideoReplyNewDialog extends StatefulWidget {
|
||||
final String? oid;
|
||||
final int? oid;
|
||||
final int? root;
|
||||
final int? parent;
|
||||
final ReplyType? replyType;
|
||||
|
||||
@ -61,6 +61,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
final Floating floating = Floating();
|
||||
// 生命周期监听
|
||||
late final AppLifecycleListener _lifecycleListener;
|
||||
bool isShowing = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -216,15 +217,15 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
videoIntroController.isPaused = true;
|
||||
plPlayerController!.removeStatusLister(playerListener);
|
||||
plPlayerController!.pause();
|
||||
plPlayerController!.danmakuController?.pause();
|
||||
plPlayerController!.danmakuController?.clear();
|
||||
}
|
||||
setState(() => isShowing = false);
|
||||
super.didPushNext();
|
||||
}
|
||||
|
||||
@override
|
||||
// 返回当前页面时
|
||||
void didPopNext() async {
|
||||
setState(() => isShowing = true);
|
||||
videoDetailController.isFirstTime = false;
|
||||
final bool autoplay = autoPlayEnable;
|
||||
videoDetailController.playerInit(autoplay: autoplay);
|
||||
@ -280,19 +281,13 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16;
|
||||
final double pinnedHeaderHeight =
|
||||
statusBarHeight + kToolbarHeight + videoHeight;
|
||||
if (MediaQuery.of(context).orientation == Orientation.landscape ||
|
||||
plPlayerController?.isFullScreen.value == true) {
|
||||
enterFullScreen();
|
||||
} else {
|
||||
exitFullScreen();
|
||||
}
|
||||
Widget childWhenDisabled = SafeArea(
|
||||
top: MediaQuery.of(context).orientation == Orientation.portrait &&
|
||||
plPlayerController?.isFullScreen.value == true,
|
||||
bottom: MediaQuery.of(context).orientation == Orientation.portrait &&
|
||||
plPlayerController?.isFullScreen.value == true,
|
||||
left: plPlayerController?.isFullScreen.value != true,
|
||||
right: plPlayerController?.isFullScreen.value != true,
|
||||
left: false, //plPlayerController?.isFullScreen.value != true,
|
||||
right: false, //plPlayerController?.isFullScreen.value != true,
|
||||
child: Stack(
|
||||
children: [
|
||||
Scaffold(
|
||||
@ -309,187 +304,189 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
body: ExtendedNestedScrollView(
|
||||
controller: _extendNestCtr,
|
||||
headerSliverBuilder:
|
||||
(BuildContext context, bool innerBoxIsScrolled) {
|
||||
(BuildContext context2, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
Obx(
|
||||
() => SliverAppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
// 假装使用一个非空变量,避免Obx检测不到而罢工
|
||||
pinned: videoDetailController.autoPlay.value ^
|
||||
false ^
|
||||
videoDetailController.autoPlay.value,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
forceElevated: innerBoxIsScrolled,
|
||||
expandedHeight: MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape ||
|
||||
plPlayerController?.isFullScreen.value == true
|
||||
? MediaQuery.sizeOf(context).height -
|
||||
(MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape
|
||||
? 0
|
||||
: MediaQuery.of(context).padding.top)
|
||||
: videoHeight,
|
||||
backgroundColor: Colors.black,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: PopScope(
|
||||
canPop:
|
||||
plPlayerController?.isFullScreen.value != true,
|
||||
onPopInvoked: (bool didPop) {
|
||||
if (plPlayerController?.isFullScreen.value ==
|
||||
true) {
|
||||
plPlayerController!
|
||||
.triggerFullScreen(status: false);
|
||||
}
|
||||
if (MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape) {
|
||||
verticalScreen();
|
||||
}
|
||||
},
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context,
|
||||
BoxConstraints boxConstraints) {
|
||||
final double maxWidth = boxConstraints.maxWidth;
|
||||
final double maxHeight =
|
||||
boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.data['status']) {
|
||||
return Obx(
|
||||
() => !videoDetailController
|
||||
.autoPlay.value
|
||||
? const SizedBox()
|
||||
: PLVideoPlayer(
|
||||
controller:
|
||||
plPlayerController!,
|
||||
headerControl:
|
||||
videoDetailController
|
||||
.headerControl,
|
||||
danmuWidget: Obx(
|
||||
() => PlDanmaku(
|
||||
key: Key(
|
||||
videoDetailController
|
||||
.danmakuCid
|
||||
.value
|
||||
.toString()),
|
||||
cid:
|
||||
videoDetailController
|
||||
.danmakuCid
|
||||
.value,
|
||||
playerController:
|
||||
plPlayerController!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
() {
|
||||
if (MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape ||
|
||||
plPlayerController?.isFullScreen.value == true) {
|
||||
enterFullScreen();
|
||||
} else {
|
||||
exitFullScreen();
|
||||
}
|
||||
return SliverAppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
// 假装使用一个非空变量,避免Obx检测不到而罢工
|
||||
pinned: videoDetailController.autoPlay.value ^
|
||||
false ^
|
||||
videoDetailController.autoPlay.value,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
forceElevated: innerBoxIsScrolled,
|
||||
expandedHeight: MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape ||
|
||||
plPlayerController?.isFullScreen.value == true
|
||||
? MediaQuery.sizeOf(context).height -
|
||||
(MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape
|
||||
? 0
|
||||
: MediaQuery.of(context).padding.top)
|
||||
: videoHeight,
|
||||
backgroundColor: Colors.black,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: PopScope(
|
||||
canPop: plPlayerController?.isFullScreen.value !=
|
||||
true,
|
||||
onPopInvoked: (bool didPop) {
|
||||
if (plPlayerController?.isFullScreen.value ==
|
||||
true) {
|
||||
plPlayerController!
|
||||
.triggerFullScreen(status: false);
|
||||
}
|
||||
if (MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape) {
|
||||
verticalScreen();
|
||||
}
|
||||
},
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context,
|
||||
BoxConstraints boxConstraints) {
|
||||
final double maxWidth =
|
||||
boxConstraints.maxWidth;
|
||||
final double maxHeight =
|
||||
boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
if (isShowing)
|
||||
FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.data['status']) {
|
||||
return Obx(
|
||||
() =>
|
||||
!videoDetailController
|
||||
.autoPlay.value
|
||||
? nil
|
||||
: PLVideoPlayer(
|
||||
controller:
|
||||
plPlayerController!,
|
||||
headerControl:
|
||||
videoDetailController
|
||||
.headerControl,
|
||||
danmuWidget: Obx(
|
||||
() => PlDanmaku(
|
||||
key: Key(videoDetailController
|
||||
.danmakuCid
|
||||
.value
|
||||
.toString()),
|
||||
cid: videoDetailController
|
||||
.danmakuCid
|
||||
.value,
|
||||
playerController:
|
||||
plPlayerController!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
/// 关闭自动播放时 手动播放
|
||||
if (!videoDetailController
|
||||
.autoPlay.value) ...<Widget>[
|
||||
Obx(
|
||||
() => Visibility(
|
||||
visible: videoDetailController
|
||||
.isShowCover.value,
|
||||
child: Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
handlePlay();
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
type: 'emote',
|
||||
src: videoDetailController
|
||||
.videoItem['pic'],
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
/// 关闭自动播放时 手动播放
|
||||
if (!videoDetailController
|
||||
.autoPlay.value) ...<Widget>[
|
||||
Obx(
|
||||
() => Visibility(
|
||||
visible: videoDetailController
|
||||
.isShowCover.value,
|
||||
child: Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
handlePlay();
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
type: 'emote',
|
||||
src: videoDetailController
|
||||
.videoItem['pic'],
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Visibility(
|
||||
visible: videoDetailController
|
||||
.isShowCover.value &&
|
||||
videoDetailController
|
||||
.isEffective.value,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: AppBar(
|
||||
primary: false,
|
||||
foregroundColor:
|
||||
Colors.white,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
backgroundColor:
|
||||
Colors.transparent,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: '稍后再看',
|
||||
onPressed: () async {
|
||||
var res = await UserHttp
|
||||
.toViewLater(
|
||||
bvid:
|
||||
videoDetailController
|
||||
.bvid);
|
||||
SmartDialog.showToast(
|
||||
res['msg']);
|
||||
},
|
||||
icon: const Icon(Icons
|
||||
.history_outlined),
|
||||
),
|
||||
const SizedBox(width: 14)
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 12,
|
||||
bottom: 10,
|
||||
child: TextButton.icon(
|
||||
style: ButtonStyle(
|
||||
Obx(
|
||||
() => Visibility(
|
||||
visible: videoDetailController
|
||||
.isShowCover.value &&
|
||||
videoDetailController
|
||||
.isEffective.value,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: AppBar(
|
||||
primary: false,
|
||||
foregroundColor:
|
||||
Colors.white,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
backgroundColor:
|
||||
MaterialStateProperty
|
||||
.resolveWith(
|
||||
(states) {
|
||||
return Colors.white
|
||||
.withOpacity(0.8);
|
||||
}),
|
||||
Colors.transparent,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: '稍后再看',
|
||||
onPressed: () async {
|
||||
var res = await UserHttp
|
||||
.toViewLater(
|
||||
bvid: videoDetailController
|
||||
.bvid);
|
||||
SmartDialog
|
||||
.showToast(
|
||||
res['msg']);
|
||||
},
|
||||
icon: const Icon(Icons
|
||||
.history_outlined),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 14)
|
||||
],
|
||||
),
|
||||
onPressed: () =>
|
||||
handlePlay(),
|
||||
icon: const Icon(
|
||||
Icons.play_circle_outline,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text('轻触封面播放'),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
},
|
||||
)),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 12,
|
||||
bottom: 10,
|
||||
child: IconButton(
|
||||
tooltip: '播放',
|
||||
onPressed: () =>
|
||||
handlePlay(),
|
||||
icon: Image.asset(
|
||||
'assets/images/play.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
)),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
},
|
||||
)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
},
|
||||
@ -500,7 +497,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
// },
|
||||
/// 不收回
|
||||
pinnedHeaderSliverHeightBuilder: () {
|
||||
return plPlayerController?.isFullScreen.value == true
|
||||
return MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape ||
|
||||
plPlayerController?.isFullScreen.value == true
|
||||
? MediaQuery.sizeOf(context).height
|
||||
: pinnedHeaderHeight;
|
||||
},
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/msg.dart';
|
||||
import 'package:pilipala/models/msg/session.dart';
|
||||
@ -8,6 +9,8 @@ class WhisperDetailController extends GetxController {
|
||||
late String face;
|
||||
late String mid;
|
||||
RxList<MessageItem> messageList = <MessageItem>[].obs;
|
||||
//表情转换图片规则
|
||||
List<dynamic>? eInfos;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -22,6 +25,9 @@ class WhisperDetailController extends GetxController {
|
||||
var res = await MsgHttp.sessionMsg(talkerId: talkerId);
|
||||
if (res['status']) {
|
||||
messageList.value = res['data'].messages;
|
||||
if (messageList.isNotEmpty && res['data'].eInfos != null) {
|
||||
eInfos = res['data'].eInfos;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -110,12 +110,16 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
|
||||
if (i == 0) {
|
||||
return Column(
|
||||
children: [
|
||||
ChatItem(item: messageList[i]),
|
||||
ChatItem(
|
||||
item: messageList[i],
|
||||
e_infos: _whisperDetailController.eInfos),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return ChatItem(item: messageList[i]);
|
||||
return ChatItem(
|
||||
item: messageList[i],
|
||||
e_infos: _whisperDetailController.eInfos);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@ -1,38 +1,370 @@
|
||||
// ignore_for_file: must_be_immutable
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
import '../../../http/search.dart';
|
||||
|
||||
enum MsgType {
|
||||
invalid(value: 0, label: "空空的~"),
|
||||
text(value: 1, label: "文本消息"),
|
||||
pic(value: 2, label: "图片消息"),
|
||||
audio(value: 3, label: "语音消息"),
|
||||
share(value: 4, label: "分享消息"),
|
||||
revoke(value: 5, label: "撤回消息"),
|
||||
custom_face(value: 6, label: "自定义表情"),
|
||||
share_v2(value: 7, label: "分享v2消息"),
|
||||
sys_cancel(value: 8, label: "系统撤销"),
|
||||
mini_program(value: 9, label: "小程序"),
|
||||
notify_msg(value: 10, label: "业务通知"),
|
||||
archive_card(value: 11, label: "投稿卡片"),
|
||||
article_card(value: 12, label: "专栏卡片"),
|
||||
pic_card(value: 13, label: "图片卡片"),
|
||||
common_share(value: 14, label: "异形卡片"),
|
||||
auto_reply_push(value: 16, label: "自动回复推送"),
|
||||
notify_text(value: 18, label: "文本提示");
|
||||
|
||||
final int value;
|
||||
final String label;
|
||||
const MsgType({required this.value, required this.label});
|
||||
static MsgType parse(int value) {
|
||||
return MsgType.values
|
||||
.firstWhere((e) => e.value == value, orElse: () => MsgType.invalid);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatItem extends StatelessWidget {
|
||||
dynamic item;
|
||||
List? e_infos;
|
||||
|
||||
ChatItem({
|
||||
super.key,
|
||||
this.item,
|
||||
this.e_infos,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool isOwner =
|
||||
item.senderUid == GStrorage.userInfo.get('userInfoCache').mid;
|
||||
bool isPic = item.msgType == 2; // 图片
|
||||
bool isText = item.msgType == 1; // 文本
|
||||
// bool isAchive = item.msgType == 11; // 投稿
|
||||
// bool isArticle = item.msgType == 12; // 专栏
|
||||
bool isRevoke = item.msgType == 5; // 撤回消息
|
||||
|
||||
bool isSystem =
|
||||
item.msgType == 18 || item.msgType == 10 || item.msgType == 13;
|
||||
int msgType = item.msgType;
|
||||
bool isPic = item.msgType == MsgType.pic.value; // 图片
|
||||
bool isText = item.msgType == MsgType.text.value; // 文本
|
||||
// bool isArchive = item.msgType == 11; // 投稿
|
||||
// bool isArticle = item.msgType == 12; // 专栏
|
||||
bool isRevoke = item.msgType == MsgType.revoke.value; // 撤回消息
|
||||
bool isShareV2 = item.msgType == MsgType.share_v2.value;
|
||||
bool isSystem = item.msgType == MsgType.notify_text.value ||
|
||||
item.msgType == MsgType.notify_msg.value ||
|
||||
item.msgType == MsgType.pic_card.value ||
|
||||
item.msgType == MsgType.auto_reply_push.value;
|
||||
dynamic content = item.content ?? '';
|
||||
Color textColor(BuildContext context) {
|
||||
return isOwner
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSecondaryContainer;
|
||||
}
|
||||
|
||||
Widget richTextMessage(BuildContext context) {
|
||||
var text = content['content'];
|
||||
if (e_infos != null) {
|
||||
final List<InlineSpan> children = [];
|
||||
Map<String, String> emojiMap = {};
|
||||
for (var e in e_infos!) {
|
||||
emojiMap[e['text']] = e['url'];
|
||||
}
|
||||
text.splitMapJoin(
|
||||
RegExp(r"\[.+?\]"),
|
||||
onMatch: (Match match) {
|
||||
final String emojiKey = match[0]!;
|
||||
if (emojiMap.containsKey(emojiKey)) {
|
||||
children.add(WidgetSpan(
|
||||
child: NetworkImgLayer(
|
||||
width: 18,
|
||||
height: 18,
|
||||
src: emojiMap[emojiKey]!,
|
||||
),
|
||||
));
|
||||
}
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String text) {
|
||||
children.add(TextSpan(
|
||||
text: text,
|
||||
style: TextStyle(
|
||||
color: textColor(context),
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
)));
|
||||
return '';
|
||||
},
|
||||
);
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
color: textColor(context),
|
||||
height: 1.5,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget messageContent(BuildContext context) {
|
||||
switch (MsgType.parse(item.msgType)) {
|
||||
case MsgType.notify_msg:
|
||||
return SystemNotice(item: item);
|
||||
case MsgType.pic_card:
|
||||
return SystemNotice2(item: item);
|
||||
case MsgType.notify_text:
|
||||
return Text(
|
||||
jsonDecode(content['content'])
|
||||
.map((m) => m['text'] as String)
|
||||
.join("\n"),
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 5,
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.8),
|
||||
),
|
||||
);
|
||||
case MsgType.text:
|
||||
return richTextMessage(context);
|
||||
case MsgType.pic:
|
||||
return NetworkImgLayer(
|
||||
width: 220,
|
||||
height: 220 * content['height'] / content['width'],
|
||||
src: content['url'],
|
||||
);
|
||||
case MsgType.share_v2:
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
SmartDialog.showLoading();
|
||||
var bvid = content["bvid"];
|
||||
final int cid = await SearchHttp.ab2c(bvid: bvid);
|
||||
final String heroTag = Utils.makeHeroTag(bvid);
|
||||
SmartDialog.dismiss<dynamic>().then(
|
||||
(e) => Get.toNamed<dynamic>('/video?bvid=$bvid&cid=$cid',
|
||||
arguments: <String, String?>{
|
||||
'pic': content['thumb'],
|
||||
'heroTag': heroTag,
|
||||
}),
|
||||
);
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
width: 220,
|
||||
height: 220 * 9 / 16,
|
||||
src: content['thumb'],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
content['title'],
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text(
|
||||
content['author'],
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context).withOpacity(0.6),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
case MsgType.archive_card:
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
SmartDialog.showLoading();
|
||||
var bvid = content["bvid"];
|
||||
final int cid = await SearchHttp.ab2c(bvid: bvid);
|
||||
final String heroTag = Utils.makeHeroTag(bvid);
|
||||
SmartDialog.dismiss<dynamic>().then(
|
||||
(e) => Get.toNamed<dynamic>('/video?bvid=$bvid&cid=$cid',
|
||||
arguments: <String, String?>{
|
||||
'pic': content['thumb'],
|
||||
'heroTag': heroTag,
|
||||
}),
|
||||
);
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
width: 220,
|
||||
height: 220 * 9 / 16,
|
||||
src: content['cover'],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
content['title'],
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text(
|
||||
Utils.timeFormat(content['times']),
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context).withOpacity(0.6),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
case MsgType.auto_reply_push:
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 300.0, // 设置最大宽度为200.0
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
.withOpacity(0.4),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(6),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
content['main_title'],
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
for (var i in content['sub_cards']) ...<Widget>[
|
||||
const SizedBox(height: 6),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}',
|
||||
caseSensitive: false);
|
||||
Iterable<Match> matches =
|
||||
bvRegex.allMatches(i['jump_url']);
|
||||
if (matches.isNotEmpty) {
|
||||
Match match = matches.first;
|
||||
String bvid = match.group(0)!;
|
||||
try {
|
||||
SmartDialog.showLoading();
|
||||
final int cid = await SearchHttp.ab2c(bvid: bvid);
|
||||
final String heroTag = Utils.makeHeroTag(bvid);
|
||||
SmartDialog.dismiss<dynamic>().then(
|
||||
(e) => Get.toNamed<dynamic>(
|
||||
'/video?bvid=$bvid&cid=$cid',
|
||||
arguments: <String, String?>{
|
||||
'pic': i['cover_url'],
|
||||
'heroTag': heroTag,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
} else {
|
||||
SmartDialog.showToast('未匹配到 BV 号');
|
||||
Get.toNamed('/webview',
|
||||
arguments: {'url': i['jump_url']});
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: 130,
|
||||
height: 130 * 9 / 16,
|
||||
src: i['cover_url'],
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
i['field1'],
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
i['field2'],
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context).withOpacity(0.6),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
Utils.timeFormat(int.parse(i['field3'])),
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context).withOpacity(0.6),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
)),
|
||||
],
|
||||
],
|
||||
));
|
||||
default:
|
||||
return Text(
|
||||
content['content'] ?? content.toString(),
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return isSystem
|
||||
? (msgType == 10
|
||||
? SystemNotice(item: item)
|
||||
: msgType == 13
|
||||
? SystemNotice2(item: item)
|
||||
: const SizedBox())
|
||||
? messageContent(context)
|
||||
: isRevoke
|
||||
? const SizedBox()
|
||||
: Row(
|
||||
@ -66,27 +398,7 @@ class ChatItem extends StatelessWidget {
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
isText
|
||||
? Text(
|
||||
content['content'],
|
||||
style: TextStyle(
|
||||
color: isOwner
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer),
|
||||
)
|
||||
: isPic
|
||||
? NetworkImgLayer(
|
||||
width: 220,
|
||||
height: 220 *
|
||||
content['height'] /
|
||||
content['width'],
|
||||
src: content['url'],
|
||||
)
|
||||
: const SizedBox(),
|
||||
messageContent(context),
|
||||
SizedBox(height: isPic ? 7 : 2),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@ -147,8 +147,8 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
|
||||
processingState: AudioProcessingState.idle,
|
||||
playing: false,
|
||||
));
|
||||
_item.removeLast();
|
||||
if (_item.isNotEmpty) {
|
||||
_item.removeLast();
|
||||
setMediaItem(_item.last);
|
||||
}
|
||||
if (_item.isEmpty) {
|
||||
|
||||
@ -1,51 +1,65 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: constant_identifier_names, non_constant_identifier_names
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IdUtils {
|
||||
static const String TABLE =
|
||||
'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF';
|
||||
static const List<int> S = [11, 10, 3, 8, 4, 6]; // 位置编码表
|
||||
static const int XOR = 177451812; // 固定异或值
|
||||
static const int ADD = 8728348608; // 固定加法值
|
||||
static const List<String> r = [
|
||||
'B',
|
||||
'V',
|
||||
'1',
|
||||
'',
|
||||
'',
|
||||
'4',
|
||||
'',
|
||||
'1',
|
||||
'',
|
||||
'7',
|
||||
'',
|
||||
''
|
||||
];
|
||||
static final XOR_CODE = BigInt.parse('23442827791579');
|
||||
static final MASK_CODE = BigInt.parse('2251799813685247');
|
||||
static final MAX_AID = BigInt.one << (BigInt.from(51)).toInt();
|
||||
static final BASE = BigInt.from(58);
|
||||
|
||||
static const data =
|
||||
'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf';
|
||||
|
||||
/// av转bv
|
||||
static String av2bv(int av) {
|
||||
int x_ = (av ^ XOR) + ADD;
|
||||
List<String> newR = [];
|
||||
newR.addAll(r);
|
||||
for (int i = 0; i < S.length; i++) {
|
||||
newR[S[i]] =
|
||||
TABLE.characters.elementAt((x_ / pow(58, i).toInt() % 58).toInt());
|
||||
static String av2bv(int aid) {
|
||||
List<String> bytes = [
|
||||
'B',
|
||||
'V',
|
||||
'1',
|
||||
'0',
|
||||
'0',
|
||||
'0',
|
||||
'0',
|
||||
'0',
|
||||
'0',
|
||||
'0',
|
||||
'0',
|
||||
'0'
|
||||
];
|
||||
int bvIndex = bytes.length - 1;
|
||||
BigInt tmp = (MAX_AID | BigInt.from(aid)) ^ XOR_CODE;
|
||||
while (tmp > BigInt.zero) {
|
||||
bytes[bvIndex] = data[(tmp % BASE).toInt()];
|
||||
tmp = tmp ~/ BASE;
|
||||
bvIndex -= 1;
|
||||
}
|
||||
return newR.join();
|
||||
String tmpSwap = bytes[3];
|
||||
bytes[3] = bytes[9];
|
||||
bytes[9] = tmpSwap;
|
||||
|
||||
tmpSwap = bytes[4];
|
||||
bytes[4] = bytes[7];
|
||||
bytes[7] = tmpSwap;
|
||||
|
||||
return bytes.join();
|
||||
}
|
||||
|
||||
/// bv转bv
|
||||
static int bv2av(String bv) {
|
||||
int r = 0;
|
||||
for (int i = 0; i < S.length; i++) {
|
||||
r += (TABLE.indexOf(bv.characters.elementAt(S[i])).toInt()) *
|
||||
pow(58, i).toInt();
|
||||
}
|
||||
return (r - ADD) ^ XOR;
|
||||
/// bv转av
|
||||
static int bv2av(String bvid) {
|
||||
List<String> bvidArr = bvid.split('');
|
||||
final tmpValue = bvidArr[3];
|
||||
bvidArr[3] = bvidArr[9];
|
||||
bvidArr[9] = tmpValue;
|
||||
|
||||
final tmpValue2 = bvidArr[4];
|
||||
bvidArr[4] = bvidArr[7];
|
||||
bvidArr[7] = tmpValue2;
|
||||
|
||||
bvidArr.removeRange(0, 3);
|
||||
BigInt tmp = bvidArr.fold(BigInt.zero,
|
||||
(pre, bvidChar) => pre * BASE + BigInt.from(data.indexOf(bvidChar)));
|
||||
return ((tmp & MASK_CODE) ^ XOR_CODE).toInt();
|
||||
}
|
||||
|
||||
// 匹配
|
||||
|
||||
@ -4,7 +4,6 @@ import 'dart:io';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:pilipala/models/model_owner.dart';
|
||||
import 'package:pilipala/models/search/hot.dart';
|
||||
import 'package:pilipala/models/user/info.dart';
|
||||
|
||||
class GStrorage {
|
||||
@ -48,8 +47,6 @@ class GStrorage {
|
||||
Hive.registerAdapter(OwnerAdapter());
|
||||
Hive.registerAdapter(UserInfoDataAdapter());
|
||||
Hive.registerAdapter(LevelInfoAdapter());
|
||||
Hive.registerAdapter(HotSearchModelAdapter());
|
||||
Hive.registerAdapter(HotSearchItemAdapter());
|
||||
}
|
||||
|
||||
static Future<void> lazyInit() async {
|
||||
@ -130,7 +127,8 @@ class SettingBoxKey {
|
||||
enableMYBar = 'enableMYBar',
|
||||
hideSearchBar = 'hideSearchBar', // 收起顶栏
|
||||
hideTabBar = 'hideTabBar', // 收起底栏
|
||||
tabbarSort = 'tabbarSort'; // 首页tabbar
|
||||
tabbarSort = 'tabbarSort', // 首页tabbar
|
||||
dynamicBadgeMode = 'dynamicBadgeMode';
|
||||
}
|
||||
|
||||
class LocalCacheKey {
|
||||
|
||||
@ -276,16 +276,18 @@ class Utils {
|
||||
// [arm64-v8a]
|
||||
String abi = androidInfo.supportedAbis.first;
|
||||
late String downloadUrl;
|
||||
for (var i in data.assets) {
|
||||
if (i.downloadUrl.contains(abi)) {
|
||||
downloadUrl = i.downloadUrl;
|
||||
if (data.assets.isNotEmpty) {
|
||||
for (var i in data.assets) {
|
||||
if (i.downloadUrl.contains(abi)) {
|
||||
downloadUrl = i.downloadUrl;
|
||||
}
|
||||
}
|
||||
// 应用外下载
|
||||
launchUrl(
|
||||
Uri.parse(downloadUrl),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
}
|
||||
// 应用外下载
|
||||
launchUrl(
|
||||
Uri.parse(downloadUrl),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -500,10 +500,11 @@ packages:
|
||||
floating:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: floating
|
||||
sha256: d9d563089e34fbd714ffdcdd2df447ec41b40c9226dacae6b4f78847aef8b991
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: d2d8421c4d80f6113f832404109853684721e11a
|
||||
url: "https://github.com/guozhigq/floating.git"
|
||||
source: git
|
||||
version: "2.0.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
|
||||
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.18+1018
|
||||
version: 1.0.19+1019
|
||||
|
||||
environment:
|
||||
sdk: ">=2.19.6 <3.0.0"
|
||||
@ -124,7 +124,10 @@ dependencies:
|
||||
# 代理
|
||||
system_proxy: ^0.1.0
|
||||
# pip
|
||||
floating: ^2.0.1
|
||||
floating:
|
||||
git:
|
||||
url: https://github.com/guozhigq/floating.git
|
||||
ref: main
|
||||
# html解析
|
||||
html: ^0.15.4
|
||||
# html渲染
|
||||
|
||||
Reference in New Issue
Block a user