Merge branch 'main' into mod-not-login-recommend2

This commit is contained in:
guozhigq
2024-02-05 00:35:11 +08:00
committed by GitHub
43 changed files with 1546 additions and 665 deletions

View File

@@ -133,6 +133,11 @@ class _AboutPageState extends State<AboutPage> {
title: const Text('赞助'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
ListTile(
onTap: () => _aboutController.logs(),
title: const Text('错误日志'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
],
),
),
@@ -260,4 +265,9 @@ class AboutController extends GetxController {
mode: LaunchMode.externalApplication,
);
}
// 日志
logs() {
Get.toNamed('/logs');
}
}

View File

@@ -35,6 +35,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
late double opacityVal;
late double fontSizeVal;
late double danmakuDurationVal;
late double strokeWidth;
int latestAddedPosition = -1;
@override
@@ -65,6 +66,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
showArea = playerController.showArea;
opacityVal = playerController.opacityVal;
fontSizeVal = playerController.fontSizeVal;
strokeWidth = playerController.strokeWidth;
danmakuDurationVal = playerController.danmakuDurationVal;
}
@@ -136,6 +138,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
hideBottom: blockTypes.contains(4),
duration:
danmakuDurationVal / playerController.playbackSpeed,
strokeWidth: strokeWidth,
// initDuration /
// (danmakuSpeedVal * widget.playerController.playbackSpeed),
),

View File

@@ -1,5 +1,6 @@
// 内容
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart';
@@ -80,7 +81,7 @@ class _ContentState extends State<Content> {
height: height,
),
),
height > maxHeight
height > Get.size.height * 0.9
? const PBadge(
text: '长图',
right: 8,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
@@ -87,7 +88,7 @@ Widget picWidget(item, context) {
childAspectRatio: aspectRatio,
children: list,
),
if (len == 1 && origAspectRatio < 0.4)
if (len == 1 && height > Get.size.height * 0.9)
const PBadge(
text: '长图',
top: null,

View File

@@ -63,13 +63,16 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
}
void setTabConfig() async {
defaultTabs = tabsConfig;
defaultTabs = [...tabsConfig];
tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort,
defaultValue: ['live', 'rcmd', 'hot', 'bangumi']);
defaultTabs.retainWhere(
(item) => tabbarSort.contains((item['type'] as TabType).id));
defaultTabs.sort((a, b) => tabbarSort
.indexOf((a['type'] as TabType).id)
.compareTo(tabbarSort.indexOf((b['type'] as TabType).id)));
tabs.value = defaultTabs
.where((i) => tabbarSort.contains((i['type'] as TabType).id))
.toList();
tabs.value = defaultTabs;
if (tabbarSort.contains(TabType.rcmd.id)) {
initialIndex.value = tabbarSort.indexOf(TabType.rcmd.id);

View File

@@ -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) {

View File

@@ -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'],

View File

@@ -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,
),

View File

@@ -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(

View File

@@ -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('设置成功');
}
}
}

View File

@@ -22,6 +22,17 @@ class _TabbarSetPageState extends State<TabbarSetPage> {
defaultTabs = tabsConfig;
tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort,
defaultValue: ['live', 'rcmd', 'hot', 'bangumi']);
// 对 tabData 进行排序
defaultTabs.sort((a, b) {
int indexA = tabbarSort.indexOf((a['type'] as TabType).id);
int indexB = tabbarSort.indexOf((b['type'] as TabType).id);
// 如果类型在 sortOrder 中不存在,则放在末尾
if (indexA == -1) indexA = tabbarSort.length;
if (indexB == -1) indexB = tabbarSort.length;
return indexA.compareTo(indexB);
});
}
void saveEdit() {

View File

@@ -0,0 +1,201 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../services/loggeer.dart';
class LogsPage extends StatefulWidget {
const LogsPage({super.key});
@override
State<LogsPage> createState() => _LogsPageState();
}
class _LogsPageState extends State<LogsPage> {
late File logsPath;
late String fileContent;
List logsContent = [];
@override
void initState() {
getPath();
super.initState();
}
void getPath() async {
logsPath = await getLogsPath();
fileContent = await logsPath.readAsString();
logsContent = await parseLogs(fileContent);
setState(() {});
}
Future<List<Map<String, dynamic>>> parseLogs(String fileContent) async {
const String splitToken =
'======================================================================';
List contentList = fileContent.split(splitToken).map((item) {
return item
.replaceAll(
'============================== CATCHER 2 LOG ==============================',
'Pilipala错误日志 \n ********************')
.replaceAll('DEVICE INFO', '设备信息')
.replaceAll('APP INFO', '应用信息')
.replaceAll('ERROR', '错误信息')
.replaceAll('STACK TRACE', '错误堆栈');
}).toList();
List<Map<String, dynamic>> result = [];
for (String i in contentList) {
DateTime? date;
String body = i
.split("\n")
.map((l) {
if (l.startsWith("Crash occurred on")) {
date = DateTime.parse(
l.split("Crash occurred on")[1].trim().split('.')[0],
);
return "";
}
return l;
})
.where((dynamic l) => l.replaceAll("\n", "").trim().isNotEmpty)
.join("\n");
if (date != null || body != '') {
result.add({'date': date, 'body': body, 'expand': false});
}
}
return result.reversed.toList();
}
void copyLogs() async {
await Clipboard.setData(ClipboardData(text: fileContent));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('复制成功')),
);
}
}
void feedback() {
launchUrl(
Uri.parse('https://github.com/guozhigq/pilipala/issues'),
// 系统自带浏览器打开
mode: LaunchMode.externalApplication,
);
}
void clearLogsHandle() async {
if (await clearLogs()) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已清空')),
);
logsContent = [];
setState(() {});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text('日志', style: Theme.of(context).textTheme.titleMedium),
actions: [
PopupMenuButton<String>(
onSelected: (String type) {
// 处理菜单项选择的逻辑
switch (type) {
case 'copy':
copyLogs();
break;
case 'feedback':
feedback();
break;
case 'clear':
clearLogsHandle();
break;
default:
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'copy',
child: Text('复制日志'),
),
const PopupMenuItem<String>(
value: 'feedback',
child: Text('错误反馈'),
),
const PopupMenuItem<String>(
value: 'clear',
child: Text('清空日志'),
),
],
),
const SizedBox(width: 6),
],
),
body: logsContent.isNotEmpty
? ListView.builder(
itemCount: logsContent.length,
itemBuilder: (context, index) {
final log = logsContent[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
log['date'].toString(),
style: Theme.of(context).textTheme.titleMedium,
),
),
TextButton.icon(
onPressed: () async {
await Clipboard.setData(
ClipboardData(text: log['body']),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'已将 ${log['date'].toString()} 复制至剪贴板',
),
),
);
}
},
icon: const Icon(Icons.copy_outlined, size: 16),
label: const Text('复制'),
)
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
elevation: 1,
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: SelectableText(log['body']),
),
),
),
const Divider(indent: 12, endIndent: 12),
],
);
},
)
: const CustomScrollView(
slivers: <Widget>[
NoData(),
],
),
);
}
}

