Merge branch 'main' into design

This commit is contained in:
guozhigq
2024-10-12 17:51:24 +08:00
13 changed files with 250 additions and 153 deletions

View File

@ -12,7 +12,6 @@ on:
- ".idea/**" - ".idea/**"
- "!.github/workflows/**" - "!.github/workflows/**"
jobs: jobs:
update_version: update_version:
name: Read and update version name: Read and update version
@ -96,7 +95,7 @@ jobs:
if: steps.cache-flutter.outputs.cache-hit != 'true' if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: 3.16.5 flutter-version: 3.19.6
channel: any channel: any
- name: 下载项目依赖 - name: 下载项目依赖

View File

@ -36,7 +36,7 @@ jobs:
if: steps.cache-flutter.outputs.cache-hit != 'true' if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: 3.16.5 flutter-version: 3.19.6
channel: any channel: any
- name: 下载项目依赖 - name: 下载项目依赖
@ -98,7 +98,7 @@ jobs:
uses: subosito/flutter-action@v2.10.0 uses: subosito/flutter-action@v2.10.0
with: with:
cache: true cache: true
flutter-version: 3.16.5 flutter-version: 3.19.6
- name: flutter build ipa - name: flutter build ipa
run: | run: |

39
change_log/1.0.25.1010.md Normal file
View File

@ -0,0 +1,39 @@
## 1.0.25
### 功能
+ 直播弹幕
+ 稍后再看、收藏夹播放全部
+ 收藏夹新建、编辑
+ 评论删除
+ 评论保存为图片
+ 动态页滑动切换up
+ up投稿筛选充电视频
+ 直播tab展示关注up
+ up主页专栏展示
### 优化
+ 视频详情页一键三连
+ 动态页标识充电视频
+ 播放器亮度、音量调整百分比展示
+ 封面预览时视频标题可复制
+ 竖屏直播布局
+ 图片预览
+ 专栏渲染优化
+ 私信图片查看
### 修复
+ 收藏夹点击异常
+ 搜索up异常
+ 系统通知已读异常
+ [赞了我的]展示错误
+ 部分up合集无法打开
+ 切换合集视频投币个数未重置
+ 搜索条件筛选面板无法滚动
+ 部分机型导航条未沉浸
+ 专栏图片渲染问题
+ 专栏浏览历史记录
+ 直播间历史记录
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -33,7 +33,11 @@ class NetworkImgLayer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final int defaultImgQuality = GlobalDataCache().imgQuality; int defaultImgQuality = 10;
try {
defaultImgQuality = GlobalDataCache().imgQuality;
} catch (_) {}
if (src == '' || src == null) { if (src == '' || src == null) {
return placeholder(context); return placeholder(context);
} }

View File

@ -536,7 +536,8 @@ class VideoHttp {
// 获取字幕内容 // 获取字幕内容
static Future<Map<String, dynamic>> getSubtitleContent(url) async { static Future<Map<String, dynamic>> getSubtitleContent(url) async {
var res = await Request().get('https:$url'); var res = await Request().get('https:$url');
final String content = SubTitleUtils.convertToWebVTT(res.data['body']); final String content =
await SubTitleUtils.convertToWebVTT(res.data['body']);
final List body = res.data['body']; final List body = res.data['body'];
return {'content': content, 'body': body}; return {'content': content, 'body': body};
} }

View File

@ -1,6 +1,7 @@
class MediaVideoItemModel { class MediaVideoItemModel {
MediaVideoItemModel({ MediaVideoItemModel({
this.id, this.id,
this.aid,
this.offset, this.offset,
this.index, this.index,
this.intro, this.intro,
@ -14,12 +15,13 @@ class MediaVideoItemModel {
this.likeState, this.likeState,
this.favState, this.favState,
this.page, this.page,
this.cid,
this.pages, this.pages,
this.title, this.title,
this.type, this.type,
this.upper, this.upper,
this.link, this.link,
this.bvId, this.bvid,
this.shortLink, this.shortLink,
this.rights, this.rights,
this.elecInfo, this.elecInfo,
@ -32,6 +34,7 @@ class MediaVideoItemModel {
}); });
int? id; int? id;
int? aid;
int? offset; int? offset;
int? index; int? index;
String? intro; String? intro;
@ -45,12 +48,13 @@ class MediaVideoItemModel {
int? likeState; int? likeState;
int? favState; int? favState;
int? page; int? page;
int? cid;
List<Page>? pages; List<Page>? pages;
String? title; String? title;
int? type; int? type;
Upper? upper; Upper? upper;
String? link; String? link;
String? bvId; String? bvid;
String? shortLink; String? shortLink;
Rights? rights; Rights? rights;
dynamic elecInfo; dynamic elecInfo;
@ -64,6 +68,7 @@ class MediaVideoItemModel {
factory MediaVideoItemModel.fromJson(Map<String, dynamic> json) => factory MediaVideoItemModel.fromJson(Map<String, dynamic> json) =>
MediaVideoItemModel( MediaVideoItemModel(
id: json["id"], id: json["id"],
aid: json["id"],
offset: json["offset"], offset: json["offset"],
index: json["index"], index: json["index"],
intro: json["intro"], intro: json["intro"],
@ -77,6 +82,7 @@ class MediaVideoItemModel {
likeState: json["like_state"], likeState: json["like_state"],
favState: json["fav_state"], favState: json["fav_state"],
page: json["page"], page: json["page"],
cid: json["pages"] == null ? -1 : json["pages"].first['id'],
// json["pages"] 可能为null // json["pages"] 可能为null
pages: json["pages"] == null pages: json["pages"] == null
? [] ? []
@ -85,7 +91,7 @@ class MediaVideoItemModel {
type: json["type"], type: json["type"],
upper: Upper.fromJson(json["upper"]), upper: Upper.fromJson(json["upper"]),
link: json["link"], link: json["link"],
bvId: json["bv_id"], bvid: json["bv_id"],
shortLink: json["short_link"], shortLink: json["short_link"],
rights: Rights.fromJson(json["rights"]), rights: Rights.fromJson(json["rights"]),
elecInfo: json["elec_info"], elecInfo: json["elec_info"],

View File

@ -64,7 +64,7 @@ class LiveRoomController extends GetxController {
? liveItem.pic ? liveItem.pic
: (liveItem.cover != null && liveItem.cover != '') : (liveItem.cover != null && liveItem.cover != '')
? liveItem.cover ? liveItem.cover
: null; : '';
} }
Request.getBuvid().then((value) => buvid = value); Request.getBuvid().then((value) => buvid = value);
} }

View File

@ -108,6 +108,12 @@ class _LiveRoomPageState extends State<LiveRoomPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final isPortrait = mediaQuery.orientation == Orientation.portrait;
final isLandscape = mediaQuery.orientation == Orientation.landscape;
final padding = mediaQuery.padding;
Widget videoPlayerPanel = FutureBuilder( Widget videoPlayerPanel = FutureBuilder(
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) { builder: (BuildContext context, AsyncSnapshot snapshot) {
@ -187,10 +193,8 @@ class _LiveRoomPageState extends State<LiveRoomPage>
children: [ children: [
Obx( Obx(
() => SizedBox( () => SizedBox(
height: MediaQuery.of(context).padding.top + height: padding.top +
(_liveRoomController.isPortrait.value || (_liveRoomController.isPortrait.value || isLandscape
MediaQuery.of(context).orientation ==
Orientation.landscape
? 0 ? 0
: kToolbarHeight), : kToolbarHeight),
), ),
@ -201,21 +205,18 @@ class _LiveRoomPageState extends State<LiveRoomPage>
if (plPlayerController.isFullScreen.value == true) { if (plPlayerController.isFullScreen.value == true) {
plPlayerController.triggerFullScreen(status: false); plPlayerController.triggerFullScreen(status: false);
} }
if (MediaQuery.of(context).orientation == if (isLandscape) {
Orientation.landscape) {
verticalScreen(); verticalScreen();
} }
}, },
child: Obx( child: Obx(
() => Container( () => Container(
width: Get.size.width, width: Get.size.width,
height: MediaQuery.of(context).orientation == height: isLandscape
Orientation.landscape
? Get.size.height ? Get.size.height
: !_liveRoomController.isPortrait.value : !_liveRoomController.isPortrait.value
? Get.size.width * 9 / 16 ? Get.size.width * 9 / 16
: Get.size.height - : Get.size.height - padding.top,
MediaQuery.of(context).padding.top,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration( decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(6)), borderRadius: BorderRadius.all(Radius.circular(6)),
@ -229,7 +230,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
// 定位 快速滑动到底部 // 定位 快速滑动到底部
Positioned( Positioned(
right: 20, right: 20,
bottom: MediaQuery.of(context).padding.bottom + 80, bottom: padding.bottom + 80,
child: SlideTransition( child: SlideTransition(
position: Tween<Offset>( position: Tween<Offset>(
begin: const Offset(0, 4), begin: const Offset(0, 4),
@ -262,10 +263,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
titleSpacing: 0, titleSpacing: 0,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
foregroundColor: Colors.white, foregroundColor: Colors.white,
toolbarHeight: toolbarHeight: isPortrait ? 56 : 0,
MediaQuery.of(context).orientation == Orientation.portrait
? 56
: 0,
title: FutureBuilder( title: FutureBuilder(
future: _futureBuilder, future: _futureBuilder,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -317,15 +315,20 @@ class _LiveRoomPageState extends State<LiveRoomPage>
), ),
// 消息列表 // 消息列表
Obx( Obx(
() => Positioned( () => Align(
top: MediaQuery.of(context).padding.top + alignment: Alignment.bottomCenter,
child: Container(
margin: EdgeInsets.only(
bottom: 90 + padding.bottom,
),
height: Get.size.height -
(padding.top +
kToolbarHeight + kToolbarHeight +
(_liveRoomController.isPortrait.value (_liveRoomController.isPortrait.value
? Get.size.width ? Get.size.width
: Get.size.width * 9 / 16), : Get.size.width * 9 / 16) +
bottom: 90 + MediaQuery.of(context).padding.bottom, 100 +
left: 0, padding.bottom),
right: 0,
child: buildMessageListUI( child: buildMessageListUI(
context, context,
_liveRoomController, _liveRoomController,
@ -333,19 +336,17 @@ class _LiveRoomPageState extends State<LiveRoomPage>
), ),
), ),
), ),
),
// 消息输入框 // 消息输入框
Visibility( Visibility(
visible: MediaQuery.of(context).orientation == Orientation.portrait, visible: isPortrait,
child: Positioned( child: Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
child: Container( child: Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 14, left: 14, right: 14, top: 4, bottom: padding.bottom + 20),
right: 14,
top: 4,
bottom: MediaQuery.of(context).padding.bottom + 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.1), color: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(20)), borderRadius: const BorderRadius.all(Radius.circular(20)),
@ -421,6 +422,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
], ],
), ),
); );
if (Platform.isAndroid) { if (Platform.isAndroid) {
return PiPSwitcher( return PiPSwitcher(
childWhenDisabled: childWhenDisabled, childWhenDisabled: childWhenDisabled,
@ -438,8 +440,7 @@ Widget buildMessageListUI(
LiveRoomController liveRoomController, LiveRoomController liveRoomController,
ScrollController scrollController, ScrollController scrollController,
) { ) {
return Expanded( return Obx(
child: Obx(
() => MediaQuery.removePadding( () => MediaQuery.removePadding(
context: context, context: context,
removeTop: true, removeTop: true,
@ -519,7 +520,6 @@ Widget buildMessageListUI(
), ),
), ),
), ),
),
); );
} }

View File

@ -24,8 +24,8 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
final MainController _mainController = Get.put(MainController()); final MainController _mainController = Get.put(MainController());
late HomeController _homeController; late HomeController _homeController;
RankController? _rankController; RankController? _rankController;
DynamicsController? _dynamicController; late DynamicsController _dynamicController;
MediaController? _mediaController; late MediaController _mediaController;
int? _lastSelectTime; //上次点击时间 int? _lastSelectTime; //上次点击时间
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
@ -76,28 +76,30 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
} }
if (currentPage is DynamicsPage) { if (currentPage is DynamicsPage) {
if (_dynamicController!.flag) { if (_dynamicController.flag) {
// 单击返回顶部 双击并刷新 // 单击返回顶部 双击并刷新
if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) { if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {
_dynamicController!.onRefresh(); _dynamicController.onRefresh();
} else { } else {
_dynamicController!.animateToTop(); _dynamicController.animateToTop();
} }
_lastSelectTime = DateTime.now().millisecondsSinceEpoch; _lastSelectTime = DateTime.now().millisecondsSinceEpoch;
} }
_dynamicController!.flag = true; _dynamicController.flag = true;
_mainController.clearUnread(); _mainController.clearUnread();
} else { } else {
_dynamicController?.flag = false; _dynamicController.flag = false;
} }
if (currentPage is MediaPage) { if (currentPage is MediaPage) {
_mediaController!.queryFavFolder(); _mediaController.queryFavFolder();
} }
} }
void controllerInit() { void controllerInit() {
_homeController = Get.put(HomeController()); _homeController = Get.put(HomeController());
_dynamicController = Get.put(DynamicsController());
_mediaController = Get.put(MediaController());
if (_mainController.pagesIds.contains(1)) { if (_mainController.pagesIds.contains(1)) {
_rankController = Get.put(RankController()); _rankController = Get.put(RankController());
} }

View File

@ -411,7 +411,12 @@ class VideoIntroController extends GetxController {
} }
// 修改分P或番剧分集 // 修改分P或番剧分集
Future changeSeasonOrbangu(bvid, cid, aid, cover) async { Future changeSeasonOrbangu(
String bvid,
int cid,
int? aid,
String? cover,
) async {
// 重新获取视频资源 // 重新获取视频资源
final VideoDetailController videoDetailCtr = final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: heroTag);
@ -422,13 +427,14 @@ class VideoIntroController extends GetxController {
releatedCtr.queryRelatedVideo(); releatedCtr.queryRelatedVideo();
} }
videoDetailCtr.bvid = bvid; videoDetailCtr
videoDetailCtr.oid.value = aid ?? IdUtils.bv2av(bvid); ..bvid = bvid
videoDetailCtr.cid.value = cid; ..oid.value = aid ?? IdUtils.bv2av(bvid)
videoDetailCtr.danmakuCid.value = cid; ..cid.value = cid
videoDetailCtr.cover.value = cover; ..danmakuCid.value = cid
videoDetailCtr.queryVideoUrl(); ..cover.value = cover ?? ''
videoDetailCtr.clearSubtitleContent(); ..queryVideoUrl()
..clearSubtitleContent();
await videoDetailCtr.getSubtitle(); await videoDetailCtr.getSubtitle();
videoDetailCtr.setSubtitleContent(); videoDetailCtr.setSubtitleContent();
// 重新请求评论 // 重新请求评论
@ -478,7 +484,13 @@ class VideoIntroController extends GetxController {
final List episodes = []; final List episodes = [];
bool isPages = false; bool isPages = false;
late String cover; late String cover;
if (videoDetail.value.ugcSeason != null) { final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
/// 优先稍后再看、收藏夹
if (videoDetailCtr.isWatchLaterVisible.value) {
episodes.addAll(videoDetailCtr.mediaList);
} else if (videoDetail.value.ugcSeason != null) {
final UgcSeason ugcSeason = videoDetail.value.ugcSeason!; final UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
final List<SectionItem> sections = ugcSeason.sections!; final List<SectionItem> sections = ugcSeason.sections!;
for (int i = 0; i < sections.length; i++) { for (int i = 0; i < sections.length; i++) {
@ -495,10 +507,15 @@ class VideoIntroController extends GetxController {
episodes.indexWhere((e) => e.cid == lastPlayCid.value); episodes.indexWhere((e) => e.cid == lastPlayCid.value);
int nextIndex = currentIndex + 1; int nextIndex = currentIndex + 1;
cover = episodes[nextIndex].cover; cover = episodes[nextIndex].cover;
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
final PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat; final PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
int cid = episodes[nextIndex].cid!;
while (cid == -1) {
nextIndex += 1;
SmartDialog.showToast('当前视频暂不支持播放,自动跳过');
cid = episodes[nextIndex].cid!;
}
// 列表循环 // 列表循环
if (nextIndex >= episodes.length) { if (nextIndex >= episodes.length) {
if (platRepeat == PlayRepeat.listCycle) { if (platRepeat == PlayRepeat.listCycle) {
@ -508,7 +525,6 @@ class VideoIntroController extends GetxController {
return; return;
} }
} }
final int cid = episodes[nextIndex].cid!;
final String rBvid = isPages ? bvid : episodes[nextIndex].bvid; final String rBvid = isPages ? bvid : episodes[nextIndex].bvid;
final int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!; final int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!;
changeSeasonOrbangu(rBvid, cid, rAid, cover); changeSeasonOrbangu(rBvid, cid, rAid, cover);

View File

@ -109,7 +109,7 @@ class _MediaListPanelState extends State<MediaListPanel> {
var item = mediaList[index]; var item = mediaList[index];
return InkWell( return InkWell(
onTap: () async { onTap: () async {
String bvid = item.bvId!; String bvid = item.bvid!;
int? aid = item.id; int? aid = item.id;
String cover = item.cover ?? ''; String cover = item.cover ?? '';
final int cid = final int cid =
@ -173,7 +173,7 @@ class _MediaListPanelState extends State<MediaListPanel> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: item.bvId == widget.bvid color: item.bvid == widget.bvid
? Theme.of(context) ? Theme.of(context)
.colorScheme .colorScheme
.primary .primary

View File

@ -1,9 +1,37 @@
import 'dart:async';
import 'dart:isolate';
class SubTitleUtils { class SubTitleUtils {
// 格式整理 // 格式整理
static String convertToWebVTT(List jsonData) { static Future<String> convertToWebVTT(List jsonData) async {
String webVTTContent = 'WEBVTT FILE\n\n'; final receivePort = ReceivePort();
await Isolate.spawn(_convertToWebVTTIsolate, receivePort.sendPort);
for (int i = 0; i < jsonData.length; i++) { final sendPort = await receivePort.first as SendPort;
final response = ReceivePort();
sendPort.send([jsonData, response.sendPort]);
return await response.first as String;
}
static void _convertToWebVTTIsolate(SendPort sendPort) async {
final port = ReceivePort();
sendPort.send(port.sendPort);
await for (final message in port) {
final List jsonData = message[0];
final SendPort replyTo = message[1];
String webVTTContent = 'WEBVTT FILE\n\n';
int chunkSize = 100; // 每次处理100条数据
int totalChunks = (jsonData.length / chunkSize).ceil();
for (int chunk = 0; chunk < totalChunks; chunk++) {
int start = chunk * chunkSize;
int end = start + chunkSize;
if (end > jsonData.length) end = jsonData.length;
for (int i = start; i < end; i++) {
final item = jsonData[i]; final item = jsonData[i];
double from = double.parse(item['from'].toString()); double from = double.parse(item['from'].toString());
double to = double.parse(item['to'].toString()); double to = double.parse(item['to'].toString());
@ -14,8 +42,10 @@ class SubTitleUtils {
webVTTContent += '${formatTime(from)} --> ${formatTime(to)}\n'; webVTTContent += '${formatTime(from)} --> ${formatTime(to)}\n';
webVTTContent += '$content\n\n'; webVTTContent += '$content\n\n';
} }
}
return webVTTContent; replyTo.send(webVTTContent);
}
} }
static String formatTime(num seconds) { static String formatTime(num seconds) {

View File

@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.24+1024 version: 1.0.25+1025
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"