View File

@@ -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'),

View File

@@ -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;
@@ -570,10 +576,12 @@ class VideoIntroController extends GetxController {
cid: lastPlayCid.value,
upMid: videoDetail.value.owner!.mid!,
);
SmartDialog.dismiss();
if (res['status']) {
modelResult = res['data'].modelResult;
} else {
SmartDialog.showToast("当前视频可能暂不支持AI视频总结");
}
SmartDialog.dismiss();
return res;
}
}

View File

@@ -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 = [];

View File

@@ -256,7 +256,12 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
// 请求错误
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
fn: () {
setState(() {
_futureBuilderFuture =
_videoReplyController.queryReplyList();
});
},
);
}
} else {

View File

@@ -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';
@@ -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('&amp;')) {
content.message = content.message.replaceAll('&amp;', '&');
}
// 匹配表情
content.message.splitMapJoin(
RegExp(r"\[.*?\]"),
onMatch: (Match match) {
final String matchStr = match[0]!;
if (content.emote.isNotEmpty &&
matchStr.indexOf('[') == matchStr.lastIndexOf('[') &&
matchStr.indexOf(']') == matchStr.lastIndexOf(']')) {
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(
text: matchStr,
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"@.*( |:)");
// }
content.message = content.message
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&nbsp;', ' ');
// 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('*', '\\*')),
];
// 只@用户没有内容
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},
);
},
),
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(
pattern,
onMatch: (Match match) {
String matchStr = match[0]!;
if (content.emote.containsKey(matchStr)) {
// 处理表情
final int size = content.emote[matchStr]['meta']['size'];
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 = () {
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,
spanChilds.addAll(
[
if (content.jumpUrl[matchStr]?['prefix_icon'] != null) ...[
WidgetSpan(
child: Image.network(
content.jumpUrl[matchStr]['prefix_icon'],
height: 19,
color: Theme.of(context).colorScheme.primary,
),
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 {
)
],
TextSpan(
text: content.jumpUrl[matchStr]['title'],
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
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(
'/webview',
parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': ''
},
'/searchResult',
parameters: {'keyword': matchRes['BV']},
);
}
} 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 ',
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)),
Get.toNamed(
'/webview',
parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': ''
},
);
} catch (_) {}
},
),
}
} 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)));
// 只显示一次
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(
@@ -880,7 +782,7 @@ InlineSpan buildContent(
height: height,
),
),
height > maxHeight
height > Get.size.height * 0.9
? const PBadge(
text: '长图',
right: 8,

View File

@@ -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;
},

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
@@ -724,6 +725,8 @@ class _HeaderControlState extends State<HeaderControl> {
double fontSizeVal = widget.controller!.fontSizeVal;
// 弹幕速度
double danmakuDurationVal = widget.controller!.danmakuDurationVal;
// 弹幕描边
double strokeWidth = widget.controller!.strokeWidth;
final DanmakuController danmakuController =
widget.controller!.danmakuController!;
@@ -857,6 +860,44 @@ class _HeaderControlState extends State<HeaderControl> {
),
),
),
Text('描边粗细 $strokeWidth'),
Padding(
padding: const EdgeInsets.only(
top: 0,
bottom: 6,
left: 10,
right: 10,
),
child: SliderTheme(
data: SliderThemeData(
trackShape: MSliderTrackShape(),
thumbColor: Theme.of(context).colorScheme.primary,
activeTrackColor: Theme.of(context).colorScheme.primary,
trackHeight: 10,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 6.0),
),
child: Slider(
min: 0,
max: 3,
value: strokeWidth,
divisions: 6,
label: '$strokeWidth',
onChanged: (double val) {
strokeWidth = val;
widget.controller!.strokeWidth = val;
setState(() {});
try {
final DanmakuOption currentOption =
danmakuController.option;
final DanmakuOption updatedOption =
currentOption.copyWith(strokeWidth: val);
danmakuController.updateOption(updatedOption);
} catch (_) {}
},
),
),
),
Text('字体大小 ${(fontSizeVal * 100).toStringAsFixed(1)}%'),
Padding(
padding: const EdgeInsets.only(

View File

@@ -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;
}

View File

@@ -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);
}
},
),

View File

@@ -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,