Merge branch 'main' into fix
This commit is contained in:
@ -1,7 +1,9 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';
|
||||||
import 'network_img_layer.dart';
|
import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';
|
||||||
|
import 'package:pilipala/utils/highlight.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class HtmlRender extends StatelessWidget {
|
class HtmlRender extends StatelessWidget {
|
||||||
@ -22,6 +24,20 @@ class HtmlRender extends StatelessWidget {
|
|||||||
data: htmlContent,
|
data: htmlContent,
|
||||||
onLinkTap: (String? url, Map<String, String> buildContext, attributes) {},
|
onLinkTap: (String? url, Map<String, String> buildContext, attributes) {},
|
||||||
extensions: [
|
extensions: [
|
||||||
|
TagExtension(
|
||||||
|
tagsToExtend: <String>{'pre'},
|
||||||
|
builder: (ExtensionContext extensionContext) {
|
||||||
|
final Map<String, dynamic> attributes = extensionContext.attributes;
|
||||||
|
final String lang = attributes['data-lang'] as String;
|
||||||
|
final String code = attributes['codecontent'] as String;
|
||||||
|
List<String> selectedLanguages = [lang.split('@').first];
|
||||||
|
TextSpan? result = highlightExistingText(code, selectedLanguages);
|
||||||
|
if (result == null) {
|
||||||
|
return const Center(child: Text('代码块渲染失败'));
|
||||||
|
}
|
||||||
|
return SelectableText.rich(result);
|
||||||
|
},
|
||||||
|
),
|
||||||
TagExtension(
|
TagExtension(
|
||||||
tagsToExtend: <String>{'img'},
|
tagsToExtend: <String>{'img'},
|
||||||
builder: (ExtensionContext extensionContext) {
|
builder: (ExtensionContext extensionContext) {
|
||||||
@ -44,20 +60,52 @@ class HtmlRender extends StatelessWidget {
|
|||||||
if (isMall) {
|
if (isMall) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
// bool inTable =
|
return InkWell(
|
||||||
// extensionContext.element!.previousElementSibling == null ||
|
onTap: () {
|
||||||
// extensionContext.element!.nextElementSibling == null;
|
Navigator.of(context).push(
|
||||||
// imgUrl = Utils().imageUrl(imgUrl!);
|
HeroDialogRoute<void>(
|
||||||
// return Image.network(
|
builder: (BuildContext context) =>
|
||||||
// imgUrl,
|
InteractiveviewerGallery(
|
||||||
// width: isEmote ? 22 : null,
|
sources: imgList ?? [imgUrl],
|
||||||
// height: isEmote ? 22 : null,
|
initIndex: imgList?.indexOf(imgUrl) ?? 0,
|
||||||
// );
|
itemBuilder: (
|
||||||
return NetworkImgLayer(
|
BuildContext context,
|
||||||
width: isEmote ? 22 : Get.size.width - 24,
|
int index,
|
||||||
height: isEmote ? 22 : 200,
|
bool isFocus,
|
||||||
src: imgUrl,
|
bool enablePageView,
|
||||||
|
) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
if (enablePageView) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Center(
|
||||||
|
child: Hero(
|
||||||
|
tag: imgList?[index] ?? imgUrl,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
fadeInDuration:
|
||||||
|
const Duration(milliseconds: 0),
|
||||||
|
imageUrl: imgList?[index] ?? imgUrl,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
onPageChanged: (int pageIndex) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: CachedNetworkImage(imageUrl: imgUrl),
|
||||||
|
);
|
||||||
|
// return NetworkImgLayer(
|
||||||
|
// width: isEmote ? 22 : Get.size.width - 24,
|
||||||
|
// height: isEmote ? 22 : 200,
|
||||||
|
// src: imgUrl,
|
||||||
|
// );
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
@ -66,7 +114,7 @@ class HtmlRender extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
style: {
|
style: {
|
||||||
'html': Style(
|
'html': Style(
|
||||||
fontSize: FontSize.medium,
|
fontSize: FontSize.large,
|
||||||
lineHeight: LineHeight.percent(140),
|
lineHeight: LineHeight.percent(140),
|
||||||
),
|
),
|
||||||
'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero),
|
'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero),
|
||||||
@ -78,7 +126,7 @@ class HtmlRender extends StatelessWidget {
|
|||||||
margin: Margins.only(bottom: 10),
|
margin: Margins.only(bottom: 10),
|
||||||
),
|
),
|
||||||
'span': Style(
|
'span': Style(
|
||||||
fontSize: FontSize.medium,
|
fontSize: FontSize.large,
|
||||||
height: Height(1.65),
|
height: Height(1.65),
|
||||||
),
|
),
|
||||||
'div': Style(height: Height.auto()),
|
'div': Style(height: Height.auto()),
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/utils/extension.dart';
|
import 'package:pilipala/utils/extension.dart';
|
||||||
import 'package:pilipala/utils/global_data.dart';
|
import 'package:pilipala/utils/global_data_cache.dart';
|
||||||
import '../../utils/storage.dart';
|
import '../../utils/storage.dart';
|
||||||
import '../constants.dart';
|
import '../constants.dart';
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ class NetworkImgLayer extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final int defaultImgQuality = GlobalData().imgQuality;
|
final int defaultImgQuality = GlobalDataCache().imgQuality;
|
||||||
if (src == '' || src == null) {
|
if (src == '' || src == null) {
|
||||||
return placeholder(context);
|
return placeholder(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ class StatDanMu extends StatelessWidget {
|
|||||||
final dynamic danmu;
|
final dynamic danmu;
|
||||||
final String? size;
|
final String? size;
|
||||||
|
|
||||||
const StatDanMu({Key? key, this.theme, this.danmu, this.size})
|
const StatDanMu({Key? key, this.theme = 'gray', this.danmu, this.size})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -17,21 +17,46 @@ class StatDanMu extends StatelessWidget {
|
|||||||
'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||||
};
|
};
|
||||||
Color color = colorObject[theme]!;
|
Color color = colorObject[theme]!;
|
||||||
|
return StatIconText(
|
||||||
|
icon: Icons.subtitles_outlined,
|
||||||
|
text: Utils.numFormat(danmu!),
|
||||||
|
color: color,
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatIconText extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String text;
|
||||||
|
final Color color;
|
||||||
|
final String? size;
|
||||||
|
|
||||||
|
const StatIconText({
|
||||||
|
Key? key,
|
||||||
|
required this.icon,
|
||||||
|
required this.text,
|
||||||
|
required this.color,
|
||||||
|
this.size,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.subtitles_outlined,
|
icon,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: color,
|
color: color,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 2),
|
const SizedBox(width: 2),
|
||||||
Text(
|
Text(
|
||||||
Utils.numFormat(danmu!),
|
text,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: size == 'medium' ? 12 : 11,
|
fontSize: size == 'medium' ? 12 : 11,
|
||||||
color: color,
|
color: color,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,12 @@ class StatView extends StatelessWidget {
|
|||||||
final dynamic view;
|
final dynamic view;
|
||||||
final String? size;
|
final String? size;
|
||||||
|
|
||||||
const StatView({Key? key, this.theme, this.view, this.size})
|
const StatView({
|
||||||
: super(key: key);
|
Key? key,
|
||||||
|
this.theme = 'gray',
|
||||||
|
this.view,
|
||||||
|
this.size,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -17,16 +21,41 @@ class StatView extends StatelessWidget {
|
|||||||
'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||||
};
|
};
|
||||||
Color color = colorObject[theme]!;
|
Color color = colorObject[theme]!;
|
||||||
|
return StatIconText(
|
||||||
|
icon: Icons.play_circle_outlined,
|
||||||
|
text: Utils.numFormat(view!),
|
||||||
|
color: color,
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatIconText extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String text;
|
||||||
|
final Color color;
|
||||||
|
final String? size;
|
||||||
|
|
||||||
|
const StatIconText({
|
||||||
|
Key? key,
|
||||||
|
required this.icon,
|
||||||
|
required this.text,
|
||||||
|
required this.color,
|
||||||
|
this.size,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.play_circle_outlined,
|
icon,
|
||||||
size: 13,
|
size: 13,
|
||||||
color: color,
|
color: color,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 2),
|
const SizedBox(width: 2),
|
||||||
Text(
|
Text(
|
||||||
Utils.numFormat(view!),
|
text,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: size == 'medium' ? 12 : 11,
|
fontSize: size == 'medium' ? 12 : 11,
|
||||||
color: color,
|
color: color,
|
||||||
|
|||||||
@ -266,17 +266,11 @@ class VideoContent extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (showView) ...[
|
if (showView) ...[
|
||||||
StatView(
|
StatView(view: videoItem.stat.view as int),
|
||||||
theme: 'gray',
|
|
||||||
view: videoItem.stat.view as int,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
if (showDanmaku)
|
if (showDanmaku)
|
||||||
StatDanMu(
|
StatDanMu(danmu: videoItem.stat.danmaku as int),
|
||||||
theme: 'gray',
|
|
||||||
danmu: videoItem.stat.danmaku as int,
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (source == 'normal')
|
if (source == 'normal')
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|||||||
@ -60,17 +60,13 @@ class VideoCardV extends StatelessWidget {
|
|||||||
// 动态
|
// 动态
|
||||||
case 'picture':
|
case 'picture':
|
||||||
try {
|
try {
|
||||||
String dynamicType = 'picture';
|
|
||||||
String uri = videoItem.uri;
|
String uri = videoItem.uri;
|
||||||
String id = '';
|
|
||||||
if (videoItem.uri.startsWith('bilibili://article/')) {
|
if (videoItem.uri.startsWith('bilibili://article/')) {
|
||||||
// https://www.bilibili.com/read/cv27063554
|
// https://www.bilibili.com/read/cv27063554
|
||||||
dynamicType = 'read';
|
|
||||||
RegExp regex = RegExp(r'\d+');
|
RegExp regex = RegExp(r'\d+');
|
||||||
Match match = regex.firstMatch(videoItem.uri)!;
|
Match match = regex.firstMatch(videoItem.uri)!;
|
||||||
String matchedNumber = match.group(0)!;
|
String matchedNumber = match.group(0)!;
|
||||||
videoItem.param = int.parse(matchedNumber);
|
videoItem.param = int.parse(matchedNumber);
|
||||||
id = 'cv${videoItem.param}';
|
|
||||||
}
|
}
|
||||||
if (uri.startsWith('http')) {
|
if (uri.startsWith('http')) {
|
||||||
String path = Uri.parse(uri).path;
|
String path = Uri.parse(uri).path;
|
||||||
@ -88,11 +84,10 @@ class VideoCardV extends StatelessWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Get.toNamed('/htmlRender', parameters: {
|
Get.toNamed('/read', parameters: {
|
||||||
'url': uri,
|
|
||||||
'title': videoItem.title,
|
'title': videoItem.title,
|
||||||
'id': id,
|
'id': videoItem.param,
|
||||||
'dynamicType': dynamicType
|
'articleType': 'read'
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
SmartDialog.showToast(err.toString());
|
SmartDialog.showToast(err.toString());
|
||||||
@ -287,9 +282,9 @@ class VideoStat extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
StatView(theme: 'gray', view: videoItem.stat.view),
|
StatView(view: videoItem.stat.view),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
StatDanMu(theme: 'gray', danmu: videoItem.stat.danmu),
|
StatDanMu(danmu: videoItem.stat.danmu),
|
||||||
if (videoItem is RecVideoItemModel) ...<Widget>[
|
if (videoItem is RecVideoItemModel) ...<Widget>[
|
||||||
crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8),
|
crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8),
|
||||||
RichText(
|
RichText(
|
||||||
|
|||||||
@ -555,14 +555,41 @@ class Api {
|
|||||||
static const String messageSystemAPi =
|
static const String messageSystemAPi =
|
||||||
'${HttpString.messageBaseUrl}/x/sys-msg/query_unified_notify';
|
'${HttpString.messageBaseUrl}/x/sys-msg/query_unified_notify';
|
||||||
|
|
||||||
|
/// 系统通知 个人
|
||||||
|
static const String userMessageSystemAPi =
|
||||||
|
'${HttpString.messageBaseUrl}/x/sys-msg/query_user_notify';
|
||||||
|
|
||||||
/// 系统通知标记已读
|
/// 系统通知标记已读
|
||||||
static const String systemMarkRead =
|
static const String systemMarkRead =
|
||||||
'${HttpString.messageBaseUrl}/x/sys-msg/update_cursor';
|
'${HttpString.messageBaseUrl}/x/sys-msg/update_cursor';
|
||||||
|
|
||||||
|
/// 编辑收藏夹
|
||||||
|
static const String editFavFolder = '/x/v3/fav/folder/edit';
|
||||||
|
|
||||||
|
/// 新建收藏夹
|
||||||
|
static const String addFavFolder = '/x/v3/fav/folder/add';
|
||||||
|
|
||||||
/// 直播间弹幕信息
|
/// 直播间弹幕信息
|
||||||
static const String getDanmuInfo =
|
static const String getDanmuInfo =
|
||||||
'${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getDanmuInfo';
|
'${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getDanmuInfo';
|
||||||
|
|
||||||
/// 直播间发送弹幕
|
/// 直播间发送弹幕
|
||||||
static const String sendLiveMsg = '${HttpString.liveBaseUrl}/msg/send';
|
static const String sendLiveMsg = '${HttpString.liveBaseUrl}/msg/send';
|
||||||
|
|
||||||
|
/// 我的关注 - 正在直播
|
||||||
|
static const String getFollowingLive =
|
||||||
|
'${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following';
|
||||||
|
|
||||||
|
/// 稍后再看&收藏夹视频列表
|
||||||
|
static const String mediaList = '/x/v2/medialist/resource/list';
|
||||||
|
|
||||||
|
/// 用户专栏
|
||||||
|
static const String opusList = '/x/polymer/web-dynamic/v1/opus/feed/space';
|
||||||
|
|
||||||
|
///
|
||||||
|
static const String getViewInfo = '/x/article/viewinfo';
|
||||||
|
|
||||||
|
/// 直播间记录
|
||||||
|
static const String liveRoomEntry =
|
||||||
|
'${HttpString.liveBaseUrl}/xlive/web-room/v1/index/roomEntryAction';
|
||||||
}
|
}
|
||||||
|
|||||||
67
lib/http/fav.dart
Normal file
67
lib/http/fav.dart
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import 'index.dart';
|
||||||
|
|
||||||
|
class FavHttp {
|
||||||
|
/// 编辑收藏夹
|
||||||
|
static Future editFolder({
|
||||||
|
required String title,
|
||||||
|
required String intro,
|
||||||
|
required String mediaId,
|
||||||
|
String? cover,
|
||||||
|
int? privacy,
|
||||||
|
}) async {
|
||||||
|
var res = await Request().post(
|
||||||
|
Api.editFavFolder,
|
||||||
|
queryParameters: {
|
||||||
|
'title': title,
|
||||||
|
'intro': intro,
|
||||||
|
'media_id': mediaId,
|
||||||
|
'cover': cover ?? '',
|
||||||
|
'privacy': privacy ?? 0,
|
||||||
|
'csrf': await Request.getCsrf(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': res.data['data'],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 新建收藏夹
|
||||||
|
static Future addFolder({
|
||||||
|
required String title,
|
||||||
|
required String intro,
|
||||||
|
String? cover,
|
||||||
|
int? privacy,
|
||||||
|
}) async {
|
||||||
|
var res = await Request().post(
|
||||||
|
Api.addFavFolder,
|
||||||
|
queryParameters: {
|
||||||
|
'title': title,
|
||||||
|
'intro': intro,
|
||||||
|
'cover': cover ?? '',
|
||||||
|
'privacy': privacy ?? 0,
|
||||||
|
'csrf': await Request.getCsrf(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': res.data['data'],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:pilipala/models/live/follow.dart';
|
||||||
|
|
||||||
import '../models/live/item.dart';
|
import '../models/live/item.dart';
|
||||||
import '../models/live/room_info.dart';
|
import '../models/live/room_info.dart';
|
||||||
import '../models/live/room_info_h5.dart';
|
import '../models/live/room_info_h5.dart';
|
||||||
@ -117,4 +119,38 @@ class LiveHttp {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 我的关注 正在直播
|
||||||
|
static Future liveFollowing({int? pn, int? ps}) async {
|
||||||
|
var res = await Request().get(Api.getFollowingLive, data: {
|
||||||
|
'page': pn,
|
||||||
|
'page_size': ps,
|
||||||
|
'platform': 'web',
|
||||||
|
'ignoreRecord': 1,
|
||||||
|
'hit_ab': true,
|
||||||
|
});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': LiveFollowingModel.fromJson(res.data['data'])
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直播历史记录
|
||||||
|
static Future liveRoomEntry({required int roomId}) async {
|
||||||
|
await Request().post(Api.liveRoomEntry, queryParameters: {
|
||||||
|
'room_id': roomId,
|
||||||
|
'platform': 'pc',
|
||||||
|
'csrf_token': await Request.getCsrf(),
|
||||||
|
'csrf': await Request.getCsrf(),
|
||||||
|
'visit_id': '',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:pilipala/models/member/article.dart';
|
||||||
import 'package:pilipala/models/member/like.dart';
|
import 'package:pilipala/models/member/like.dart';
|
||||||
import '../common/constants.dart';
|
import '../common/constants.dart';
|
||||||
import '../models/dynamics/result.dart';
|
import '../models/dynamics/result.dart';
|
||||||
@ -556,4 +559,60 @@ class MemberHttp {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future getWWebid({required int mid}) async {
|
||||||
|
var res = await Request().get('https://space.bilibili.com/$mid/article');
|
||||||
|
String? headContent = parse(res.data).head?.outerHtml;
|
||||||
|
final regex = RegExp(
|
||||||
|
r'<script id="__RENDER_DATA__" type="application/json">(.*?)</script>');
|
||||||
|
if (headContent != null) {
|
||||||
|
final match = regex.firstMatch(headContent);
|
||||||
|
if (match != null && match.groupCount >= 1) {
|
||||||
|
final content = match.group(1);
|
||||||
|
String decodedString = Uri.decodeComponent(content!);
|
||||||
|
Map<String, dynamic> map = jsonDecode(decodedString);
|
||||||
|
return {'status': true, 'data': map['access_id']};
|
||||||
|
} else {
|
||||||
|
return {'status': false, 'data': '请检查登录状态'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {'status': false, 'data': '请检查登录状态'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户专栏
|
||||||
|
static Future getMemberArticle({
|
||||||
|
required int mid,
|
||||||
|
required int pn,
|
||||||
|
required String wWebid,
|
||||||
|
String? offset,
|
||||||
|
}) async {
|
||||||
|
Map params = await WbiSign().makSign({
|
||||||
|
'host_mid': mid,
|
||||||
|
'page': pn,
|
||||||
|
'offset': offset,
|
||||||
|
'web_location': 333.999,
|
||||||
|
'w_webid': wWebid,
|
||||||
|
});
|
||||||
|
var res = await Request().get(Api.opusList, data: {
|
||||||
|
'host_mid': mid,
|
||||||
|
'page': pn,
|
||||||
|
'offset': offset,
|
||||||
|
'web_location': 333.999,
|
||||||
|
'w_webid': wWebid,
|
||||||
|
'w_rid': params['w_rid'],
|
||||||
|
'wts': params['wts'],
|
||||||
|
});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': MemberArticleDataModel.fromJson(res.data['data'])
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'] ?? '请求异常',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -330,4 +330,27 @@ class MsgHttp {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future messageSystemAccount() async {
|
||||||
|
var res = await Request().get(Api.userMessageSystemAPi, data: {
|
||||||
|
'csrf': await Request.getCsrf(),
|
||||||
|
'page_size': 20,
|
||||||
|
'build': 0,
|
||||||
|
'mobi_app': 'web',
|
||||||
|
});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': res.data['data']['system_notify_list']
|
||||||
|
.map<MessageSystemModel>((e) => MessageSystemModel.fromJson(e))
|
||||||
|
.toList(),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {'status': false, 'date': [], 'msg': err.toString()};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {'status': false, 'date': [], 'msg': res.data['message']};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
lib/http/read.dart
Normal file
116
lib/http/read.dart
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:pilipala/models/read/opus.dart';
|
||||||
|
import 'package:pilipala/models/read/read.dart';
|
||||||
|
import 'package:pilipala/utils/wbi_sign.dart';
|
||||||
|
import 'index.dart';
|
||||||
|
|
||||||
|
class ReadHttp {
|
||||||
|
static List<String> extractScriptContents(String htmlContent) {
|
||||||
|
RegExp scriptRegExp = RegExp(r'<script>([\s\S]*?)<\/script>');
|
||||||
|
Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);
|
||||||
|
List<String> scriptContents = [];
|
||||||
|
for (Match match in matches) {
|
||||||
|
String scriptContent = match.group(1)!;
|
||||||
|
scriptContents.add(scriptContent);
|
||||||
|
}
|
||||||
|
return scriptContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析专栏 opus格式
|
||||||
|
static Future parseArticleOpus({required String id}) async {
|
||||||
|
var res = await Request().get('https://www.bilibili.com/opus/$id', extra: {
|
||||||
|
'ua': 'pc',
|
||||||
|
});
|
||||||
|
String? headContent = parse(res.data).head?.outerHtml;
|
||||||
|
var document = parse(headContent);
|
||||||
|
var linkTags = document.getElementsByTagName('link');
|
||||||
|
bool isCv = false;
|
||||||
|
String cvId = '';
|
||||||
|
for (var linkTag in linkTags) {
|
||||||
|
var attributes = linkTag.attributes;
|
||||||
|
if (attributes.containsKey('rel') &&
|
||||||
|
attributes['rel'] == 'canonical' &&
|
||||||
|
attributes.containsKey('data-vue-meta') &&
|
||||||
|
attributes['data-vue-meta'] == 'true') {
|
||||||
|
final String cvHref = linkTag.attributes['href']!;
|
||||||
|
RegExp regex = RegExp(r'cv(\d+)');
|
||||||
|
RegExpMatch? match = regex.firstMatch(cvHref);
|
||||||
|
if (match != null) {
|
||||||
|
cvId = match.group(1)!;
|
||||||
|
} else {
|
||||||
|
print('No match found.');
|
||||||
|
}
|
||||||
|
isCv = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String scriptContent =
|
||||||
|
extractScriptContents(parse(res.data).body!.outerHtml)[0];
|
||||||
|
int startIndex = scriptContent.indexOf('{');
|
||||||
|
int endIndex = scriptContent.lastIndexOf('};');
|
||||||
|
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
|
||||||
|
// 解析JSON字符串为Map
|
||||||
|
Map<String, dynamic> jsonData = json.decode(jsonContent);
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': OpusDataModel.fromJson(jsonData),
|
||||||
|
'isCv': isCv,
|
||||||
|
'cvId': cvId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析专栏 cv格式
|
||||||
|
static Future parseArticleCv({required String id}) async {
|
||||||
|
var res = await Request().get(
|
||||||
|
'https://www.bilibili.com/read/cv$id',
|
||||||
|
extra: {'ua': 'pc'},
|
||||||
|
);
|
||||||
|
String scriptContent =
|
||||||
|
extractScriptContents(parse(res.data).body!.outerHtml)[0];
|
||||||
|
int startIndex = scriptContent.indexOf('{');
|
||||||
|
int endIndex = scriptContent.lastIndexOf('};');
|
||||||
|
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
|
||||||
|
// 解析JSON字符串为Map
|
||||||
|
Map<String, dynamic> jsonData = json.decode(jsonContent);
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': ReadDataModel.fromJson(jsonData),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
static Future getViewInfo({required String id}) async {
|
||||||
|
Map params = await WbiSign().makSign({
|
||||||
|
'id': id,
|
||||||
|
'mobi_app': 'pc',
|
||||||
|
'from': 'web',
|
||||||
|
'gaia_source': 'main_web',
|
||||||
|
'web_location': 333.976,
|
||||||
|
});
|
||||||
|
var res = await Request().get(
|
||||||
|
Api.getViewInfo,
|
||||||
|
data: {
|
||||||
|
'id': id,
|
||||||
|
'mobi_app': 'pc',
|
||||||
|
'from': 'web',
|
||||||
|
'gaia_source': 'main_web',
|
||||||
|
'web_location': 333.976,
|
||||||
|
'w_rid': params['w_rid'],
|
||||||
|
'wts': params['wts'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': res.data['data'],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -143,7 +143,11 @@ class SearchHttp {
|
|||||||
}
|
}
|
||||||
final dynamic res =
|
final dynamic res =
|
||||||
await Request().get(Api.ab2c, data: <String, dynamic>{...data});
|
await Request().get(Api.ab2c, data: <String, dynamic>{...data});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
return res.data['data'].first['cid'];
|
return res.data['data'].first['cid'];
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> bangumiInfo(
|
static Future<Map<String, dynamic>> bangumiInfo(
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:pilipala/models/video/later.dart';
|
||||||
import '../common/constants.dart';
|
import '../common/constants.dart';
|
||||||
import '../models/model_hot_video_item.dart';
|
import '../models/model_hot_video_item.dart';
|
||||||
import '../models/user/fav_detail.dart';
|
import '../models/user/fav_detail.dart';
|
||||||
@ -57,6 +62,8 @@ class UserHttp {
|
|||||||
if (res.data['data'] != null) {
|
if (res.data['data'] != null) {
|
||||||
data = FavFolderData.fromJson(res.data['data']);
|
data = FavFolderData.fromJson(res.data['data']);
|
||||||
return {'status': true, 'data': data};
|
return {'status': true, 'data': data};
|
||||||
|
} else {
|
||||||
|
return {'status': false, 'msg': '收藏夹为空'};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
@ -428,4 +435,106 @@ class UserHttp {
|
|||||||
return {'status': false, 'msg': res.data['message']};
|
return {'status': false, 'msg': res.data['message']};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 稍后再看播放全部
|
||||||
|
// static Future toViewPlayAll({required int oid, required String bvid}) async {
|
||||||
|
// var res = await Request().get(
|
||||||
|
// Api.watchLaterHtml,
|
||||||
|
// data: {
|
||||||
|
// 'oid': oid,
|
||||||
|
// 'bvid': bvid,
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// String scriptContent =
|
||||||
|
// extractScriptContents(parse(res.data).body!.outerHtml)[0];
|
||||||
|
// int startIndex = scriptContent.indexOf('{');
|
||||||
|
// int endIndex = scriptContent.lastIndexOf('};');
|
||||||
|
// String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
|
||||||
|
// // 解析JSON字符串为Map
|
||||||
|
// Map<String, dynamic> jsonData = json.decode(jsonContent);
|
||||||
|
// // 输出解析后的数据
|
||||||
|
// return {
|
||||||
|
// 'status': true,
|
||||||
|
// 'data': jsonData['resourceList']
|
||||||
|
// .map((e) => MediaVideoItemModel.fromJson(e))
|
||||||
|
// .toList()
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
static List<String> extractScriptContents(String htmlContent) {
|
||||||
|
RegExp scriptRegExp = RegExp(r'<script>([\s\S]*?)<\/script>');
|
||||||
|
Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);
|
||||||
|
List<String> scriptContents = [];
|
||||||
|
for (Match match in matches) {
|
||||||
|
String scriptContent = match.group(1)!;
|
||||||
|
scriptContents.add(scriptContent);
|
||||||
|
}
|
||||||
|
return scriptContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 稍后再看列表
|
||||||
|
static Future getMediaList({
|
||||||
|
required int type,
|
||||||
|
required int bizId,
|
||||||
|
required int ps,
|
||||||
|
int? oid,
|
||||||
|
}) async {
|
||||||
|
var res = await Request().get(
|
||||||
|
Api.mediaList,
|
||||||
|
data: {
|
||||||
|
'mobi_app': 'web',
|
||||||
|
'type': type,
|
||||||
|
'biz_id': bizId,
|
||||||
|
'oid': oid ?? '',
|
||||||
|
'otype': 2,
|
||||||
|
'ps': ps,
|
||||||
|
'direction': false,
|
||||||
|
'desc': true,
|
||||||
|
'sort_field': 1,
|
||||||
|
'tid': 0,
|
||||||
|
'with_current': false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': res.data['data']['media_list'] != null
|
||||||
|
? res.data['data']['media_list']
|
||||||
|
.map<MediaVideoItemModel>(
|
||||||
|
(e) => MediaVideoItemModel.fromJson(e))
|
||||||
|
.toList()
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {'status': false, 'msg': res.data['message']};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析收藏夹视频
|
||||||
|
static Future parseFavVideo({
|
||||||
|
required int mediaId,
|
||||||
|
required int oid,
|
||||||
|
required String bvid,
|
||||||
|
}) async {
|
||||||
|
var res = await Request().get(
|
||||||
|
'https://www.bilibili.com/list/ml$mediaId',
|
||||||
|
data: {
|
||||||
|
'oid': mediaId,
|
||||||
|
'bvid': bvid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
String scriptContent =
|
||||||
|
extractScriptContents(parse(res.data).body!.outerHtml)[0];
|
||||||
|
int startIndex = scriptContent.indexOf('{');
|
||||||
|
int endIndex = scriptContent.lastIndexOf('};');
|
||||||
|
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
|
||||||
|
// 解析JSON字符串为Map
|
||||||
|
Map<String, dynamic> jsonData = json.decode(jsonContent);
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': jsonData['resourceList']
|
||||||
|
.map<MediaVideoItemModel>((e) => MediaVideoItemModel.fromJson(e))
|
||||||
|
.toList()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import 'package:pilipala/pages/main/view.dart';
|
|||||||
import 'package:pilipala/services/service_locator.dart';
|
import 'package:pilipala/services/service_locator.dart';
|
||||||
import 'package:pilipala/utils/app_scheme.dart';
|
import 'package:pilipala/utils/app_scheme.dart';
|
||||||
import 'package:pilipala/utils/data.dart';
|
import 'package:pilipala/utils/data.dart';
|
||||||
import 'package:pilipala/utils/global_data.dart';
|
import 'package:pilipala/utils/global_data_cache.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:pilipala/utils/recommend_filter.dart';
|
import 'package:pilipala/utils/recommend_filter.dart';
|
||||||
@ -33,7 +33,6 @@ void main() async {
|
|||||||
await SystemChrome.setPreferredOrientations(
|
await SystemChrome.setPreferredOrientations(
|
||||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
|
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
|
||||||
await GStrorage.init();
|
await GStrorage.init();
|
||||||
await setupServiceLocator();
|
|
||||||
clearLogs();
|
clearLogs();
|
||||||
Request();
|
Request();
|
||||||
await Request.setCookie();
|
await Request.setCookie();
|
||||||
@ -65,6 +64,7 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PiliSchame.init();
|
PiliSchame.init();
|
||||||
|
await GlobalDataCache().initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
@ -266,10 +266,10 @@ class BuildMainApp extends StatelessWidget {
|
|||||||
VideoDetailPage.routeObserver,
|
VideoDetailPage.routeObserver,
|
||||||
SearchPage.routeObserver,
|
SearchPage.routeObserver,
|
||||||
],
|
],
|
||||||
onInit: () {
|
onReady: () async {
|
||||||
RecommendFilter();
|
RecommendFilter();
|
||||||
Data.init();
|
Data.init();
|
||||||
GlobalData();
|
setupServiceLocator();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
126
lib/models/live/follow.dart
Normal file
126
lib/models/live/follow.dart
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
class LiveFollowingModel {
|
||||||
|
int? count;
|
||||||
|
List<LiveFollowingItemModel>? list;
|
||||||
|
int? liveCount;
|
||||||
|
int? neverLivedCount;
|
||||||
|
List? neverLivedFaces;
|
||||||
|
int? pageSize;
|
||||||
|
String? title;
|
||||||
|
int? totalPage;
|
||||||
|
|
||||||
|
LiveFollowingModel({
|
||||||
|
this.count,
|
||||||
|
this.list,
|
||||||
|
this.liveCount,
|
||||||
|
this.neverLivedCount,
|
||||||
|
this.neverLivedFaces,
|
||||||
|
this.pageSize,
|
||||||
|
this.title,
|
||||||
|
this.totalPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
LiveFollowingModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
count = json['count'];
|
||||||
|
if (json['list'] != null) {
|
||||||
|
list = <LiveFollowingItemModel>[];
|
||||||
|
json['list'].forEach((v) {
|
||||||
|
list!.add(LiveFollowingItemModel.fromJson(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
liveCount = json['live_count'];
|
||||||
|
neverLivedCount = json['never_lived_count'];
|
||||||
|
if (json['never_lived_faces'] != null) {
|
||||||
|
neverLivedFaces = <dynamic>[];
|
||||||
|
json['never_lived_faces'].forEach((v) {
|
||||||
|
neverLivedFaces!.add(v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pageSize = json['pageSize'];
|
||||||
|
title = json['title'];
|
||||||
|
totalPage = json['totalPage'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LiveFollowingItemModel {
|
||||||
|
int? roomId;
|
||||||
|
int? uid;
|
||||||
|
String? uname;
|
||||||
|
String? title;
|
||||||
|
String? face;
|
||||||
|
int? liveStatus;
|
||||||
|
int? recordNum;
|
||||||
|
String? recentRecordId;
|
||||||
|
int? isAttention;
|
||||||
|
int? clipNum;
|
||||||
|
int? fansNum;
|
||||||
|
String? areaName;
|
||||||
|
String? areaValue;
|
||||||
|
String? tags;
|
||||||
|
String? recentRecordIdV2;
|
||||||
|
int? recordNumV2;
|
||||||
|
int? recordLiveTime;
|
||||||
|
String? areaNameV2;
|
||||||
|
String? roomNews;
|
||||||
|
String? watchIcon;
|
||||||
|
String? textSmall;
|
||||||
|
String? roomCover;
|
||||||
|
String? pic;
|
||||||
|
int? parentAreaId;
|
||||||
|
int? areaId;
|
||||||
|
|
||||||
|
LiveFollowingItemModel({
|
||||||
|
this.roomId,
|
||||||
|
this.uid,
|
||||||
|
this.uname,
|
||||||
|
this.title,
|
||||||
|
this.face,
|
||||||
|
this.liveStatus,
|
||||||
|
this.recordNum,
|
||||||
|
this.recentRecordId,
|
||||||
|
this.isAttention,
|
||||||
|
this.clipNum,
|
||||||
|
this.fansNum,
|
||||||
|
this.areaName,
|
||||||
|
this.areaValue,
|
||||||
|
this.tags,
|
||||||
|
this.recentRecordIdV2,
|
||||||
|
this.recordNumV2,
|
||||||
|
this.recordLiveTime,
|
||||||
|
this.areaNameV2,
|
||||||
|
this.roomNews,
|
||||||
|
this.watchIcon,
|
||||||
|
this.textSmall,
|
||||||
|
this.roomCover,
|
||||||
|
this.pic,
|
||||||
|
this.parentAreaId,
|
||||||
|
this.areaId,
|
||||||
|
});
|
||||||
|
|
||||||
|
LiveFollowingItemModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
roomId = json['roomid'];
|
||||||
|
uid = json['uid'];
|
||||||
|
uname = json['uname'];
|
||||||
|
title = json['title'];
|
||||||
|
face = json['face'];
|
||||||
|
liveStatus = json['live_status'];
|
||||||
|
recordNum = json['record_num'];
|
||||||
|
recentRecordId = json['recent_record_id'];
|
||||||
|
isAttention = json['is_attention'];
|
||||||
|
clipNum = json['clipnum'];
|
||||||
|
fansNum = json['fans_num'];
|
||||||
|
areaName = json['area_name'];
|
||||||
|
areaValue = json['area_value'];
|
||||||
|
tags = json['tags'];
|
||||||
|
recentRecordIdV2 = json['recent_record_id_v2'];
|
||||||
|
recordNumV2 = json['record_num_v2'];
|
||||||
|
recordLiveTime = json['record_live_time'];
|
||||||
|
areaNameV2 = json['area_name_v2'];
|
||||||
|
roomNews = json['room_news'];
|
||||||
|
watchIcon = json['watch_icon'];
|
||||||
|
textSmall = json['text_small'];
|
||||||
|
roomCover = json['room_cover'];
|
||||||
|
pic = json['room_cover'];
|
||||||
|
parentAreaId = json['parent_area_id'];
|
||||||
|
areaId = json['area_id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +1,13 @@
|
|||||||
class RoomInfoModel {
|
class RoomInfoModel {
|
||||||
RoomInfoModel({
|
RoomInfoModel({
|
||||||
this.roomId,
|
this.roomId,
|
||||||
|
this.isPortrait,
|
||||||
this.liveStatus,
|
this.liveStatus,
|
||||||
this.liveTime,
|
this.liveTime,
|
||||||
this.playurlInfo,
|
this.playurlInfo,
|
||||||
});
|
});
|
||||||
int? roomId;
|
int? roomId;
|
||||||
|
bool? isPortrait;
|
||||||
int? liveStatus;
|
int? liveStatus;
|
||||||
int? liveTime;
|
int? liveTime;
|
||||||
PlayurlInfo? playurlInfo;
|
PlayurlInfo? playurlInfo;
|
||||||
@ -13,6 +15,7 @@ class RoomInfoModel {
|
|||||||
RoomInfoModel.fromJson(Map<String, dynamic> json) {
|
RoomInfoModel.fromJson(Map<String, dynamic> json) {
|
||||||
roomId = json['room_id'];
|
roomId = json['room_id'];
|
||||||
liveStatus = json['live_status'];
|
liveStatus = json['live_status'];
|
||||||
|
isPortrait = json['is_portrait'];
|
||||||
liveTime = json['live_time'];
|
liveTime = json['live_time'];
|
||||||
playurlInfo = PlayurlInfo.fromJson(json['playurl_info']);
|
playurlInfo = PlayurlInfo.fromJson(json['playurl_info']);
|
||||||
}
|
}
|
||||||
|
|||||||
46
lib/models/member/article.dart
Normal file
46
lib/models/member/article.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
class MemberArticleDataModel {
|
||||||
|
MemberArticleDataModel({
|
||||||
|
this.hasMore,
|
||||||
|
this.items,
|
||||||
|
this.offset,
|
||||||
|
this.updateNum,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool? hasMore;
|
||||||
|
List<MemberArticleItemModel>? items;
|
||||||
|
String? offset;
|
||||||
|
int? updateNum;
|
||||||
|
|
||||||
|
MemberArticleDataModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
hasMore = json['has_more'];
|
||||||
|
items = json['items']
|
||||||
|
.map<MemberArticleItemModel>((e) => MemberArticleItemModel.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
offset = json['offset'];
|
||||||
|
updateNum = json['update_num'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemberArticleItemModel {
|
||||||
|
MemberArticleItemModel({
|
||||||
|
this.content,
|
||||||
|
this.cover,
|
||||||
|
this.jumpUrl,
|
||||||
|
this.opusId,
|
||||||
|
this.stat,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? content;
|
||||||
|
Map? cover;
|
||||||
|
String? jumpUrl;
|
||||||
|
String? opusId;
|
||||||
|
Map? stat;
|
||||||
|
|
||||||
|
MemberArticleItemModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
content = json['content'];
|
||||||
|
cover = json['cover'];
|
||||||
|
jumpUrl = json['jump_url'];
|
||||||
|
opusId = json['opus_id'];
|
||||||
|
stat = json['stat'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@ class MessageSystemModel {
|
|||||||
int? cursor;
|
int? cursor;
|
||||||
int? type;
|
int? type;
|
||||||
String? title;
|
String? title;
|
||||||
Map? content;
|
dynamic content;
|
||||||
Source? source;
|
Source? source;
|
||||||
String? timeAt;
|
String? timeAt;
|
||||||
int? cardType;
|
int? cardType;
|
||||||
@ -45,7 +45,9 @@ class MessageSystemModel {
|
|||||||
cursor: jsons["cursor"],
|
cursor: jsons["cursor"],
|
||||||
type: jsons["type"],
|
type: jsons["type"],
|
||||||
title: jsons["title"],
|
title: jsons["title"],
|
||||||
content: json.decode(jsons["content"]),
|
content: isValidJson(jsons["content"])
|
||||||
|
? json.decode(jsons["content"])
|
||||||
|
: jsons["content"],
|
||||||
source: Source.fromJson(jsons["source"]),
|
source: Source.fromJson(jsons["source"]),
|
||||||
timeAt: jsons["time_at"],
|
timeAt: jsons["time_at"],
|
||||||
cardType: jsons["card_type"],
|
cardType: jsons["card_type"],
|
||||||
@ -75,3 +77,12 @@ class Source {
|
|||||||
logo: json["logo"],
|
logo: json["logo"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isValidJson(String str) {
|
||||||
|
try {
|
||||||
|
json.decode(str);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
485
lib/models/read/opus.dart
Normal file
485
lib/models/read/opus.dart
Normal file
@ -0,0 +1,485 @@
|
|||||||
|
class OpusDataModel {
|
||||||
|
OpusDataModel({
|
||||||
|
this.id,
|
||||||
|
this.detail,
|
||||||
|
this.type,
|
||||||
|
this.theme,
|
||||||
|
this.themeMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? id;
|
||||||
|
OpusDetailDataModel? detail;
|
||||||
|
int? type;
|
||||||
|
String? theme;
|
||||||
|
String? themeMode;
|
||||||
|
|
||||||
|
OpusDataModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
id = json['id'];
|
||||||
|
detail = json['detail'] != null
|
||||||
|
? OpusDetailDataModel.fromJson(json['detail'])
|
||||||
|
: null;
|
||||||
|
type = json['type'];
|
||||||
|
theme = json['theme'];
|
||||||
|
themeMode = json['themeMode'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpusDetailDataModel {
|
||||||
|
OpusDetailDataModel({
|
||||||
|
this.basic,
|
||||||
|
this.idStr,
|
||||||
|
this.modules,
|
||||||
|
this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
Basic? basic;
|
||||||
|
String? idStr;
|
||||||
|
List<OpusModuleDataModel>? modules;
|
||||||
|
int? type;
|
||||||
|
|
||||||
|
OpusDetailDataModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
basic = json['basic'] != null ? Basic.fromJson(json['basic']) : null;
|
||||||
|
idStr = json['id_str'];
|
||||||
|
if (json['modules'] != null) {
|
||||||
|
modules = <OpusModuleDataModel>[];
|
||||||
|
json['modules'].forEach((v) {
|
||||||
|
modules!.add(OpusModuleDataModel.fromJson(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
type = json['type'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Basic {
|
||||||
|
Basic({
|
||||||
|
this.commentIdStr,
|
||||||
|
this.commentType,
|
||||||
|
this.ridStr,
|
||||||
|
this.title,
|
||||||
|
this.uid,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? commentIdStr;
|
||||||
|
int? commentType;
|
||||||
|
String? ridStr;
|
||||||
|
String? title;
|
||||||
|
int? uid;
|
||||||
|
|
||||||
|
Basic.fromJson(Map<String, dynamic> json) {
|
||||||
|
commentIdStr = json['comment_id_str'];
|
||||||
|
commentType = json['comment_type'];
|
||||||
|
ridStr = json['rid_str'];
|
||||||
|
title = json['title'];
|
||||||
|
uid = json['uid'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpusModuleDataModel {
|
||||||
|
OpusModuleDataModel({
|
||||||
|
this.moduleTitle,
|
||||||
|
this.moduleAuthor,
|
||||||
|
this.moduleContent,
|
||||||
|
this.moduleExtend,
|
||||||
|
this.moduleBottom,
|
||||||
|
this.moduleStat,
|
||||||
|
});
|
||||||
|
|
||||||
|
ModuleTop? moduleTop;
|
||||||
|
ModuleTitle? moduleTitle;
|
||||||
|
ModuleAuthor? moduleAuthor;
|
||||||
|
ModuleContent? moduleContent;
|
||||||
|
ModuleExtend? moduleExtend;
|
||||||
|
ModuleBottom? moduleBottom;
|
||||||
|
ModuleStat? moduleStat;
|
||||||
|
|
||||||
|
OpusModuleDataModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
moduleTop = json['module_top'] != null
|
||||||
|
? ModuleTop.fromJson(json['module_top'])
|
||||||
|
: null;
|
||||||
|
moduleTitle = json['module_title'] != null
|
||||||
|
? ModuleTitle.fromJson(json['module_title'])
|
||||||
|
: null;
|
||||||
|
moduleAuthor = json['module_author'] != null
|
||||||
|
? ModuleAuthor.fromJson(json['module_author'])
|
||||||
|
: null;
|
||||||
|
moduleContent = json['module_content'] != null
|
||||||
|
? ModuleContent.fromJson(json['module_content'])
|
||||||
|
: null;
|
||||||
|
moduleExtend = json['module_extend'] != null
|
||||||
|
? ModuleExtend.fromJson(json['module_extend'])
|
||||||
|
: null;
|
||||||
|
moduleBottom = json['module_bottom'] != null
|
||||||
|
? ModuleBottom.fromJson(json['module_bottom'])
|
||||||
|
: null;
|
||||||
|
moduleStat = json['module_stat'] != null
|
||||||
|
? ModuleStat.fromJson(json['module_stat'])
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleTop {
|
||||||
|
ModuleTop({
|
||||||
|
this.type,
|
||||||
|
this.video,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? type;
|
||||||
|
Map? video;
|
||||||
|
|
||||||
|
ModuleTop.fromJson(Map<String, dynamic> json) {
|
||||||
|
type = json['type'];
|
||||||
|
video = json['video'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleTitle {
|
||||||
|
ModuleTitle({
|
||||||
|
this.text,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? text;
|
||||||
|
|
||||||
|
ModuleTitle.fromJson(Map<String, dynamic> json) {
|
||||||
|
text = json['text'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleAuthor {
|
||||||
|
ModuleAuthor({
|
||||||
|
this.face,
|
||||||
|
this.mid,
|
||||||
|
this.name,
|
||||||
|
this.pubTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? face;
|
||||||
|
int? mid;
|
||||||
|
String? name;
|
||||||
|
String? pubTime;
|
||||||
|
|
||||||
|
ModuleAuthor.fromJson(Map<String, dynamic> json) {
|
||||||
|
face = json['face'];
|
||||||
|
mid = json['mid'];
|
||||||
|
name = json['name'];
|
||||||
|
pubTime = json['pub_time'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleContent {
|
||||||
|
ModuleContent({
|
||||||
|
this.paragraphs,
|
||||||
|
this.moduleType,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<ModuleParagraph>? paragraphs;
|
||||||
|
String? moduleType;
|
||||||
|
|
||||||
|
ModuleContent.fromJson(Map<String, dynamic> json) {
|
||||||
|
if (json['paragraphs'] != null) {
|
||||||
|
paragraphs = <ModuleParagraph>[];
|
||||||
|
json['paragraphs'].forEach((v) {
|
||||||
|
paragraphs!.add(ModuleParagraph.fromJson(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
moduleType = json['module_type'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleParagraph {
|
||||||
|
ModuleParagraph({
|
||||||
|
this.align,
|
||||||
|
this.paraType,
|
||||||
|
this.pic,
|
||||||
|
this.text,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 0 左对齐 1 居中 2 右对齐
|
||||||
|
int? align;
|
||||||
|
int? paraType;
|
||||||
|
Pics? pic;
|
||||||
|
ModuleParagraphText? text;
|
||||||
|
LinkCard? linkCard;
|
||||||
|
|
||||||
|
ModuleParagraph.fromJson(Map<String, dynamic> json) {
|
||||||
|
align = json['align'];
|
||||||
|
paraType = json['para_type'] == null && json['link_card'] != null
|
||||||
|
? 3
|
||||||
|
: json['para_type'];
|
||||||
|
pic = json['pic'] != null ? Pics.fromJson(json['pic']) : null;
|
||||||
|
text = json['text'] != null
|
||||||
|
? ModuleParagraphText.fromJson(json['text'])
|
||||||
|
: null;
|
||||||
|
linkCard =
|
||||||
|
json['link_card'] != null ? LinkCard.fromJson(json['link_card']) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Pics {
|
||||||
|
Pics({
|
||||||
|
this.pics,
|
||||||
|
this.style,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Pic>? pics;
|
||||||
|
int? style;
|
||||||
|
|
||||||
|
Pics.fromJson(Map<String, dynamic> json) {
|
||||||
|
if (json['pics'] != null) {
|
||||||
|
pics = <Pic>[];
|
||||||
|
json['pics'].forEach((v) {
|
||||||
|
pics!.add(Pic.fromJson(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
style = json['style'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Pic {
|
||||||
|
Pic({
|
||||||
|
this.height,
|
||||||
|
this.size,
|
||||||
|
this.url,
|
||||||
|
this.width,
|
||||||
|
this.aspectRatio,
|
||||||
|
this.scale,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? height;
|
||||||
|
double? size;
|
||||||
|
String? url;
|
||||||
|
int? width;
|
||||||
|
double? aspectRatio;
|
||||||
|
double? scale;
|
||||||
|
|
||||||
|
Pic.fromJson(Map<String, dynamic> json) {
|
||||||
|
height = json['height'];
|
||||||
|
size = json['size'];
|
||||||
|
url = json['url'];
|
||||||
|
width = json['width'];
|
||||||
|
aspectRatio = json['width'] / json['height'];
|
||||||
|
scale = customDivision(json['width'], 600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LinkCard {
|
||||||
|
LinkCard({
|
||||||
|
this.cover,
|
||||||
|
this.descSecond,
|
||||||
|
this.duration,
|
||||||
|
this.jumpUrl,
|
||||||
|
this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? cover;
|
||||||
|
String? descSecond;
|
||||||
|
String? duration;
|
||||||
|
String? jumpUrl;
|
||||||
|
String? title;
|
||||||
|
|
||||||
|
LinkCard.fromJson(Map<String, dynamic> json) {
|
||||||
|
cover = json['card']['cover'];
|
||||||
|
descSecond = json['card']['desc_second'];
|
||||||
|
duration = json['card']['duration'];
|
||||||
|
jumpUrl = json['card']['jump_url'];
|
||||||
|
title = json['card']['title'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleParagraphText {
|
||||||
|
ModuleParagraphText({
|
||||||
|
this.nodes,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<ModuleParagraphTextNode>? nodes;
|
||||||
|
|
||||||
|
ModuleParagraphText.fromJson(Map<String, dynamic> json) {
|
||||||
|
if (json['nodes'] != null) {
|
||||||
|
nodes = <ModuleParagraphTextNode>[];
|
||||||
|
json['nodes'].forEach((v) {
|
||||||
|
nodes!.add(ModuleParagraphTextNode.fromJson(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleParagraphTextNode {
|
||||||
|
ModuleParagraphTextNode({
|
||||||
|
this.type,
|
||||||
|
this.nodeType,
|
||||||
|
this.word,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? type;
|
||||||
|
int? nodeType;
|
||||||
|
ModuleParagraphTextNodeWord? word;
|
||||||
|
|
||||||
|
ModuleParagraphTextNode.fromJson(Map<String, dynamic> json) {
|
||||||
|
type = json['type'];
|
||||||
|
nodeType = json['node_type'];
|
||||||
|
word = json['word'] != null
|
||||||
|
? ModuleParagraphTextNodeWord.fromJson(json['word'])
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleParagraphTextNodeWord {
|
||||||
|
ModuleParagraphTextNodeWord({
|
||||||
|
this.color,
|
||||||
|
this.fontSize,
|
||||||
|
this.style,
|
||||||
|
this.words,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? color;
|
||||||
|
int? fontSize;
|
||||||
|
ModuleParagraphTextNodeWordStyle? style;
|
||||||
|
String? words;
|
||||||
|
|
||||||
|
ModuleParagraphTextNodeWord.fromJson(Map<String, dynamic> json) {
|
||||||
|
color = json['color'];
|
||||||
|
fontSize = json['font_size'];
|
||||||
|
style = json['style'] != null
|
||||||
|
? ModuleParagraphTextNodeWordStyle.fromJson(json['style'])
|
||||||
|
: null;
|
||||||
|
words = json['words'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleParagraphTextNodeWordStyle {
|
||||||
|
ModuleParagraphTextNodeWordStyle({
|
||||||
|
this.bold,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool? bold;
|
||||||
|
|
||||||
|
ModuleParagraphTextNodeWordStyle.fromJson(Map<String, dynamic> json) {
|
||||||
|
bold = json['bold'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleExtend {
|
||||||
|
ModuleExtend({
|
||||||
|
this.items,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<ModuleExtendItem>? items;
|
||||||
|
|
||||||
|
ModuleExtend.fromJson(Map<String, dynamic> json) {
|
||||||
|
if (json['items'] != null) {
|
||||||
|
items = <ModuleExtendItem>[];
|
||||||
|
json['items'].forEach((v) {
|
||||||
|
items!.add(ModuleExtendItem.fromJson(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleExtendItem {
|
||||||
|
ModuleExtendItem({
|
||||||
|
this.bizId,
|
||||||
|
this.bizType,
|
||||||
|
this.icon,
|
||||||
|
this.jumpUrl,
|
||||||
|
this.text,
|
||||||
|
});
|
||||||
|
|
||||||
|
dynamic bizId;
|
||||||
|
int? bizType;
|
||||||
|
dynamic icon;
|
||||||
|
String? jumpUrl;
|
||||||
|
String? text;
|
||||||
|
|
||||||
|
ModuleExtendItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
bizId = json['biz_id'];
|
||||||
|
bizType = json['biz_type'];
|
||||||
|
icon = json['icon'];
|
||||||
|
jumpUrl = json['jump_url'];
|
||||||
|
text = json['text'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleBottom {
|
||||||
|
ModuleBottom({
|
||||||
|
this.shareInfo,
|
||||||
|
});
|
||||||
|
|
||||||
|
ShareInfo? shareInfo;
|
||||||
|
|
||||||
|
ModuleBottom.fromJson(Map<String, dynamic> json) {
|
||||||
|
shareInfo = json['share_info'] != null
|
||||||
|
? ShareInfo.fromJson(json['share_info'])
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShareInfo {
|
||||||
|
ShareInfo({
|
||||||
|
this.pic,
|
||||||
|
this.summary,
|
||||||
|
this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? pic;
|
||||||
|
String? summary;
|
||||||
|
String? title;
|
||||||
|
|
||||||
|
ShareInfo.fromJson(Map<String, dynamic> json) {
|
||||||
|
pic = json['pic'];
|
||||||
|
summary = json['summary'];
|
||||||
|
title = json['title'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModuleStat {
|
||||||
|
ModuleStat({
|
||||||
|
this.coin,
|
||||||
|
this.comment,
|
||||||
|
this.favorite,
|
||||||
|
this.forward,
|
||||||
|
this.like,
|
||||||
|
});
|
||||||
|
|
||||||
|
StatItem? coin;
|
||||||
|
StatItem? comment;
|
||||||
|
StatItem? favorite;
|
||||||
|
StatItem? forward;
|
||||||
|
StatItem? like;
|
||||||
|
|
||||||
|
ModuleStat.fromJson(Map<String, dynamic> json) {
|
||||||
|
coin = json['coin'] != null ? StatItem.fromJson(json['coin']) : null;
|
||||||
|
comment =
|
||||||
|
json['comment'] != null ? StatItem.fromJson(json['comment']) : null;
|
||||||
|
favorite =
|
||||||
|
json['favorite'] != null ? StatItem.fromJson(json['favorite']) : null;
|
||||||
|
forward =
|
||||||
|
json['forward'] != null ? StatItem.fromJson(json['forward']) : null;
|
||||||
|
like = json['like'] != null ? StatItem.fromJson(json['like']) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatItem {
|
||||||
|
StatItem({
|
||||||
|
this.count,
|
||||||
|
this.forbidden,
|
||||||
|
this.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? count;
|
||||||
|
bool? forbidden;
|
||||||
|
bool? status;
|
||||||
|
|
||||||
|
StatItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
count = json['count'];
|
||||||
|
forbidden = json['forbidden'];
|
||||||
|
status = json['status'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double customDivision(int a, int b) {
|
||||||
|
double result = a / b;
|
||||||
|
if (result < 1) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
286
lib/models/read/read.dart
Normal file
286
lib/models/read/read.dart
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
import 'package:pilipala/models/member/info.dart';
|
||||||
|
|
||||||
|
import 'opus.dart';
|
||||||
|
|
||||||
|
class ReadDataModel {
|
||||||
|
ReadDataModel({
|
||||||
|
this.cvid,
|
||||||
|
this.readInfo,
|
||||||
|
this.readViewInfo,
|
||||||
|
this.upInfo,
|
||||||
|
this.catalogList,
|
||||||
|
this.recommendInfoList,
|
||||||
|
this.hiddenInteraction,
|
||||||
|
this.isModern,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? cvid;
|
||||||
|
ReadInfo? readInfo;
|
||||||
|
Map? readViewInfo;
|
||||||
|
Map? upInfo;
|
||||||
|
List<dynamic>? catalogList;
|
||||||
|
List<dynamic>? recommendInfoList;
|
||||||
|
bool? hiddenInteraction;
|
||||||
|
bool? isModern;
|
||||||
|
|
||||||
|
ReadDataModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
cvid = json['cvid'];
|
||||||
|
readInfo =
|
||||||
|
json['readInfo'] != null ? ReadInfo.fromJson(json['readInfo']) : null;
|
||||||
|
readViewInfo = json['readViewInfo'];
|
||||||
|
upInfo = json['upInfo'];
|
||||||
|
if (json['catalogList'] != null) {
|
||||||
|
catalogList = <dynamic>[];
|
||||||
|
json['catalogList'].forEach((v) {
|
||||||
|
catalogList!.add(v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (json['recommendInfoList'] != null) {
|
||||||
|
recommendInfoList = <dynamic>[];
|
||||||
|
json['recommendInfoList'].forEach((v) {
|
||||||
|
recommendInfoList!.add(v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
hiddenInteraction = json['hiddenInteraction'];
|
||||||
|
isModern = json['isModern'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReadInfo {
|
||||||
|
ReadInfo({
|
||||||
|
this.id,
|
||||||
|
this.category,
|
||||||
|
this.title,
|
||||||
|
this.summary,
|
||||||
|
this.bannerUrl,
|
||||||
|
this.author,
|
||||||
|
this.publishTime,
|
||||||
|
this.ctime,
|
||||||
|
this.mtime,
|
||||||
|
this.stats,
|
||||||
|
this.attributes,
|
||||||
|
this.words,
|
||||||
|
this.originImageUrls,
|
||||||
|
this.content,
|
||||||
|
this.opus,
|
||||||
|
this.dynIdStr,
|
||||||
|
this.totalArtNum,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? id;
|
||||||
|
Map? category;
|
||||||
|
String? title;
|
||||||
|
String? summary;
|
||||||
|
String? bannerUrl;
|
||||||
|
Author? author;
|
||||||
|
int? publishTime;
|
||||||
|
int? ctime;
|
||||||
|
int? mtime;
|
||||||
|
Map? stats;
|
||||||
|
int? attributes;
|
||||||
|
int? words;
|
||||||
|
List<String>? originImageUrls;
|
||||||
|
String? content;
|
||||||
|
Opus? opus;
|
||||||
|
String? dynIdStr;
|
||||||
|
int? totalArtNum;
|
||||||
|
|
||||||
|
ReadInfo.fromJson(Map<String, dynamic> json) {
|
||||||
|
id = json['id'];
|
||||||
|
category = json['category'];
|
||||||
|
title = json['title'];
|
||||||
|
summary = json['summary'];
|
||||||
|
bannerUrl = json['banner_url'];
|
||||||
|
author = Author.fromJson(json['author']);
|
||||||
|
publishTime = json['publish_time'];
|
||||||
|
ctime = json['ctime'];
|
||||||
|
mtime = json['mtime'];
|
||||||
|
stats = json['stats'];
|
||||||
|
attributes = json['attributes'];
|
||||||
|
words = json['words'];
|
||||||
|
if (json['origin_image_urls'] != null) {
|
||||||
|
originImageUrls = <String>[];
|
||||||
|
json['origin_image_urls'].forEach((v) {
|
||||||
|
originImageUrls!.add(v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
content = json['content'];
|
||||||
|
opus = json['opus'] != null ? Opus.fromJson(json['opus']) : null;
|
||||||
|
dynIdStr = json['dyn_id_str'];
|
||||||
|
totalArtNum = json['total_art_num'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Author {
|
||||||
|
Author({
|
||||||
|
this.mid,
|
||||||
|
this.name,
|
||||||
|
this.face,
|
||||||
|
this.vip,
|
||||||
|
this.fans,
|
||||||
|
this.level,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? mid;
|
||||||
|
String? name;
|
||||||
|
String? face;
|
||||||
|
Vip? vip;
|
||||||
|
int? fans;
|
||||||
|
int? level;
|
||||||
|
|
||||||
|
Author.fromJson(Map<String, dynamic> json) {
|
||||||
|
mid = json['mid'];
|
||||||
|
name = json['name'];
|
||||||
|
face = json['face'];
|
||||||
|
vip = json['vip'] != null ? Vip.fromJson(json['vip']) : null;
|
||||||
|
fans = json['fans'];
|
||||||
|
level = json['level'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Opus {
|
||||||
|
// "opus_id": 976625853207150600,
|
||||||
|
// "opus_source": 2,
|
||||||
|
// "title": "真的很想骂人 但又没什么好骂的",
|
||||||
|
// "content": {
|
||||||
|
// "paragraphs": [{
|
||||||
|
// "para_type": 1,
|
||||||
|
// "text": {
|
||||||
|
// "nodes": [{
|
||||||
|
// "node_type": 1,
|
||||||
|
// "word": {
|
||||||
|
// "words": "21年玩到今年4月的号没了 ow1的时候45的号 玩了三年 后面第9赛季一个英杰5的号(虽然是偷的 但我任何违规行为都没有还是给我封了) 最近玩的号叫velleity 只和队友打天梯以及训练赛 又没了 连带着我一个一把没玩过只玩过一场训练赛的小号也没了 实在是无话可说了...",
|
||||||
|
// "font_size": 17,
|
||||||
|
// "style": {},
|
||||||
|
// "font_level": "regular"
|
||||||
|
// }
|
||||||
|
// }]
|
||||||
|
// }
|
||||||
|
// }, {
|
||||||
|
// "para_type": 2,
|
||||||
|
// "pic": {
|
||||||
|
// "pics": [{
|
||||||
|
// "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002Fba4e57459451fe74dcb70fd20bde9823316082117.jpg",
|
||||||
|
// "width": 1600,
|
||||||
|
// "height": 1000,
|
||||||
|
// "size": 588.482421875
|
||||||
|
// }],
|
||||||
|
// "style": 1
|
||||||
|
// }
|
||||||
|
// }, {
|
||||||
|
// "para_type": 1,
|
||||||
|
// "text": {
|
||||||
|
// "nodes": [{
|
||||||
|
// "node_type": 1,
|
||||||
|
// "word": {
|
||||||
|
// "words": "\n",
|
||||||
|
// "font_size": 17,
|
||||||
|
// "style": {},
|
||||||
|
// "font_level": "regular"
|
||||||
|
// }
|
||||||
|
// }]
|
||||||
|
// }
|
||||||
|
// }, {
|
||||||
|
// "para_type": 2,
|
||||||
|
// "pic": {
|
||||||
|
// "pics": [{
|
||||||
|
// "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002F0945be6b621091ddb8189482a87a36fb316082117.jpg",
|
||||||
|
// "width": 1600,
|
||||||
|
// "height": 1002,
|
||||||
|
// "size": 665.7861328125
|
||||||
|
// }],
|
||||||
|
// "style": 1
|
||||||
|
// }
|
||||||
|
// }, {
|
||||||
|
// "para_type": 1,
|
||||||
|
// "text": {
|
||||||
|
// "nodes": [{
|
||||||
|
// "node_type": 1,
|
||||||
|
// "word": {
|
||||||
|
// "words": "\n",
|
||||||
|
// "font_size": 17,
|
||||||
|
// "style": {},
|
||||||
|
// "font_level": "regular"
|
||||||
|
// }
|
||||||
|
// }]
|
||||||
|
// }
|
||||||
|
// }, {
|
||||||
|
// "para_type": 2,
|
||||||
|
// "pic": {
|
||||||
|
// "pics": [{
|
||||||
|
// "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002Ffa60649f8786578a764a1e68a2c5d23f316082117.jpg",
|
||||||
|
// "width": 1600,
|
||||||
|
// "height": 999,
|
||||||
|
// "size": 332.970703125
|
||||||
|
// }],
|
||||||
|
// "style": 1
|
||||||
|
// }
|
||||||
|
// }, {
|
||||||
|
// "para_type": 1,
|
||||||
|
// "text": {
|
||||||
|
// "nodes": [{
|
||||||
|
// "node_type": 1,
|
||||||
|
// "word": {
|
||||||
|
// "words": "\n",
|
||||||
|
// "font_size": 17,
|
||||||
|
// "style": {},
|
||||||
|
// "font_level": "regular"
|
||||||
|
// }
|
||||||
|
// }]
|
||||||
|
// }
|
||||||
|
// }]
|
||||||
|
// },
|
||||||
|
// "pub_info": {
|
||||||
|
// "uid": 316082117,
|
||||||
|
// "pub_time": 1726226826
|
||||||
|
// },
|
||||||
|
// "article": {
|
||||||
|
// "category_id": 15,
|
||||||
|
// "cover": [{
|
||||||
|
// "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002Fbanner\u002Feb10074186a62f98c18e1b5b9deb38be316082117.png",
|
||||||
|
// "width": 1071,
|
||||||
|
// "height": 315,
|
||||||
|
// "size": 225.625
|
||||||
|
// }]
|
||||||
|
// },
|
||||||
|
// "version": {
|
||||||
|
// "cvid": 38660379,
|
||||||
|
// "version_id": 101683514411343360
|
||||||
|
// }
|
||||||
|
Opus({
|
||||||
|
this.opusId,
|
||||||
|
this.opusSource,
|
||||||
|
this.title,
|
||||||
|
this.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? opusId;
|
||||||
|
int? opusSource;
|
||||||
|
String? title;
|
||||||
|
Content? content;
|
||||||
|
|
||||||
|
Opus.fromJson(Map<String, dynamic> json) {
|
||||||
|
opusId = json['opus_id'];
|
||||||
|
opusSource = json['opus_source'];
|
||||||
|
title = json['title'];
|
||||||
|
content =
|
||||||
|
json['content'] != null ? Content.fromJson(json['content']) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Content {
|
||||||
|
Content({
|
||||||
|
this.paragraphs,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<ModuleParagraph>? paragraphs;
|
||||||
|
|
||||||
|
Content.fromJson(Map<String, dynamic> json) {
|
||||||
|
if (json['paragraphs'] != null) {
|
||||||
|
paragraphs = <ModuleParagraph>[];
|
||||||
|
json['paragraphs'].forEach((v) {
|
||||||
|
paragraphs!.add(ModuleParagraph.fromJson(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
270
lib/models/video/later.dart
Normal file
270
lib/models/video/later.dart
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
class MediaVideoItemModel {
|
||||||
|
MediaVideoItemModel({
|
||||||
|
this.id,
|
||||||
|
this.offset,
|
||||||
|
this.index,
|
||||||
|
this.intro,
|
||||||
|
this.attr,
|
||||||
|
this.tid,
|
||||||
|
this.copyRight,
|
||||||
|
this.cntInfo,
|
||||||
|
this.cover,
|
||||||
|
this.duration,
|
||||||
|
this.pubtime,
|
||||||
|
this.likeState,
|
||||||
|
this.favState,
|
||||||
|
this.page,
|
||||||
|
this.pages,
|
||||||
|
this.title,
|
||||||
|
this.type,
|
||||||
|
this.upper,
|
||||||
|
this.link,
|
||||||
|
this.bvId,
|
||||||
|
this.shortLink,
|
||||||
|
this.rights,
|
||||||
|
this.elecInfo,
|
||||||
|
this.coin,
|
||||||
|
this.progressPercent,
|
||||||
|
this.badge,
|
||||||
|
this.forbidFav,
|
||||||
|
this.moreType,
|
||||||
|
this.businessOid,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? id;
|
||||||
|
int? offset;
|
||||||
|
int? index;
|
||||||
|
String? intro;
|
||||||
|
int? attr;
|
||||||
|
int? tid;
|
||||||
|
int? copyRight;
|
||||||
|
Map? cntInfo;
|
||||||
|
String? cover;
|
||||||
|
int? duration;
|
||||||
|
int? pubtime;
|
||||||
|
int? likeState;
|
||||||
|
int? favState;
|
||||||
|
int? page;
|
||||||
|
List<Page>? pages;
|
||||||
|
String? title;
|
||||||
|
int? type;
|
||||||
|
Upper? upper;
|
||||||
|
String? link;
|
||||||
|
String? bvId;
|
||||||
|
String? shortLink;
|
||||||
|
Rights? rights;
|
||||||
|
dynamic elecInfo;
|
||||||
|
Coin? coin;
|
||||||
|
double? progressPercent;
|
||||||
|
dynamic badge;
|
||||||
|
bool? forbidFav;
|
||||||
|
int? moreType;
|
||||||
|
int? businessOid;
|
||||||
|
|
||||||
|
factory MediaVideoItemModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
MediaVideoItemModel(
|
||||||
|
id: json["id"],
|
||||||
|
offset: json["offset"],
|
||||||
|
index: json["index"],
|
||||||
|
intro: json["intro"],
|
||||||
|
attr: json["attr"],
|
||||||
|
tid: json["tid"],
|
||||||
|
copyRight: json["copy_right"],
|
||||||
|
cntInfo: json["cnt_info"],
|
||||||
|
cover: json["cover"],
|
||||||
|
duration: json["duration"],
|
||||||
|
pubtime: json["pubtime"],
|
||||||
|
likeState: json["like_state"],
|
||||||
|
favState: json["fav_state"],
|
||||||
|
page: json["page"],
|
||||||
|
// json["pages"] 可能为null
|
||||||
|
pages: json["pages"] == null
|
||||||
|
? []
|
||||||
|
: List<Page>.from(json["pages"].map((x) => Page.fromJson(x))),
|
||||||
|
title: json["title"],
|
||||||
|
type: json["type"],
|
||||||
|
upper: Upper.fromJson(json["upper"]),
|
||||||
|
link: json["link"],
|
||||||
|
bvId: json["bv_id"],
|
||||||
|
shortLink: json["short_link"],
|
||||||
|
rights: Rights.fromJson(json["rights"]),
|
||||||
|
elecInfo: json["elec_info"],
|
||||||
|
coin: Coin.fromJson(json["coin"]),
|
||||||
|
progressPercent: json["progress_percent"].toDouble(),
|
||||||
|
badge: json["badge"],
|
||||||
|
forbidFav: json["forbid_fav"],
|
||||||
|
moreType: json["more_type"],
|
||||||
|
businessOid: json["business_oid"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coin {
|
||||||
|
Coin({
|
||||||
|
this.maxNum,
|
||||||
|
this.coinNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? maxNum;
|
||||||
|
int? coinNumber;
|
||||||
|
|
||||||
|
factory Coin.fromJson(Map<String, dynamic> json) => Coin(
|
||||||
|
maxNum: json["max_num"],
|
||||||
|
coinNumber: json["coin_number"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Page {
|
||||||
|
Page({
|
||||||
|
this.id,
|
||||||
|
this.title,
|
||||||
|
this.intro,
|
||||||
|
this.duration,
|
||||||
|
this.link,
|
||||||
|
this.page,
|
||||||
|
this.metas,
|
||||||
|
this.from,
|
||||||
|
this.dimension,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? id;
|
||||||
|
String? title;
|
||||||
|
String? intro;
|
||||||
|
int? duration;
|
||||||
|
String? link;
|
||||||
|
int? page;
|
||||||
|
List<Meta>? metas;
|
||||||
|
String? from;
|
||||||
|
Dimension? dimension;
|
||||||
|
|
||||||
|
factory Page.fromJson(Map<String, dynamic> json) => Page(
|
||||||
|
id: json["id"],
|
||||||
|
title: json["title"],
|
||||||
|
intro: json["intro"],
|
||||||
|
duration: json["duration"],
|
||||||
|
link: json["link"],
|
||||||
|
page: json["page"],
|
||||||
|
metas: List<Meta>.from(json["metas"].map((x) => Meta.fromJson(x))),
|
||||||
|
from: json["from"],
|
||||||
|
dimension: Dimension.fromJson(json["dimension"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dimension {
|
||||||
|
Dimension({
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.rotate,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? width;
|
||||||
|
int? height;
|
||||||
|
int? rotate;
|
||||||
|
|
||||||
|
factory Dimension.fromJson(Map<String, dynamic> json) => Dimension(
|
||||||
|
width: json["width"],
|
||||||
|
height: json["height"],
|
||||||
|
rotate: json["rotate"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Meta {
|
||||||
|
Meta({
|
||||||
|
this.quality,
|
||||||
|
this.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? quality;
|
||||||
|
int? size;
|
||||||
|
|
||||||
|
factory Meta.fromJson(Map<String, dynamic> json) => Meta(
|
||||||
|
quality: json["quality"],
|
||||||
|
size: json["size"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Rights {
|
||||||
|
Rights({
|
||||||
|
this.bp,
|
||||||
|
this.elec,
|
||||||
|
this.download,
|
||||||
|
this.movie,
|
||||||
|
this.pay,
|
||||||
|
this.ugcPay,
|
||||||
|
this.hd5,
|
||||||
|
this.noReprint,
|
||||||
|
this.autoplay,
|
||||||
|
this.noBackground,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? bp;
|
||||||
|
int? elec;
|
||||||
|
int? download;
|
||||||
|
int? movie;
|
||||||
|
int? pay;
|
||||||
|
int? ugcPay;
|
||||||
|
int? hd5;
|
||||||
|
int? noReprint;
|
||||||
|
int? autoplay;
|
||||||
|
int? noBackground;
|
||||||
|
|
||||||
|
factory Rights.fromJson(Map<String, dynamic> json) => Rights(
|
||||||
|
bp: json["bp"],
|
||||||
|
elec: json["elec"],
|
||||||
|
download: json["download"],
|
||||||
|
movie: json["movie"],
|
||||||
|
pay: json["pay"],
|
||||||
|
ugcPay: json["ugc_pay"],
|
||||||
|
hd5: json["hd5"],
|
||||||
|
noReprint: json["no_reprint"],
|
||||||
|
autoplay: json["autoplay"],
|
||||||
|
noBackground: json["no_background"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Upper {
|
||||||
|
Upper({
|
||||||
|
this.mid,
|
||||||
|
this.name,
|
||||||
|
this.face,
|
||||||
|
this.followed,
|
||||||
|
this.fans,
|
||||||
|
this.vipType,
|
||||||
|
this.vipStatue,
|
||||||
|
this.vipDueDate,
|
||||||
|
this.vipPayType,
|
||||||
|
this.officialRole,
|
||||||
|
this.officialTitle,
|
||||||
|
this.officialDesc,
|
||||||
|
this.displayName,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? mid;
|
||||||
|
String? name;
|
||||||
|
String? face;
|
||||||
|
int? followed;
|
||||||
|
int? fans;
|
||||||
|
int? vipType;
|
||||||
|
int? vipStatue;
|
||||||
|
int? vipDueDate;
|
||||||
|
int? vipPayType;
|
||||||
|
int? officialRole;
|
||||||
|
String? officialTitle;
|
||||||
|
String? officialDesc;
|
||||||
|
String? displayName;
|
||||||
|
|
||||||
|
factory Upper.fromJson(Map<String, dynamic> json) => Upper(
|
||||||
|
mid: json["mid"],
|
||||||
|
name: json["name"],
|
||||||
|
face: json["face"],
|
||||||
|
followed: json["followed"],
|
||||||
|
fans: json["fans"],
|
||||||
|
vipType: json["vip_type"],
|
||||||
|
vipStatue: json["vip_statue"],
|
||||||
|
vipDueDate: json["vip_due_date"],
|
||||||
|
vipPayType: json["vip_pay_type"],
|
||||||
|
officialRole: json["official_role"],
|
||||||
|
officialTitle: json["official_title"],
|
||||||
|
officialDesc: json["official_desc"],
|
||||||
|
displayName: json["display_name"],
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -255,13 +255,11 @@ class _BangumiInfoState extends State<BangumiInfo> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
StatView(
|
StatView(
|
||||||
theme: 'gray',
|
|
||||||
view: widget.bangumiDetail!.stat!['views'],
|
view: widget.bangumiDetail!.stat!['views'],
|
||||||
size: 'medium',
|
size: 'medium',
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
StatDanMu(
|
StatDanMu(
|
||||||
theme: 'gray',
|
|
||||||
danmu: widget.bangumiDetail!.stat!['danmakus'],
|
danmu: widget.bangumiDetail!.stat!['danmakus'],
|
||||||
size: 'medium',
|
size: 'medium',
|
||||||
),
|
),
|
||||||
|
|||||||
@ -60,13 +60,11 @@ class IntroDetail extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
StatView(
|
StatView(
|
||||||
theme: 'gray',
|
|
||||||
view: bangumiDetail!.stat!['views'],
|
view: bangumiDetail!.stat!['views'],
|
||||||
size: 'medium',
|
size: 'medium',
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
StatDanMu(
|
StatDanMu(
|
||||||
theme: 'gray',
|
|
||||||
danmu: bangumiDetail!.stat!['danmakus'],
|
danmu: bangumiDetail!.stat!['danmakus'],
|
||||||
size: 'medium',
|
size: 'medium',
|
||||||
),
|
),
|
||||||
|
|||||||
@ -146,20 +146,26 @@ class DynamicsController extends GetxController {
|
|||||||
/// 专栏文章查看
|
/// 专栏文章查看
|
||||||
case 'DYNAMIC_TYPE_ARTICLE':
|
case 'DYNAMIC_TYPE_ARTICLE':
|
||||||
String title = item.modules.moduleDynamic.major.opus.title;
|
String title = item.modules.moduleDynamic.major.opus.title;
|
||||||
String url = item.modules.moduleDynamic.major.opus.jumpUrl;
|
String jumpUrl = item.modules.moduleDynamic.major.opus.jumpUrl;
|
||||||
if (url.contains('opus') || url.contains('read')) {
|
String url =
|
||||||
|
jumpUrl.startsWith('//') ? jumpUrl.split('//').last : jumpUrl;
|
||||||
|
if (jumpUrl.contains('opus') || jumpUrl.contains('read')) {
|
||||||
RegExp digitRegExp = RegExp(r'\d+');
|
RegExp digitRegExp = RegExp(r'\d+');
|
||||||
Iterable<Match> matches = digitRegExp.allMatches(url);
|
Iterable<Match> matches = digitRegExp.allMatches(jumpUrl);
|
||||||
String number = matches.first.group(0)!;
|
String number = matches.first.group(0)!;
|
||||||
if (url.contains('read')) {
|
if (jumpUrl.contains('read')) {
|
||||||
number = 'cv$number';
|
Get.toNamed('/read', parameters: {
|
||||||
}
|
|
||||||
Get.toNamed('/htmlRender', parameters: {
|
|
||||||
'url': url.startsWith('//') ? url.split('//').last : url,
|
|
||||||
'title': title,
|
'title': title,
|
||||||
'id': number,
|
'id': number,
|
||||||
'dynamicType': url.split('//').last.split('/')[1]
|
'articleType': url.split('/')[1]
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
Get.toNamed('/opus', parameters: {
|
||||||
|
'title': title,
|
||||||
|
'id': number,
|
||||||
|
'articleType': 'opus'
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Get.toNamed(
|
Get.toNamed(
|
||||||
'/webview',
|
'/webview',
|
||||||
|
|||||||
@ -86,7 +86,7 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
|
|||||||
height: width / StyleString.aspectRatio,
|
height: width / StyleString.aspectRatio,
|
||||||
src: content.cover,
|
src: content.cover,
|
||||||
),
|
),
|
||||||
if (content.badge != null && type == 'pgc')
|
if (content.badge != null && content.badge['text'] != null)
|
||||||
PBadge(
|
PBadge(
|
||||||
text: content.badge['text'],
|
text: content.badge['text'],
|
||||||
top: 8.0,
|
top: 8.0,
|
||||||
|
|||||||
@ -55,6 +55,22 @@ class _FavPageState extends State<FavPage> {
|
|||||||
tooltip: 'Ta的订阅',
|
tooltip: 'Ta的订阅',
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink()),
|
: const SizedBox.shrink()),
|
||||||
|
|
||||||
|
// 新建收藏夹
|
||||||
|
Obx(() => _favController.isOwner.value
|
||||||
|
? IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await Get.toNamed('/favEdit');
|
||||||
|
_favController.hasMore.value = true;
|
||||||
|
_favController.currentPage = 1;
|
||||||
|
setState(() {
|
||||||
|
_futureBuilderFuture = _favController.queryFavFolder();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add_outlined),
|
||||||
|
tooltip: '新建收藏夹',
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink()),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Get.toNamed(
|
onPressed: () => Get.toNamed(
|
||||||
'/favSearch?searchType=1&mediaId=${_favController.favFolderData.value.list!.first.id}'),
|
'/favSearch?searchType=1&mediaId=${_favController.favFolderData.value.list!.first.id}'),
|
||||||
|
|||||||
@ -74,7 +74,7 @@ class VideoContent extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
|
padding: const EdgeInsets.fromLTRB(10, 2, 6, 10),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -94,6 +94,15 @@ class VideoContent extends StatelessWidget {
|
|||||||
color: Theme.of(context).colorScheme.outline,
|
color: Theme.of(context).colorScheme.outline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
[23, 1].contains(favFolderItem.attr) ? '私密' : '公开',
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -6,17 +6,17 @@ import 'package:pilipala/http/video.dart';
|
|||||||
import 'package:pilipala/models/user/fav_detail.dart';
|
import 'package:pilipala/models/user/fav_detail.dart';
|
||||||
import 'package:pilipala/models/user/fav_folder.dart';
|
import 'package:pilipala/models/user/fav_folder.dart';
|
||||||
import 'package:pilipala/pages/fav/index.dart';
|
import 'package:pilipala/pages/fav/index.dart';
|
||||||
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
|
||||||
class FavDetailController extends GetxController {
|
class FavDetailController extends GetxController {
|
||||||
FavFolderItemData? item;
|
FavFolderItemData? item;
|
||||||
Rx<FavDetailData> favDetailData = FavDetailData().obs;
|
|
||||||
|
|
||||||
int? mediaId;
|
int? mediaId;
|
||||||
late String heroTag;
|
late String heroTag;
|
||||||
int currentPage = 1;
|
int currentPage = 1;
|
||||||
bool isLoadingMore = false;
|
bool isLoadingMore = false;
|
||||||
RxMap favInfo = {}.obs;
|
RxMap favInfo = {}.obs;
|
||||||
RxList favList = [].obs;
|
RxList<FavDetailItemData> favList = <FavDetailItemData>[].obs;
|
||||||
RxString loadingText = '加载中...'.obs;
|
RxString loadingText = '加载中...'.obs;
|
||||||
RxInt mediaCount = 0.obs;
|
RxInt mediaCount = 0.obs;
|
||||||
late String isOwner;
|
late String isOwner;
|
||||||
@ -115,4 +115,35 @@ class FavDetailController extends GetxController {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onEditFavFolder() async {
|
||||||
|
Get.toNamed(
|
||||||
|
'/favEdit',
|
||||||
|
arguments: {
|
||||||
|
'mediaId': mediaId.toString(),
|
||||||
|
'title': item!.title,
|
||||||
|
'intro': item!.intro,
|
||||||
|
'cover': item!.cover,
|
||||||
|
'privacy': item!.attr,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future toViewPlayAll() async {
|
||||||
|
final FavDetailItemData firstItem = favList.first;
|
||||||
|
final String heroTag = Utils.makeHeroTag(firstItem.bvid);
|
||||||
|
Get.toNamed(
|
||||||
|
'/video?bvid=${firstItem.bvid}&cid=${firstItem.cid}',
|
||||||
|
arguments: {
|
||||||
|
'videoItem': firstItem,
|
||||||
|
'heroTag': heroTag,
|
||||||
|
'sourceType': 'fav',
|
||||||
|
'mediaId': favInfo['id'],
|
||||||
|
'oid': firstItem.id,
|
||||||
|
'favTitle': favInfo['title'],
|
||||||
|
'favInfo': favInfo,
|
||||||
|
'count': favInfo['media_count'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,6 +106,11 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
|||||||
position: PopupMenuPosition.under,
|
position: PopupMenuPosition.under,
|
||||||
onSelected: (String type) {},
|
onSelected: (String type) {},
|
||||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
onTap: () => _favDetailController.onEditFavFolder(),
|
||||||
|
value: 'edit',
|
||||||
|
child: const Text('编辑收藏夹'),
|
||||||
|
),
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
onTap: () => _favDetailController.onDelFavFolder(),
|
onTap: () => _favDetailController.onDelFavFolder(),
|
||||||
value: 'pause',
|
value: 'pause',
|
||||||
@ -255,6 +260,15 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
floatingActionButton: Obx(
|
||||||
|
() => _favDetailController.mediaCount > 0
|
||||||
|
? FloatingActionButton.extended(
|
||||||
|
onPressed: _favDetailController.toViewPlayAll,
|
||||||
|
label: const Text('播放全部'),
|
||||||
|
icon: const Icon(Icons.playlist_play),
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -203,13 +203,9 @@ class VideoContent extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.only(top: 2),
|
padding: const EdgeInsets.only(top: 2),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
StatView(
|
StatView(view: videoItem.cntInfo['play']),
|
||||||
theme: 'gray',
|
|
||||||
view: videoItem.cntInfo['play'],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
StatDanMu(
|
StatDanMu(danmu: videoItem.cntInfo['danmaku']),
|
||||||
theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
77
lib/pages/fav_edit/controller.dart
Normal file
77
lib/pages/fav_edit/controller.dart
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/http/fav.dart';
|
||||||
|
|
||||||
|
class FavEditController extends GetxController {
|
||||||
|
final GlobalKey formKey = GlobalKey<FormState>();
|
||||||
|
final TextEditingController titleController = TextEditingController();
|
||||||
|
final TextEditingController contentController = TextEditingController();
|
||||||
|
|
||||||
|
final FocusNode titleTextFieldNode = FocusNode();
|
||||||
|
final FocusNode contentTextFieldNode = FocusNode();
|
||||||
|
|
||||||
|
// 默认新建
|
||||||
|
RxString type = 'add'.obs;
|
||||||
|
|
||||||
|
String? mediaId;
|
||||||
|
String cover = ''; // 封面
|
||||||
|
String title = ''; // 名称
|
||||||
|
String intro = ''; // 简介
|
||||||
|
RxInt privacy = 0.obs; // 是否公开 0公开 1私密
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
var args = Get.arguments;
|
||||||
|
if (args != null) {
|
||||||
|
type.value = 'edit';
|
||||||
|
mediaId = args['mediaId'];
|
||||||
|
title = args['title'];
|
||||||
|
intro = args['intro'];
|
||||||
|
cover = args['cover'];
|
||||||
|
privacy.value = args['privacy'];
|
||||||
|
titleController.text = title;
|
||||||
|
contentController.text = intro;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSubmit() async {
|
||||||
|
// 表单验证
|
||||||
|
if ((formKey.currentState as FormState).validate()) {
|
||||||
|
if (type.value == 'edit') {
|
||||||
|
await editFolder();
|
||||||
|
} else {
|
||||||
|
await addFolder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> editFolder() async {
|
||||||
|
var res = await FavHttp.editFolder(
|
||||||
|
title: title,
|
||||||
|
intro: intro,
|
||||||
|
mediaId: mediaId!,
|
||||||
|
cover: cover,
|
||||||
|
);
|
||||||
|
if (res['status']) {
|
||||||
|
SmartDialog.showToast('编辑成功');
|
||||||
|
Get.back();
|
||||||
|
} else {
|
||||||
|
SmartDialog.showToast(res['msg']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addFolder() async {
|
||||||
|
var res = await FavHttp.addFolder(
|
||||||
|
title: title,
|
||||||
|
intro: intro,
|
||||||
|
);
|
||||||
|
if (res['status']) {
|
||||||
|
SmartDialog.showToast('新建成功');
|
||||||
|
Get.back();
|
||||||
|
} else {
|
||||||
|
SmartDialog.showToast(res['msg']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
lib/pages/fav_edit/index.dart
Normal file
4
lib/pages/fav_edit/index.dart
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
library fav_edit;
|
||||||
|
|
||||||
|
export './controller.dart';
|
||||||
|
export './view.dart';
|
||||||
111
lib/pages/fav_edit/view.dart
Normal file
111
lib/pages/fav_edit/view.dart
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import 'controller.dart';
|
||||||
|
|
||||||
|
class FavEditPage extends StatefulWidget {
|
||||||
|
const FavEditPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FavEditPage> createState() => _FavEditPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavEditPageState extends State<FavEditPage> {
|
||||||
|
final FavEditController _favEditController = Get.put(FavEditController());
|
||||||
|
String title = '';
|
||||||
|
String content = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: Obx(
|
||||||
|
() => _favEditController.type.value == 'add'
|
||||||
|
? Text(
|
||||||
|
'新建收藏夹',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'编辑收藏夹',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: false,
|
||||||
|
actions: [
|
||||||
|
Obx(
|
||||||
|
() => _favEditController.privacy.value == 0
|
||||||
|
? IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_favEditController.privacy.value = 1;
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.lock_open_outlined))
|
||||||
|
: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_favEditController.privacy.value = 0;
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.lock_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _favEditController.onSubmit, child: const Text('保存')),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Form(
|
||||||
|
key: _favEditController.formKey, //设置globalKey,用于后面获取FormState
|
||||||
|
autovalidateMode: AutovalidateMode.disabled,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 10, 14, 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor.withOpacity(0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: TextFormField(
|
||||||
|
autofocus: true,
|
||||||
|
controller: _favEditController.titleController,
|
||||||
|
focusNode: _favEditController.titleTextFieldNode,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: "收藏夹名称",
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
),
|
||||||
|
// 校验标题
|
||||||
|
validator: (v) {
|
||||||
|
return v!.trim().isNotEmpty ? null : "请输入收藏夹名称";
|
||||||
|
},
|
||||||
|
onChanged: (val) {
|
||||||
|
_favEditController.title = val;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 14, vertical: 5),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _favEditController.contentController,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 5,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: '输入收藏夹简介', border: InputBorder.none),
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
onChanged: (val) {
|
||||||
|
_favEditController.intro = val;
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -49,7 +49,10 @@ class FollowItem extends StatelessWidget {
|
|||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await showFlexibleBottomSheet(
|
await showFlexibleBottomSheet(
|
||||||
bottomSheetColor: Colors.transparent,
|
bottomSheetBorderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
),
|
||||||
minHeight: 1,
|
minHeight: 1,
|
||||||
initHeight: 1,
|
initHeight: 1,
|
||||||
maxHeight: 1,
|
maxHeight: 1,
|
||||||
|
|||||||
@ -43,14 +43,17 @@ class HistoryItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
if (videoItem.history.business.contains('article')) {
|
if (videoItem.history.business.contains('article')) {
|
||||||
int cid = videoItem.history.cid ??
|
int cid = videoItem.history.cid ??
|
||||||
// videoItem.history.oid ??
|
videoItem.history.oid ??
|
||||||
await SearchHttp.ab2c(aid: aid, bvid: bvid);
|
await SearchHttp.ab2c(aid: aid, bvid: bvid);
|
||||||
|
if (cid == -1) {
|
||||||
|
return SmartDialog.showToast('无法获取文章内容');
|
||||||
|
}
|
||||||
Get.toNamed(
|
Get.toNamed(
|
||||||
'/webview',
|
'/read',
|
||||||
parameters: {
|
parameters: {
|
||||||
'url': 'https://www.bilibili.com/read/cv$cid',
|
'title': videoItem.title,
|
||||||
'type': 'note',
|
'id': cid.toString(),
|
||||||
'pageTitle': videoItem.title
|
'articleType': 'read',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else if (videoItem.history.business == 'live') {
|
} else if (videoItem.history.business == 'live') {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import 'package:pilipala/http/user.dart';
|
|||||||
import 'package:pilipala/models/model_hot_video_item.dart';
|
import 'package:pilipala/models/model_hot_video_item.dart';
|
||||||
import 'package:pilipala/models/user/info.dart';
|
import 'package:pilipala/models/user/info.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
|
||||||
class LaterController extends GetxController {
|
class LaterController extends GetxController {
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
@ -48,7 +49,7 @@ class LaterController extends GetxController {
|
|||||||
aid != null ? '即将移除该视频,确定是否移除' : '即将删除所有已观看视频,此操作不可恢复。确定是否删除?'),
|
aid != null ? '即将移除该视频,确定是否移除' : '即将删除所有已观看视频,此操作不可恢复。确定是否删除?'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => SmartDialog.dismiss(),
|
onPressed: SmartDialog.dismiss,
|
||||||
child: Text(
|
child: Text(
|
||||||
'取消',
|
'取消',
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||||
@ -87,7 +88,7 @@ class LaterController extends GetxController {
|
|||||||
content: const Text('确定要清空你的稍后再看列表吗?'),
|
content: const Text('确定要清空你的稍后再看列表吗?'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => SmartDialog.dismiss(),
|
onPressed: SmartDialog.dismiss,
|
||||||
child: Text(
|
child: Text(
|
||||||
'取消',
|
'取消',
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||||
@ -109,4 +110,19 @@ class LaterController extends GetxController {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 稍后再看播放全部
|
||||||
|
Future toViewPlayAll() async {
|
||||||
|
final HotVideoItemModel firstItem = laterList.first;
|
||||||
|
final String heroTag = Utils.makeHeroTag(firstItem.bvid);
|
||||||
|
Get.toNamed(
|
||||||
|
'/video?bvid=${firstItem.bvid}&cid=${firstItem.cid}',
|
||||||
|
arguments: {
|
||||||
|
'videoItem': firstItem,
|
||||||
|
'heroTag': heroTag,
|
||||||
|
'sourceType': 'watchLater',
|
||||||
|
'count': laterList.length,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -128,6 +128,15 @@ class _LaterPageState extends State<LaterPage> {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
floatingActionButton: Obx(
|
||||||
|
() => _laterController.laterList.isNotEmpty
|
||||||
|
? FloatingActionButton.extended(
|
||||||
|
onPressed: _laterController.toViewPlayAll,
|
||||||
|
label: const Text('播放全部'),
|
||||||
|
icon: const Icon(Icons.playlist_play),
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/http/live.dart';
|
import 'package:pilipala/http/live.dart';
|
||||||
|
import 'package:pilipala/models/live/follow.dart';
|
||||||
import 'package:pilipala/models/live/item.dart';
|
import 'package:pilipala/models/live/item.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
|
||||||
@ -11,6 +12,8 @@ class LiveController extends GetxController {
|
|||||||
int _currentPage = 1;
|
int _currentPage = 1;
|
||||||
RxInt crossAxisCount = 2.obs;
|
RxInt crossAxisCount = 2.obs;
|
||||||
RxList<LiveItemModel> liveList = <LiveItemModel>[].obs;
|
RxList<LiveItemModel> liveList = <LiveItemModel>[].obs;
|
||||||
|
RxList<LiveFollowingItemModel> liveFollowingList =
|
||||||
|
<LiveFollowingItemModel>[].obs;
|
||||||
bool flag = false;
|
bool flag = false;
|
||||||
OverlayEntry? popupDialog;
|
OverlayEntry? popupDialog;
|
||||||
Box setting = GStrorage.setting;
|
Box setting = GStrorage.setting;
|
||||||
@ -44,6 +47,7 @@ class LiveController extends GetxController {
|
|||||||
// 下拉刷新
|
// 下拉刷新
|
||||||
Future onRefresh() async {
|
Future onRefresh() async {
|
||||||
queryLiveList('init');
|
queryLiveList('init');
|
||||||
|
fetchLiveFollowing();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上拉加载
|
// 上拉加载
|
||||||
@ -61,4 +65,17 @@ class LiveController extends GetxController {
|
|||||||
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
|
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
Future fetchLiveFollowing() async {
|
||||||
|
var res = await LiveHttp.liveFollowing(pn: 1, ps: 20);
|
||||||
|
if (res['status']) {
|
||||||
|
liveFollowingList.value =
|
||||||
|
(res['data'].list as List<LiveFollowingItemModel>)
|
||||||
|
.where((LiveFollowingItemModel item) =>
|
||||||
|
item.liveStatus == 1 && item.recordLiveTime == 0) // 根据条件过滤
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import 'package:get/get.dart';
|
|||||||
import 'package:pilipala/common/constants.dart';
|
import 'package:pilipala/common/constants.dart';
|
||||||
import 'package:pilipala/common/skeleton/video_card_v.dart';
|
import 'package:pilipala/common/skeleton/video_card_v.dart';
|
||||||
import 'package:pilipala/common/widgets/http_error.dart';
|
import 'package:pilipala/common/widgets/http_error.dart';
|
||||||
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
|
import 'package:pilipala/models/live/follow.dart';
|
||||||
import 'package:pilipala/utils/main_stream.dart';
|
import 'package:pilipala/utils/main_stream.dart';
|
||||||
|
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
@ -22,6 +24,7 @@ class _LivePageState extends State<LivePage>
|
|||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
final LiveController _liveController = Get.put(LiveController());
|
final LiveController _liveController = Get.put(LiveController());
|
||||||
late Future _futureBuilderFuture;
|
late Future _futureBuilderFuture;
|
||||||
|
late Future _futureBuilderFuture2;
|
||||||
late ScrollController scrollController;
|
late ScrollController scrollController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -31,6 +34,7 @@ class _LivePageState extends State<LivePage>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_futureBuilderFuture = _liveController.queryLiveList('init');
|
_futureBuilderFuture = _liveController.queryLiveList('init');
|
||||||
|
_futureBuilderFuture2 = _liveController.fetchLiveFollowing();
|
||||||
scrollController = _liveController.scrollController;
|
scrollController = _liveController.scrollController;
|
||||||
scrollController.addListener(
|
scrollController.addListener(
|
||||||
() {
|
() {
|
||||||
@ -69,6 +73,7 @@ class _LivePageState extends State<LivePage>
|
|||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _liveController.scrollController,
|
controller: _liveController.scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
|
buildFollowingList(),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
// 单列布局 EdgeInsets.zero
|
// 单列布局 EdgeInsets.zero
|
||||||
padding:
|
padding:
|
||||||
@ -147,4 +152,148 @@ class _LivePageState extends State<LivePage>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关注的up直播
|
||||||
|
Widget buildFollowingList() {
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Obx(
|
||||||
|
() => Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
const TextSpan(
|
||||||
|
text: ' 我的关注 ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' ${_liveController.liveFollowingList.length}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: '人正在直播',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FutureBuilder(
|
||||||
|
future: _futureBuilderFuture2,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
|
if (snapshot.data == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
Map? data = snapshot.data;
|
||||||
|
if (data?['status']) {
|
||||||
|
RxList list = _liveController.liveFollowingList;
|
||||||
|
// ignore: invalid_use_of_protected_member
|
||||||
|
return Obx(() => LiveFollowingListView(list: list.value));
|
||||||
|
} else {
|
||||||
|
return SizedBox(
|
||||||
|
height: 80,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
data?['msg'] ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LiveFollowingListView extends StatelessWidget {
|
||||||
|
final List list;
|
||||||
|
|
||||||
|
const LiveFollowingListView({super.key, required this.list});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 100,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final LiveFollowingItemModel item = list[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(3, 12, 3, 0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Get.toNamed(
|
||||||
|
'/liveRoom?roomid=${item.roomId}',
|
||||||
|
arguments: {
|
||||||
|
'liveItem': item,
|
||||||
|
'heroTag': item.roomId.toString()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 54,
|
||||||
|
height: 54,
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(27),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: NetworkImgLayer(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
type: 'avatar',
|
||||||
|
src: list[index].face,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
SizedBox(
|
||||||
|
width: 62,
|
||||||
|
child: Text(
|
||||||
|
list[index].uname,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: list.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,6 +48,7 @@ class LiveRoomController extends GetxController {
|
|||||||
// 直播间弹幕开关 默认打开
|
// 直播间弹幕开关 默认打开
|
||||||
RxBool danmakuSwitch = true.obs;
|
RxBool danmakuSwitch = true.obs;
|
||||||
late String buvid;
|
late String buvid;
|
||||||
|
RxBool isPortrait = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -58,11 +59,12 @@ class LiveRoomController extends GetxController {
|
|||||||
if (Get.arguments != null) {
|
if (Get.arguments != null) {
|
||||||
liveItem = Get.arguments['liveItem'];
|
liveItem = Get.arguments['liveItem'];
|
||||||
heroTag = Get.arguments['heroTag'] ?? '';
|
heroTag = Get.arguments['heroTag'] ?? '';
|
||||||
if (liveItem != null && liveItem.pic != null && liveItem.pic != '') {
|
if (liveItem != null) {
|
||||||
cover = liveItem.pic;
|
cover = (liveItem.pic != null && liveItem.pic != '')
|
||||||
}
|
? liveItem.pic
|
||||||
if (liveItem != null && liveItem.cover != null && liveItem.cover != '') {
|
: (liveItem.cover != null && liveItem.cover != '')
|
||||||
cover = liveItem.cover;
|
? liveItem.cover
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
Request.getBuvid().then((value) => buvid = value);
|
Request.getBuvid().then((value) => buvid = value);
|
||||||
}
|
}
|
||||||
@ -95,11 +97,13 @@ class LiveRoomController extends GetxController {
|
|||||||
autoplay: true,
|
autoplay: true,
|
||||||
);
|
);
|
||||||
plPlayerController.isOpenDanmu.value = danmakuSwitch.value;
|
plPlayerController.isOpenDanmu.value = danmakuSwitch.value;
|
||||||
|
heartBeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future queryLiveInfo() async {
|
Future queryLiveInfo() async {
|
||||||
var res = await LiveHttp.liveRoomInfo(roomId: roomId, qn: currentQn);
|
var res = await LiveHttp.liveRoomInfo(roomId: roomId, qn: currentQn);
|
||||||
if (res['status']) {
|
if (res['status']) {
|
||||||
|
isPortrait.value = res['data'].isPortrait;
|
||||||
List<CodecItem> codec =
|
List<CodecItem> codec =
|
||||||
res['data'].playurlInfo.playurl.stream.first.format.first.codec;
|
res['data'].playurlInfo.playurl.stream.first.format.first.codec;
|
||||||
CodecItem item = codec.first;
|
CodecItem item = codec.first;
|
||||||
@ -278,8 +282,20 @@ class LiveRoomController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 历史记录
|
||||||
|
void heartBeat() {
|
||||||
|
LiveHttp.liveRoomEntry(roomId: roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
String encodeToBase64(String input) {
|
||||||
|
List<int> bytes = utf8.encode(input);
|
||||||
|
String base64Str = base64.encode(bytes);
|
||||||
|
return base64Str;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
|
heartBeat();
|
||||||
plSocket?.onClose();
|
plSocket?.onClose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,6 +115,9 @@ class _LiveRoomPageState extends State<LiveRoomPage>
|
|||||||
plPlayerController = _liveRoomController.plPlayerController;
|
plPlayerController = _liveRoomController.plPlayerController;
|
||||||
return PLVideoPlayer(
|
return PLVideoPlayer(
|
||||||
controller: plPlayerController,
|
controller: plPlayerController,
|
||||||
|
alignment: _liveRoomController.isPortrait.value
|
||||||
|
? Alignment.topCenter
|
||||||
|
: Alignment.center,
|
||||||
bottomControl: BottomControl(
|
bottomControl: BottomControl(
|
||||||
controller: plPlayerController,
|
controller: plPlayerController,
|
||||||
liveRoomCtr: _liveRoomController,
|
liveRoomCtr: _liveRoomController,
|
||||||
@ -178,10 +181,83 @@ class _LiveRoomPageState extends State<LiveRoomPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AppBar(
|
Obx(
|
||||||
|
() => SizedBox(
|
||||||
|
height: MediaQuery.of(context).padding.top +
|
||||||
|
(_liveRoomController.isPortrait.value ||
|
||||||
|
MediaQuery.of(context).orientation ==
|
||||||
|
Orientation.landscape
|
||||||
|
? 0
|
||||||
|
: kToolbarHeight),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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: Obx(
|
||||||
|
() => Container(
|
||||||
|
width: Get.size.width,
|
||||||
|
height: MediaQuery.of(context).orientation ==
|
||||||
|
Orientation.landscape
|
||||||
|
? Get.size.height
|
||||||
|
: !_liveRoomController.isPortrait.value
|
||||||
|
? Get.size.width * 9 / 16
|
||||||
|
: Get.size.height -
|
||||||
|
MediaQuery.of(context).padding.top,
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||||
|
),
|
||||||
|
child: videoPlayerPanel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// 定位 快速滑动到底部
|
||||||
|
Positioned(
|
||||||
|
right: 20,
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom + 80,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: const Offset(0, 4),
|
||||||
|
end: const Offset(0, 0),
|
||||||
|
).animate(CurvedAnimation(
|
||||||
|
parent: fabAnimationCtr,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
)),
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
_scrollToBottom();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.keyboard_arrow_down), // 图标
|
||||||
|
label: const Text('新消息'), // 文字
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
// primary: Colors.blue, // 按钮背景颜色
|
||||||
|
// onPrimary: Colors.white, // 按钮文字颜色
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 12, 20, 12), // 按钮内边距
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 顶栏
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: AppBar(
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
titleSpacing: 0,
|
titleSpacing: 0,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
@ -213,8 +289,8 @@ class _LiveRoomPageState extends State<LiveRoomPage>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_liveRoomController.roomInfoH5.value
|
_liveRoomController.roomInfoH5.value.anchorInfo!
|
||||||
.anchorInfo!.baseInfo!.uname!,
|
.baseInfo!.uname!,
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 1),
|
const SizedBox(height: 1),
|
||||||
@ -238,77 +314,33 @@ class _LiveRoomPageState extends State<LiveRoomPage>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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: SizedBox(
|
|
||||||
width: Get.size.width,
|
|
||||||
height: MediaQuery.of(context).orientation ==
|
|
||||||
Orientation.landscape
|
|
||||||
? Get.size.height
|
|
||||||
: Get.size.width * 9 / 16,
|
|
||||||
child: videoPlayerPanel,
|
|
||||||
),
|
),
|
||||||
),
|
// 消息列表
|
||||||
// 显示消息的列表
|
Obx(
|
||||||
buildMessageListUI(
|
() => Positioned(
|
||||||
|
top: MediaQuery.of(context).padding.top +
|
||||||
|
kToolbarHeight +
|
||||||
|
(_liveRoomController.isPortrait.value
|
||||||
|
? Get.size.width
|
||||||
|
: Get.size.width * 9 / 16),
|
||||||
|
bottom: 90 + MediaQuery.of(context).padding.bottom,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: buildMessageListUI(
|
||||||
context,
|
context,
|
||||||
_liveRoomController,
|
_liveRoomController,
|
||||||
_scrollController,
|
_scrollController,
|
||||||
),
|
),
|
||||||
// Container(
|
),
|
||||||
// padding: const EdgeInsets.only(
|
),
|
||||||
// left: 14, right: 14, top: 4, bottom: 4),
|
// 消息输入框
|
||||||
// margin: const EdgeInsets.only(
|
Visibility(
|
||||||
// bottom: 6,
|
visible: MediaQuery.of(context).orientation == Orientation.portrait,
|
||||||
// left: 14,
|
child: Positioned(
|
||||||
// ),
|
bottom: 0,
|
||||||
// decoration: BoxDecoration(
|
left: 0,
|
||||||
// color: Colors.grey.withOpacity(0.1),
|
right: 0,
|
||||||
// borderRadius: const BorderRadius.all(Radius.circular(20)),
|
child: Container(
|
||||||
// ),
|
|
||||||
// child: Obx(
|
|
||||||
// () => AnimatedSwitcher(
|
|
||||||
// duration: const Duration(milliseconds: 300),
|
|
||||||
// transitionBuilder:
|
|
||||||
// (Widget child, Animation<double> animation) {
|
|
||||||
// return FadeTransition(opacity: animation, child: child);
|
|
||||||
// },
|
|
||||||
// child: Text.rich(
|
|
||||||
// key:
|
|
||||||
// ValueKey(_liveRoomController.joinRoomTip['userName']),
|
|
||||||
// TextSpan(
|
|
||||||
// style: const TextStyle(color: Colors.white),
|
|
||||||
// children: [
|
|
||||||
// TextSpan(
|
|
||||||
// text:
|
|
||||||
// '${_liveRoomController.joinRoomTip['userName']} ',
|
|
||||||
// style: TextStyle(
|
|
||||||
// color: Colors.white.withOpacity(0.6),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// TextSpan(
|
|
||||||
// text:
|
|
||||||
// '${_liveRoomController.joinRoomTip['message']}',
|
|
||||||
// style: const TextStyle(color: Colors.white),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
// 弹幕输入框
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: 14,
|
left: 14,
|
||||||
right: 14,
|
right: 14,
|
||||||
@ -384,32 +416,6 @@ class _LiveRoomPageState extends State<LiveRoomPage>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
// 定位 快速滑动到底部
|
|
||||||
Positioned(
|
|
||||||
right: 20,
|
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 80,
|
|
||||||
child: SlideTransition(
|
|
||||||
position: Tween<Offset>(
|
|
||||||
begin: const Offset(0, 4),
|
|
||||||
end: const Offset(0, 0),
|
|
||||||
).animate(CurvedAnimation(
|
|
||||||
parent: fabAnimationCtr,
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
)),
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
_scrollToBottom();
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.keyboard_arrow_down), // 图标
|
|
||||||
label: const Text('新消息'), // 文字
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
// primary: Colors.blue, // 按钮背景颜色
|
|
||||||
// onPrimary: Colors.white, // 按钮文字颜色
|
|
||||||
padding: const EdgeInsets.fromLTRB(14, 12, 20, 12), // 按钮内边距
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -467,7 +473,9 @@ Widget buildMessageListUI(
|
|||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.withOpacity(0.1),
|
color: liveRoomController.isPortrait.value
|
||||||
|
? Colors.black.withOpacity(0.3)
|
||||||
|
: Colors.grey.withOpacity(0.1),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
),
|
),
|
||||||
margin: EdgeInsets.only(
|
margin: EdgeInsets.only(
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import 'package:pilipala/pages/media/index.dart';
|
|||||||
import 'package:pilipala/pages/rank/index.dart';
|
import 'package:pilipala/pages/rank/index.dart';
|
||||||
import 'package:pilipala/utils/event_bus.dart';
|
import 'package:pilipala/utils/event_bus.dart';
|
||||||
import 'package:pilipala/utils/feed_back.dart';
|
import 'package:pilipala/utils/feed_back.dart';
|
||||||
import 'package:pilipala/utils/global_data.dart';
|
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
import './controller.dart';
|
import './controller.dart';
|
||||||
|
|
||||||
@ -30,6 +29,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
|||||||
|
|
||||||
int? _lastSelectTime; //上次点击时间
|
int? _lastSelectTime; //上次点击时间
|
||||||
Box setting = GStrorage.setting;
|
Box setting = GStrorage.setting;
|
||||||
|
late bool enableMYBar;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -37,6 +37,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
|||||||
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
|
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
_mainController.pageController =
|
_mainController.pageController =
|
||||||
PageController(initialPage: _mainController.selectedIndex);
|
PageController(initialPage: _mainController.selectedIndex);
|
||||||
|
enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setIndex(int value) async {
|
void setIndex(int value) async {
|
||||||
@ -171,7 +172,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
|||||||
curve: Curves.easeInOutCubicEmphasized,
|
curve: Curves.easeInOutCubicEmphasized,
|
||||||
duration: const Duration(milliseconds: 500),
|
duration: const Duration(milliseconds: 500),
|
||||||
offset: Offset(0, snapshot.data ? 0 : 1),
|
offset: Offset(0, snapshot.data ? 0 : 1),
|
||||||
child: GlobalData().enableMYBar
|
child: enableMYBar
|
||||||
? Obx(
|
? Obx(
|
||||||
() => NavigationBar(
|
() => NavigationBar(
|
||||||
onDestinationSelected: (value) => setIndex(value),
|
onDestinationSelected: (value) => setIndex(value),
|
||||||
|
|||||||
@ -240,4 +240,6 @@ class MemberController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void pushfavPage() => Get.toNamed('/fav?mid=$mid');
|
void pushfavPage() => Get.toNamed('/fav?mid=$mid');
|
||||||
|
// 跳转图文专栏
|
||||||
|
void pushArticlePage() => Get.toNamed('/memberArticle?mid=$mid');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -171,32 +171,44 @@ class _MemberPageState extends State<MemberPage>
|
|||||||
),
|
),
|
||||||
|
|
||||||
/// 视频
|
/// 视频
|
||||||
Obx(() => ListTile(
|
Obx(
|
||||||
|
() => ListTile(
|
||||||
onTap: _memberController.pushArchivesPage,
|
onTap: _memberController.pushArchivesPage,
|
||||||
title: Text(
|
title: Text(
|
||||||
'${_memberController.isOwner.value ? '我' : 'Ta'}的投稿'),
|
'${_memberController.isOwner.value ? '我' : 'Ta'}的投稿'),
|
||||||
trailing: const Icon(Icons.arrow_forward_outlined,
|
trailing:
|
||||||
size: 19),
|
const Icon(Icons.arrow_forward_outlined, size: 19),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
/// 他的收藏夹
|
/// 他的收藏夹
|
||||||
Obx(() => ListTile(
|
Obx(
|
||||||
|
() => ListTile(
|
||||||
onTap: _memberController.pushfavPage,
|
onTap: _memberController.pushfavPage,
|
||||||
title: Text(
|
title: Text(
|
||||||
'${_memberController.isOwner.value ? '我' : 'Ta'}的收藏'),
|
'${_memberController.isOwner.value ? '我' : 'Ta'}的收藏'),
|
||||||
trailing: const Icon(Icons.arrow_forward_outlined,
|
trailing:
|
||||||
size: 19),
|
const Icon(Icons.arrow_forward_outlined, size: 19),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
/// 专栏
|
/// 专栏
|
||||||
Obx(() => ListTile(
|
Obx(
|
||||||
|
() => ListTile(
|
||||||
|
onTap: _memberController.pushArticlePage,
|
||||||
title: Text(
|
title: Text(
|
||||||
'${_memberController.isOwner.value ? '我' : 'Ta'}的专栏'))),
|
'${_memberController.isOwner.value ? '我' : 'Ta'}的专栏'),
|
||||||
|
trailing:
|
||||||
|
const Icon(Icons.arrow_forward_outlined, size: 19),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
/// 合集
|
/// 合集
|
||||||
Obx(() => ListTile(
|
Obx(
|
||||||
|
() => ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
'${_memberController.isOwner.value ? '我' : 'Ta'}的合集'))),
|
'${_memberController.isOwner.value ? '我' : 'Ta'}的合集')),
|
||||||
|
),
|
||||||
MediaQuery.removePadding(
|
MediaQuery.removePadding(
|
||||||
removeTop: true,
|
removeTop: true,
|
||||||
removeBottom: true,
|
removeBottom: true,
|
||||||
|
|||||||
68
lib/pages/member_article/controller.dart
Normal file
68
lib/pages/member_article/controller.dart
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/http/member.dart';
|
||||||
|
import 'package:pilipala/models/member/article.dart';
|
||||||
|
|
||||||
|
class MemberArticleController extends GetxController {
|
||||||
|
final ScrollController scrollController = ScrollController();
|
||||||
|
late int mid;
|
||||||
|
int pn = 1;
|
||||||
|
String? offset;
|
||||||
|
bool hasMore = true;
|
||||||
|
String? wWebid;
|
||||||
|
RxBool isLoading = false.obs;
|
||||||
|
RxList<MemberArticleItemModel> articleList = <MemberArticleItemModel>[].obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
mid = int.parse(Get.parameters['mid']!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取wWebid
|
||||||
|
Future getWWebid() async {
|
||||||
|
var res = await MemberHttp.getWWebid(mid: mid);
|
||||||
|
if (res['status']) {
|
||||||
|
wWebid = res['data'];
|
||||||
|
} else {
|
||||||
|
wWebid = '-1';
|
||||||
|
SmartDialog.showToast(res['msg']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future getMemberArticle(type) async {
|
||||||
|
if (isLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLoading.value = true;
|
||||||
|
if (wWebid == null) {
|
||||||
|
await getWWebid();
|
||||||
|
}
|
||||||
|
if (type == 'init') {
|
||||||
|
pn = 1;
|
||||||
|
articleList.clear();
|
||||||
|
}
|
||||||
|
var res = await MemberHttp.getMemberArticle(
|
||||||
|
mid: mid,
|
||||||
|
pn: pn,
|
||||||
|
offset: offset,
|
||||||
|
wWebid: wWebid!,
|
||||||
|
);
|
||||||
|
if (res['status']) {
|
||||||
|
offset = res['data'].offset;
|
||||||
|
hasMore = res['data'].hasMore!;
|
||||||
|
if (type == 'init') {
|
||||||
|
articleList.value = res['data'].items;
|
||||||
|
}
|
||||||
|
if (type == 'onLoad') {
|
||||||
|
articleList.addAll(res['data'].items);
|
||||||
|
}
|
||||||
|
pn += 1;
|
||||||
|
} else {
|
||||||
|
SmartDialog.showToast(res['msg']);
|
||||||
|
}
|
||||||
|
isLoading.value = false;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
lib/pages/member_article/index.dart
Normal file
4
lib/pages/member_article/index.dart
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
library member_article;
|
||||||
|
|
||||||
|
export './controller.dart';
|
||||||
|
export './view.dart';
|
||||||
176
lib/pages/member_article/view.dart
Normal file
176
lib/pages/member_article/view.dart
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import 'package:easy_debounce/easy_throttle.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/common/skeleton/skeleton.dart';
|
||||||
|
import 'package:pilipala/common/widgets/http_error.dart';
|
||||||
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
|
import 'package:pilipala/common/widgets/no_data.dart';
|
||||||
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
|
||||||
|
import 'controller.dart';
|
||||||
|
|
||||||
|
class MemberArticlePage extends StatefulWidget {
|
||||||
|
const MemberArticlePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MemberArticlePage> createState() => _MemberArticlePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MemberArticlePageState extends State<MemberArticlePage> {
|
||||||
|
late MemberArticleController _memberArticleController;
|
||||||
|
late Future _futureBuilderFuture;
|
||||||
|
late ScrollController scrollController;
|
||||||
|
late int mid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
mid = int.parse(Get.parameters['mid']!);
|
||||||
|
final String heroTag = Utils.makeHeroTag(mid);
|
||||||
|
_memberArticleController = Get.put(MemberArticleController(), tag: heroTag);
|
||||||
|
_futureBuilderFuture = _memberArticleController.getMemberArticle('init');
|
||||||
|
scrollController = _memberArticleController.scrollController;
|
||||||
|
|
||||||
|
scrollController.addListener(_scrollListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollListener() {
|
||||||
|
if (scrollController.position.pixels >=
|
||||||
|
scrollController.position.maxScrollExtent - 200) {
|
||||||
|
EasyThrottle.throttle(
|
||||||
|
'member_archives', const Duration(milliseconds: 500), () {
|
||||||
|
_memberArticleController.getMemberArticle('onLoad');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
titleSpacing: 0,
|
||||||
|
centerTitle: false,
|
||||||
|
title: const Text('Ta的图文', style: TextStyle(fontSize: 16)),
|
||||||
|
),
|
||||||
|
body: FutureBuilder(
|
||||||
|
future: _futureBuilderFuture,
|
||||||
|
builder: (BuildContext context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
|
if (snapshot.data != null) {
|
||||||
|
return _buildContent(snapshot.data as Map);
|
||||||
|
} else {
|
||||||
|
return _buildError(snapshot.data['msg']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: 10,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return _buildSkeleton();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(Map data) {
|
||||||
|
RxList list = _memberArticleController.articleList;
|
||||||
|
if (data['status']) {
|
||||||
|
return Obx(
|
||||||
|
() => list.isNotEmpty
|
||||||
|
? ListView.separated(
|
||||||
|
controller: scrollController,
|
||||||
|
itemCount: list.length,
|
||||||
|
separatorBuilder: (BuildContext context, int index) {
|
||||||
|
return Divider(
|
||||||
|
height: 10,
|
||||||
|
color: Theme.of(context).dividerColor.withOpacity(0.15),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return _buildListItem(list[index]);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: const CustomScrollView(
|
||||||
|
physics: NeverScrollableScrollPhysics(),
|
||||||
|
slivers: [
|
||||||
|
NoData(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _buildError(data['msg']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListItem(dynamic item) {
|
||||||
|
return ListTile(
|
||||||
|
onTap: () {
|
||||||
|
Get.toNamed('/opus', parameters: {
|
||||||
|
'title': item.content,
|
||||||
|
'id': item.opusId,
|
||||||
|
'articleType': 'opus',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
leading: NetworkImgLayer(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
type: 'emote',
|
||||||
|
src: item.cover['url'],
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
item.content,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
|
'${item.stat["like"]}人点赞',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildError(String errMsg) {
|
||||||
|
return CustomScrollView(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: HttpError(
|
||||||
|
errMsg: errMsg,
|
||||||
|
fn: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSkeleton() {
|
||||||
|
return Skeleton(
|
||||||
|
child: ListTile(
|
||||||
|
leading: Container(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Container(
|
||||||
|
height: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||||
|
),
|
||||||
|
subtitle: Container(
|
||||||
|
height: 11,
|
||||||
|
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -69,10 +69,7 @@ class MemberCoinsItem extends StatelessWidget {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
StatView(
|
StatView(view: coinItem.view),
|
||||||
view: coinItem.view,
|
|
||||||
theme: 'gray',
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
Utils.CustomStamp_str(
|
Utils.CustomStamp_str(
|
||||||
|
|||||||
@ -69,10 +69,7 @@ class MemberLikeItem extends StatelessWidget {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
StatView(
|
StatView(view: likeItem.stat!.view),
|
||||||
view: likeItem.stat!.view,
|
|
||||||
theme: 'gray',
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
Utils.CustomStamp_str(
|
Utils.CustomStamp_str(
|
||||||
|
|||||||
@ -78,10 +78,7 @@ class MemberSeasonsItem extends StatelessWidget {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
StatView(
|
StatView(view: seasonItem.view),
|
||||||
view: seasonItem.view,
|
|
||||||
theme: 'gray',
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
Utils.CustomStamp_str(
|
Utils.CustomStamp_str(
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pilipala/http/msg.dart';
|
import 'package:pilipala/http/msg.dart';
|
||||||
import 'package:pilipala/models/msg/system.dart';
|
import 'package:pilipala/models/msg/system.dart';
|
||||||
@ -5,18 +6,44 @@ import 'package:pilipala/models/msg/system.dart';
|
|||||||
class MessageSystemController extends GetxController {
|
class MessageSystemController extends GetxController {
|
||||||
RxList<MessageSystemModel> systemItems = <MessageSystemModel>[].obs;
|
RxList<MessageSystemModel> systemItems = <MessageSystemModel>[].obs;
|
||||||
|
|
||||||
Future queryMessageSystem({String type = 'init'}) async {
|
Future<void> queryAndProcessMessages({String type = 'init'}) async {
|
||||||
var res = await MsgHttp.messageSystem();
|
// 并行调用两个接口
|
||||||
if (res['status']) {
|
var results = await Future.wait([
|
||||||
if (type == 'init') {
|
queryMessageSystem(type: type),
|
||||||
systemItems.value = res['data'];
|
queryMessageSystemAccount(type: type),
|
||||||
} else {
|
]);
|
||||||
systemItems.addAll(res['data']);
|
|
||||||
}
|
// 对返回的数据进行处理
|
||||||
|
var systemRes = results[0];
|
||||||
|
var accountRes = results[1];
|
||||||
|
|
||||||
|
if (systemRes['status'] || accountRes['status']) {
|
||||||
|
// 处理返回的数据
|
||||||
|
List<MessageSystemModel> combinedData = [
|
||||||
|
...systemRes['data'],
|
||||||
|
...accountRes['data']
|
||||||
|
];
|
||||||
|
combinedData.sort((a, b) => b.cursor!.compareTo(a.cursor!));
|
||||||
|
systemItems.addAll(combinedData);
|
||||||
|
systemItems.refresh();
|
||||||
if (systemItems.isNotEmpty) {
|
if (systemItems.isNotEmpty) {
|
||||||
systemMarkRead(systemItems.first.cursor!);
|
systemMarkRead(systemItems.first.cursor!);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
SmartDialog.showToast(systemRes['msg'] ?? accountRes['msg']);
|
||||||
}
|
}
|
||||||
|
return systemRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取系统消息
|
||||||
|
Future queryMessageSystem({String type = 'init'}) async {
|
||||||
|
var res = await MsgHttp.messageSystem();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取系统消息 个人
|
||||||
|
Future queryMessageSystemAccount({String type = 'init'}) async {
|
||||||
|
var res = await MsgHttp.messageSystemAccount();
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ class _MessageSystemPageState extends State<MessageSystemPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_futureBuilderFuture = _messageSystemCtr.queryMessageSystem();
|
_futureBuilderFuture = _messageSystemCtr.queryAndProcessMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -31,7 +31,7 @@ class _MessageSystemPageState extends State<MessageSystemPage> {
|
|||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await _messageSystemCtr.queryMessageSystem();
|
await _messageSystemCtr.queryAndProcessMessages();
|
||||||
},
|
},
|
||||||
child: FutureBuilder(
|
child: FutureBuilder(
|
||||||
future: _futureBuilderFuture,
|
future: _futureBuilderFuture,
|
||||||
@ -42,7 +42,6 @@ class _MessageSystemPageState extends State<MessageSystemPage> {
|
|||||||
}
|
}
|
||||||
if (snapshot.data['status']) {
|
if (snapshot.data['status']) {
|
||||||
final systemItems = _messageSystemCtr.systemItems;
|
final systemItems = _messageSystemCtr.systemItems;
|
||||||
print(systemItems.length);
|
|
||||||
return Obx(
|
return Obx(
|
||||||
() => ListView.separated(
|
() => ListView.separated(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
@ -115,7 +114,7 @@ class SystemItem extends StatelessWidget {
|
|||||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(item.content!['web']),
|
Text(item.content is String ? item.content : item.content!['web']),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
100
lib/pages/opus/controller.dart
Normal file
100
lib/pages/opus/controller.dart
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/http/read.dart';
|
||||||
|
import 'package:pilipala/models/read/opus.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';
|
||||||
|
|
||||||
|
class OpusController extends GetxController {
|
||||||
|
late String url;
|
||||||
|
RxString title = ''.obs;
|
||||||
|
late String id;
|
||||||
|
late String articleType;
|
||||||
|
Rx<OpusDataModel> opusData = OpusDataModel().obs;
|
||||||
|
final ScrollController scrollController = ScrollController();
|
||||||
|
late StreamController<bool> appbarStream = StreamController<bool>.broadcast();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
title.value = Get.parameters['title'] ?? '';
|
||||||
|
id = Get.parameters['id']!;
|
||||||
|
articleType = Get.parameters['articleType']!;
|
||||||
|
if (articleType == 'opus') {
|
||||||
|
url = 'https://www.bilibili.com/opus/$id';
|
||||||
|
}
|
||||||
|
scrollController.addListener(_scrollListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future fetchOpusData() async {
|
||||||
|
var res = await ReadHttp.parseArticleOpus(id: id);
|
||||||
|
if (res['status']) {
|
||||||
|
List<String> keys = res.keys.toList();
|
||||||
|
if (keys.contains('isCv') && res['isCv']) {
|
||||||
|
Get.offNamed('/read', parameters: {
|
||||||
|
'id': res['cvId'],
|
||||||
|
'title': title.value,
|
||||||
|
'articleType': 'cv',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
opusData.value = res['data'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollListener() {
|
||||||
|
final double offset = scrollController.position.pixels;
|
||||||
|
if (offset > 100) {
|
||||||
|
appbarStream.add(true);
|
||||||
|
} else {
|
||||||
|
appbarStream.add(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPreviewImg(picList, initIndex, context) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
HeroDialogRoute<void>(
|
||||||
|
builder: (BuildContext context) => InteractiveviewerGallery(
|
||||||
|
sources: picList,
|
||||||
|
initIndex: initIndex,
|
||||||
|
itemBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
int index,
|
||||||
|
bool isFocus,
|
||||||
|
bool enablePageView,
|
||||||
|
) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
if (enablePageView) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Center(
|
||||||
|
child: Hero(
|
||||||
|
tag: picList[index],
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
|
imageUrl: picList[index],
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onPageChanged: (int pageIndex) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
scrollController.removeListener(_scrollListener);
|
||||||
|
appbarStream.close();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
4
lib/pages/opus/index.dart
Normal file
4
lib/pages/opus/index.dart
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
library opus;
|
||||||
|
|
||||||
|
export 'controller.dart';
|
||||||
|
export 'view.dart';
|
||||||
62
lib/pages/opus/text_helper.dart
Normal file
62
lib/pages/opus/text_helper.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pilipala/models/read/opus.dart';
|
||||||
|
|
||||||
|
class TextHelper {
|
||||||
|
static Alignment getAlignment(int? align) {
|
||||||
|
switch (align) {
|
||||||
|
case 1:
|
||||||
|
return Alignment.center;
|
||||||
|
case 0:
|
||||||
|
return Alignment.centerLeft;
|
||||||
|
case 2:
|
||||||
|
return Alignment.centerRight;
|
||||||
|
default:
|
||||||
|
return Alignment.centerLeft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static TextSpan buildTextSpan(
|
||||||
|
ModuleParagraphTextNode node, int? align, BuildContext context) {
|
||||||
|
// 获取node的所有key
|
||||||
|
if (node.nodeType != null) {
|
||||||
|
return TextSpan(
|
||||||
|
text: node.word?.words ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize:
|
||||||
|
node.word?.fontSize != null ? node.word!.fontSize! * 0.95 : 14,
|
||||||
|
fontWeight: node.word?.style?.bold != null
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
|
height: align == 1 ? 2 : 1.5,
|
||||||
|
color: node.word?.color != null
|
||||||
|
? Color(int.parse(node.word!.color!.substring(1, 7), radix: 16) +
|
||||||
|
0xFF000000)
|
||||||
|
: Theme.of(context).colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
switch (node.type) {
|
||||||
|
case 'TEXT_NODE_TYPE_WORD':
|
||||||
|
return TextSpan(
|
||||||
|
text: node.word?.words ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: node.word?.fontSize != null
|
||||||
|
? node.word!.fontSize! * 0.95
|
||||||
|
: 14,
|
||||||
|
fontWeight: node.word?.style?.bold != null
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
|
height: align == 1 ? 2 : 1.5,
|
||||||
|
color: node.word?.color != null
|
||||||
|
? Color(
|
||||||
|
int.parse(node.word!.color!.substring(1, 7), radix: 16) +
|
||||||
|
0xFF000000)
|
||||||
|
: Theme.of(context).colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return const TextSpan(text: '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
286
lib/pages/opus/view.dart
Normal file
286
lib/pages/opus/view.dart
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
|
import 'package:pilipala/models/read/opus.dart';
|
||||||
|
import 'controller.dart';
|
||||||
|
import 'text_helper.dart';
|
||||||
|
|
||||||
|
class OpusPage extends StatefulWidget {
|
||||||
|
const OpusPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<OpusPage> createState() => _OpusPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OpusPageState extends State<OpusPage> {
|
||||||
|
final OpusController controller = Get.put(OpusController());
|
||||||
|
late Future _futureBuilderFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_futureBuilderFuture = controller.fetchOpusData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: _buildAppBar(),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
controller: controller.scrollController,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildTitle(),
|
||||||
|
_buildFutureContent(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppBar _buildAppBar() {
|
||||||
|
return AppBar(
|
||||||
|
title: StreamBuilder(
|
||||||
|
stream: controller.appbarStream.stream.distinct(),
|
||||||
|
initialData: false,
|
||||||
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||||
|
return AnimatedOpacity(
|
||||||
|
opacity: snapshot.data ? 1 : 0,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
child: Obx(
|
||||||
|
() => Text(
|
||||||
|
controller.title.value,
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.more_vert_rounded),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTitle() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||||
|
child: Obx(
|
||||||
|
() => Text(
|
||||||
|
controller.title.value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFutureContent() {
|
||||||
|
return FutureBuilder(
|
||||||
|
future: _futureBuilderFuture,
|
||||||
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
|
if (snapshot.data == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
if (snapshot.data['status']) {
|
||||||
|
return _buildContent(controller.opusData.value);
|
||||||
|
} else {
|
||||||
|
return _buildError(snapshot.data['message']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return _buildLoading();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(OpusDataModel opusData) {
|
||||||
|
if (opusData.detail == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
final modules = opusData.detail!.modules!;
|
||||||
|
late ModuleContent moduleContent;
|
||||||
|
// 获取所有的图片链接
|
||||||
|
final List<String> picList = [];
|
||||||
|
final int moduleIndex =
|
||||||
|
modules.indexWhere((module) => module.moduleContent != null);
|
||||||
|
if (moduleIndex != -1) {
|
||||||
|
moduleContent = modules[moduleIndex].moduleContent!;
|
||||||
|
for (var paragraph in moduleContent.paragraphs!) {
|
||||||
|
if (paragraph.paraType == 2) {
|
||||||
|
for (var pic in paragraph.pic!.pics!) {
|
||||||
|
picList.add(pic.url!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print('No moduleContent found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
16, 0, 16, MediaQuery.of(context).padding.bottom + 40),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 30),
|
||||||
|
child: _buildStatsWidget(opusData),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
child: _buildAuthorWidget(opusData),
|
||||||
|
),
|
||||||
|
...moduleContent.paragraphs!.map(
|
||||||
|
(ModuleParagraph paragraph) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
if (paragraph.paraType == 1) ...[
|
||||||
|
Container(
|
||||||
|
alignment: TextHelper.getAlignment(paragraph.align),
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: paragraph.text?.nodes?.map((node) {
|
||||||
|
return TextHelper.buildTextSpan(
|
||||||
|
node, paragraph.align, context);
|
||||||
|
}).toList() ??
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
] else if (paragraph.paraType == 2) ...[
|
||||||
|
...paragraph.pic?.pics?.map(
|
||||||
|
(Pic pic) => Center(
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(top: 10, bottom: 10),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
controller.onPreviewImg(
|
||||||
|
picList,
|
||||||
|
picList.indexOf(pic.url!),
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: NetworkImgLayer(
|
||||||
|
src: pic.url,
|
||||||
|
width: (Get.size.width - 32) * pic.scale!,
|
||||||
|
height: (Get.size.width - 32) *
|
||||||
|
pic.scale! /
|
||||||
|
pic.aspectRatio!,
|
||||||
|
type: 'emote',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
[],
|
||||||
|
] else
|
||||||
|
const SizedBox(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAuthorWidget(OpusDataModel opusData) {
|
||||||
|
final modules = opusData.detail!.modules!;
|
||||||
|
late ModuleAuthor moduleAuthor;
|
||||||
|
final int moduleIndex =
|
||||||
|
modules.indexWhere((module) => module.moduleAuthor != null);
|
||||||
|
if (moduleIndex != -1) {
|
||||||
|
moduleAuthor = modules[moduleIndex].moduleAuthor!;
|
||||||
|
} else {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
NetworkImgLayer(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
type: 'avatar',
|
||||||
|
src: moduleAuthor.face,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
moduleAuthor.name!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StyledText(moduleAuthor.pubTime!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatsWidget(OpusDataModel opusData) {
|
||||||
|
final modules = opusData.detail!.modules!;
|
||||||
|
final ModuleStat moduleStat = modules.last.moduleStat!;
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
StyledText('${moduleStat.comment!.count}评论'),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
StyledText('${moduleStat.like!.count}赞'),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
StyledText('${moduleStat.favorite!.count}转发'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildError(String message) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 100,
|
||||||
|
child: Center(
|
||||||
|
child: Text(message),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoading() {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 100,
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StyledText extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const StyledText(this.text, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
lib/pages/read/controller.dart
Normal file
94
lib/pages/read/controller.dart
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/http/read.dart';
|
||||||
|
import 'package:pilipala/models/read/read.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';
|
||||||
|
|
||||||
|
class ReadPageController extends GetxController {
|
||||||
|
late String url;
|
||||||
|
RxString title = ''.obs;
|
||||||
|
late String id;
|
||||||
|
late String articleType;
|
||||||
|
Rx<ReadDataModel> cvData = ReadDataModel().obs;
|
||||||
|
final ScrollController scrollController = ScrollController();
|
||||||
|
late StreamController<bool> appbarStream = StreamController<bool>.broadcast();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
title.value = Get.parameters['title'] ?? '';
|
||||||
|
id = Get.parameters['id']!;
|
||||||
|
articleType = Get.parameters['articleType']!;
|
||||||
|
scrollController.addListener(_scrollListener);
|
||||||
|
fetchViewInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future fetchCvData() async {
|
||||||
|
var res = await ReadHttp.parseArticleCv(id: id);
|
||||||
|
if (res['status']) {
|
||||||
|
cvData.value = res['data'];
|
||||||
|
title.value = cvData.value.readInfo!.title!;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollListener() {
|
||||||
|
final double offset = scrollController.position.pixels;
|
||||||
|
if (offset > 100) {
|
||||||
|
appbarStream.add(true);
|
||||||
|
} else {
|
||||||
|
appbarStream.add(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPreviewImg(picList, initIndex, context) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
HeroDialogRoute<void>(
|
||||||
|
builder: (BuildContext context) => InteractiveviewerGallery(
|
||||||
|
sources: picList,
|
||||||
|
initIndex: initIndex,
|
||||||
|
itemBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
int index,
|
||||||
|
bool isFocus,
|
||||||
|
bool enablePageView,
|
||||||
|
) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
if (enablePageView) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Center(
|
||||||
|
child: Hero(
|
||||||
|
tag: picList[index],
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
|
imageUrl: picList[index],
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onPageChanged: (int pageIndex) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fetchViewInfo() {
|
||||||
|
ReadHttp.getViewInfo(id: id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
scrollController.removeListener(_scrollListener);
|
||||||
|
appbarStream.close();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
4
lib/pages/read/index.dart
Normal file
4
lib/pages/read/index.dart
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
library read;
|
||||||
|
|
||||||
|
export 'controller.dart';
|
||||||
|
export 'view.dart';
|
||||||
342
lib/pages/read/view.dart
Normal file
342
lib/pages/read/view.dart
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:pilipala/common/widgets/html_render.dart';
|
||||||
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
|
import 'package:pilipala/models/read/opus.dart';
|
||||||
|
import 'package:pilipala/models/read/read.dart';
|
||||||
|
import 'package:pilipala/pages/opus/text_helper.dart';
|
||||||
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
import 'controller.dart';
|
||||||
|
|
||||||
|
class ReadPage extends StatefulWidget {
|
||||||
|
const ReadPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ReadPage> createState() => _ReadPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReadPageState extends State<ReadPage> {
|
||||||
|
final ReadPageController controller = Get.put(ReadPageController());
|
||||||
|
late Future _futureBuilderFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_futureBuilderFuture = controller.fetchCvData();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> extractDataSrc(String input) {
|
||||||
|
final regex = RegExp(r'data-src="([^"]*)"');
|
||||||
|
final matches = regex.allMatches(input);
|
||||||
|
return matches.map((match) {
|
||||||
|
final dataSrc = match.group(1)!;
|
||||||
|
return dataSrc.startsWith('//') ? 'https:$dataSrc' : dataSrc;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: _buildAppBar(),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
controller: controller.scrollController,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildTitle(),
|
||||||
|
_buildFutureContent(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppBar _buildAppBar() {
|
||||||
|
return AppBar(
|
||||||
|
title: StreamBuilder(
|
||||||
|
stream: controller.appbarStream.stream.distinct(),
|
||||||
|
initialData: false,
|
||||||
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||||
|
return AnimatedOpacity(
|
||||||
|
opacity: snapshot.data ? 1 : 0,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
child: Obx(
|
||||||
|
() => Text(
|
||||||
|
controller.title.value,
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.more_vert_rounded),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTitle() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||||
|
child: Obx(
|
||||||
|
() => Text(
|
||||||
|
controller.title.value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFutureContent() {
|
||||||
|
return FutureBuilder(
|
||||||
|
future: _futureBuilderFuture,
|
||||||
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
|
if (snapshot.data == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
if (snapshot.data['status']) {
|
||||||
|
return _buildContent(snapshot.data['data']);
|
||||||
|
} else {
|
||||||
|
return _buildError(snapshot.data['message']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return _buildLoading();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(ReadDataModel cvData) {
|
||||||
|
final List<String> picList = _extractPicList(cvData);
|
||||||
|
final List<String> imgList = extractDataSrc(cvData.readInfo!.content!);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
16, 0, 16, MediaQuery.of(context).padding.bottom + 40),
|
||||||
|
child: cvData.readInfo!.opus == null
|
||||||
|
? _buildNonOpusContent(cvData, imgList)
|
||||||
|
: _buildOpusContent(cvData, picList),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _extractPicList(ReadDataModel cvData) {
|
||||||
|
final List<String> picList = [];
|
||||||
|
if (cvData.readInfo!.opus != null) {
|
||||||
|
final List<ModuleParagraph> paragraphs =
|
||||||
|
cvData.readInfo!.opus!.content!.paragraphs!;
|
||||||
|
for (var paragraph in paragraphs) {
|
||||||
|
if (paragraph.paraType == 2) {
|
||||||
|
for (var pic in paragraph.pic!.pics!) {
|
||||||
|
picList.add(pic.url!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return picList;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNonOpusContent(ReadDataModel cvData, List<String> imgList) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 30),
|
||||||
|
child: _buildStatsWidget(cvData),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
child: _buildAuthorWidget(cvData),
|
||||||
|
),
|
||||||
|
HtmlRender(
|
||||||
|
htmlContent: cvData.readInfo!.content!,
|
||||||
|
imgList: imgList,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOpusContent(ReadDataModel cvData, List<String> picList) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 30),
|
||||||
|
child: _buildStatsWidget(cvData),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
child: _buildAuthorWidget(cvData),
|
||||||
|
),
|
||||||
|
...cvData.readInfo!.opus!.content!.paragraphs!.map(
|
||||||
|
(ModuleParagraph paragraph) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
if (paragraph.paraType == 1)
|
||||||
|
_buildTextParagraph(paragraph)
|
||||||
|
else if (paragraph.paraType == 2)
|
||||||
|
..._buildPics(paragraph, picList)
|
||||||
|
else
|
||||||
|
const SizedBox(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTextParagraph(ModuleParagraph paragraph) {
|
||||||
|
return Container(
|
||||||
|
alignment: TextHelper.getAlignment(paragraph.align),
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: paragraph.text?.nodes?.map((node) {
|
||||||
|
return TextHelper.buildTextSpan(node, paragraph.align, context);
|
||||||
|
}).toList() ??
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildError(String message) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 100,
|
||||||
|
child: Center(
|
||||||
|
child: Text(message),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoading() {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 100,
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatsWidget(ReadDataModel cvData) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
StyledText(Utils.CustomStamp_str(
|
||||||
|
timestamp: cvData.readInfo!.publishTime!,
|
||||||
|
date: 'YY-MM-DD hh:mm',
|
||||||
|
toInt: false,
|
||||||
|
)),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
StyledText('${Utils.numFormat(cvData.readInfo!.stats!['view'])}浏览'),
|
||||||
|
const StyledText(' · '),
|
||||||
|
StyledText('${cvData.readInfo!.stats!['like']}点赞'),
|
||||||
|
// const StyledText(' · '),
|
||||||
|
// StyledText('${cvData.readInfo!.stats!['reply']}评论'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAuthorWidget(ReadDataModel cvData) {
|
||||||
|
final Author author = cvData.readInfo!.author!;
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
NetworkImgLayer(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
type: 'avatar',
|
||||||
|
src: author.face,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
author.name!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: author.vip!.nicknameColor != null
|
||||||
|
? Color(author.vip!.nicknameColor!)
|
||||||
|
: null,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/lv/lv${author.level}.png',
|
||||||
|
height: 11,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
StyledText('粉丝: ${Utils.numFormat(author.fans)}'),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
StyledText(
|
||||||
|
'文章: ${Utils.numFormat(cvData.readInfo!.totalArtNum)}'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildPics(ModuleParagraph paragraph, List<String> picList) {
|
||||||
|
return paragraph.pic?.pics
|
||||||
|
?.map(
|
||||||
|
(Pic pic) => Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10, bottom: 10),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
controller.onPreviewImg(
|
||||||
|
picList,
|
||||||
|
picList.indexOf(pic.url!),
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: NetworkImgLayer(
|
||||||
|
src: pic.url,
|
||||||
|
width: (Get.size.width - 32) * pic.scale!,
|
||||||
|
height:
|
||||||
|
(Get.size.width - 32) * pic.scale! / pic.aspectRatio!,
|
||||||
|
type: 'emote',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StyledText extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const StyledText(this.text, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,11 +14,10 @@ Widget searchArticlePanel(BuildContext context, ctr, list) {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Get.toNamed('/htmlRender', parameters: {
|
Get.toNamed('/read', parameters: {
|
||||||
'url': 'www.bilibili.com/read/cv${list[index].id}',
|
|
||||||
'title': list[index].subTitle,
|
'title': list[index].subTitle,
|
||||||
'id': 'cv${list[index].id}',
|
'id': list[index].id.toString(),
|
||||||
'dynamicType': 'read'
|
'articleType': 'read'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/models/common/action_type.dart';
|
import 'package:pilipala/models/common/action_type.dart';
|
||||||
import 'package:pilipala/utils/global_data.dart';
|
import 'package:pilipala/utils/global_data_cache.dart';
|
||||||
import '../../../utils/storage.dart';
|
import '../../../utils/storage.dart';
|
||||||
|
|
||||||
class ActionMenuSetPage extends StatefulWidget {
|
class ActionMenuSetPage extends StatefulWidget {
|
||||||
@ -38,7 +38,7 @@ class _ActionMenuSetPageState extends State<ActionMenuSetPage> {
|
|||||||
.map<String>((i) => (i['value'] as ActionType).value)
|
.map<String>((i) => (i['value'] as ActionType).value)
|
||||||
.toList();
|
.toList();
|
||||||
setting.put(SettingBoxKey.actionTypeSort, sortedTabbar);
|
setting.put(SettingBoxKey.actionTypeSort, sortedTabbar);
|
||||||
GlobalData().actionTypeSort = sortedTabbar;
|
GlobalDataCache().actionTypeSort = sortedTabbar;
|
||||||
SmartDialog.showToast('操作成功');
|
SmartDialog.showToast('操作成功');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -74,7 +74,6 @@ class _NavigationbarSetPageState extends State<NavigationBarSetPage> {
|
|||||||
},
|
},
|
||||||
title: Text(defaultNavTabs[i]['label']),
|
title: Text(defaultNavTabs[i]['label']),
|
||||||
secondary: const Icon(Icons.drag_indicator_rounded),
|
secondary: const Icon(Icons.drag_indicator_rounded),
|
||||||
enabled: defaultNavTabs[i]['id'] != 0,
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/utils/global_data.dart';
|
import 'package:pilipala/utils/global_data_cache.dart';
|
||||||
|
|
||||||
import '../../../models/common/gesture_mode.dart';
|
import '../../../models/common/gesture_mode.dart';
|
||||||
import '../../../utils/storage.dart';
|
import '../../../utils/storage.dart';
|
||||||
@ -64,11 +64,11 @@ class _PlayGesturePageState extends State<PlayGesturePage> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
GlobalData().fullScreenGestureMode = FullScreenGestureMode
|
GlobalDataCache().fullScreenGestureMode = FullScreenGestureMode
|
||||||
.values
|
.values
|
||||||
.firstWhere((element) => element.values == result);
|
.firstWhere((element) => element.values == result);
|
||||||
fullScreenGestureMode =
|
fullScreenGestureMode =
|
||||||
GlobalData().fullScreenGestureMode.index;
|
GlobalDataCache().fullScreenGestureMode.index;
|
||||||
setting.put(
|
setting.put(
|
||||||
SettingBoxKey.fullScreenGestureMode, fullScreenGestureMode);
|
SettingBoxKey.fullScreenGestureMode, fullScreenGestureMode);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import 'package:pilipala/models/video/play/quality.dart';
|
|||||||
import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
|
import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
|
||||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||||
import 'package:pilipala/services/service_locator.dart';
|
import 'package:pilipala/services/service_locator.dart';
|
||||||
import 'package:pilipala/utils/global_data.dart';
|
import 'package:pilipala/utils/global_data_cache.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
|
||||||
import '../../models/live/quality.dart';
|
import '../../models/live/quality.dart';
|
||||||
@ -162,7 +162,7 @@ class _PlaySettingState extends State<PlaySetting> {
|
|||||||
setKey: SettingBoxKey.enablePlayerControlAnimation,
|
setKey: SettingBoxKey.enablePlayerControlAnimation,
|
||||||
defaultVal: true,
|
defaultVal: true,
|
||||||
callFn: (bool val) {
|
callFn: (bool val) {
|
||||||
GlobalData().enablePlayerControlAnimation = val;
|
GlobalDataCache().enablePlayerControlAnimation = val;
|
||||||
}),
|
}),
|
||||||
SetSwitchItem(
|
SetSwitchItem(
|
||||||
title: '港澳台模式',
|
title: '港澳台模式',
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import 'package:pilipala/models/common/theme_type.dart';
|
|||||||
import 'package:pilipala/pages/setting/pages/color_select.dart';
|
import 'package:pilipala/pages/setting/pages/color_select.dart';
|
||||||
import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
|
import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
|
||||||
import 'package:pilipala/pages/setting/widgets/slide_dialog.dart';
|
import 'package:pilipala/pages/setting/widgets/slide_dialog.dart';
|
||||||
import 'package:pilipala/utils/global_data.dart';
|
import 'package:pilipala/utils/global_data_cache.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
|
||||||
import '../../models/common/dynamic_badge_mode.dart';
|
import '../../models/common/dynamic_badge_mode.dart';
|
||||||
@ -176,7 +176,7 @@ class _StyleSettingState extends State<StyleSetting> {
|
|||||||
SettingBoxKey.defaultPicQa, picQuality);
|
SettingBoxKey.defaultPicQa, picQuality);
|
||||||
Get.back();
|
Get.back();
|
||||||
settingController.picQuality.value = picQuality;
|
settingController.picQuality.value = picQuality;
|
||||||
GlobalData().imgQuality = picQuality;
|
GlobalDataCache().imgQuality = picQuality;
|
||||||
SmartDialog.showToast('设置成功');
|
SmartDialog.showToast('设置成功');
|
||||||
},
|
},
|
||||||
child: const Text('确定'),
|
child: const Text('确定'),
|
||||||
|
|||||||
@ -154,13 +154,9 @@ class VideoContent extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.only(top: 2),
|
padding: const EdgeInsets.only(top: 2),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
StatView(
|
StatView(view: videoItem.cntInfo['play']),
|
||||||
theme: 'gray',
|
|
||||||
view: videoItem.cntInfo['play'],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
StatDanMu(
|
StatDanMu(danmu: videoItem.cntInfo['danmaku']),
|
||||||
theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -7,9 +7,11 @@ import 'package:get/get.dart';
|
|||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:ns_danmaku/ns_danmaku.dart';
|
import 'package:ns_danmaku/ns_danmaku.dart';
|
||||||
import 'package:pilipala/http/constants.dart';
|
import 'package:pilipala/http/constants.dart';
|
||||||
|
import 'package:pilipala/http/user.dart';
|
||||||
import 'package:pilipala/http/video.dart';
|
import 'package:pilipala/http/video.dart';
|
||||||
import 'package:pilipala/models/common/reply_type.dart';
|
import 'package:pilipala/models/common/reply_type.dart';
|
||||||
import 'package:pilipala/models/common/search_type.dart';
|
import 'package:pilipala/models/common/search_type.dart';
|
||||||
|
import 'package:pilipala/models/video/later.dart';
|
||||||
import 'package:pilipala/models/video/play/quality.dart';
|
import 'package:pilipala/models/video/play/quality.dart';
|
||||||
import 'package:pilipala/models/video/play/url.dart';
|
import 'package:pilipala/models/video/play/url.dart';
|
||||||
import 'package:pilipala/models/video/reply/item.dart';
|
import 'package:pilipala/models/video/reply/item.dart';
|
||||||
@ -24,7 +26,10 @@ import '../../../models/video/subTitile/content.dart';
|
|||||||
import '../../../http/danmaku.dart';
|
import '../../../http/danmaku.dart';
|
||||||
import '../../../plugin/pl_player/models/bottom_control_type.dart';
|
import '../../../plugin/pl_player/models/bottom_control_type.dart';
|
||||||
import '../../../utils/id_utils.dart';
|
import '../../../utils/id_utils.dart';
|
||||||
|
import 'introduction/controller.dart';
|
||||||
|
import 'reply/controller.dart';
|
||||||
import 'widgets/header_control.dart';
|
import 'widgets/header_control.dart';
|
||||||
|
import 'widgets/watch_later_list.dart';
|
||||||
|
|
||||||
class VideoDetailController extends GetxController
|
class VideoDetailController extends GetxController
|
||||||
with GetSingleTickerProviderStateMixin {
|
with GetSingleTickerProviderStateMixin {
|
||||||
@ -37,9 +42,10 @@ class VideoDetailController extends GetxController
|
|||||||
Map videoItem = {};
|
Map videoItem = {};
|
||||||
// 视频类型 默认投稿视频
|
// 视频类型 默认投稿视频
|
||||||
SearchType videoType = Get.arguments['videoType'] ?? SearchType.video;
|
SearchType videoType = Get.arguments['videoType'] ?? SearchType.video;
|
||||||
|
// 页面来源 稍后再看 收藏夹
|
||||||
|
RxString sourceType = 'normal'.obs;
|
||||||
|
|
||||||
/// tabs相关配置
|
/// tabs相关配置
|
||||||
int tabInitialIndex = 0;
|
|
||||||
late TabController tabCtr;
|
late TabController tabCtr;
|
||||||
RxList<String> tabs = <String>['简介', '评论'].obs;
|
RxList<String> tabs = <String>['简介', '评论'].obs;
|
||||||
|
|
||||||
@ -110,6 +116,9 @@ class VideoDetailController extends GetxController
|
|||||||
RxDouble sheetHeight = 0.0.obs;
|
RxDouble sheetHeight = 0.0.obs;
|
||||||
RxString archiveSourceType = 'dash'.obs;
|
RxString archiveSourceType = 'dash'.obs;
|
||||||
ScrollController? replyScrillController;
|
ScrollController? replyScrillController;
|
||||||
|
List<MediaVideoItemModel> mediaList = <MediaVideoItemModel>[];
|
||||||
|
RxBool isWatchLaterVisible = false.obs;
|
||||||
|
RxString watchLaterTitle = ''.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -119,9 +128,7 @@ class VideoDetailController extends GetxController
|
|||||||
if (argMap.containsKey('videoItem')) {
|
if (argMap.containsKey('videoItem')) {
|
||||||
var args = argMap['videoItem'];
|
var args = argMap['videoItem'];
|
||||||
updateCover(args.pic);
|
updateCover(args.pic);
|
||||||
}
|
} else if (argMap.containsKey('pic')) {
|
||||||
|
|
||||||
if (argMap.containsKey('pic')) {
|
|
||||||
updateCover(argMap['pic']);
|
updateCover(argMap['pic']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,6 +167,21 @@ class VideoDetailController extends GetxController
|
|||||||
bvid: bvid,
|
bvid: bvid,
|
||||||
videoType: videoType,
|
videoType: videoType,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
sourceType.value = argMap['sourceType'] ?? 'normal';
|
||||||
|
isWatchLaterVisible.value =
|
||||||
|
sourceType.value == 'watchLater' || sourceType.value == 'fav';
|
||||||
|
if (sourceType.value == 'watchLater') {
|
||||||
|
watchLaterTitle.value = '稍后再看';
|
||||||
|
fetchMediaList();
|
||||||
|
}
|
||||||
|
if (sourceType.value == 'fav') {
|
||||||
|
watchLaterTitle.value = argMap['favTitle'];
|
||||||
|
queryFavVideoList();
|
||||||
|
}
|
||||||
|
tabCtr.addListener(() {
|
||||||
|
onTabChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showReplyReplyPanel(oid, fRpid, firstFloor, currentReply, loadMore) {
|
showReplyReplyPanel(oid, fRpid, firstFloor, currentReply, loadMore) {
|
||||||
@ -561,4 +583,101 @@ class VideoDetailController extends GetxController
|
|||||||
duration: const Duration(milliseconds: 300), curve: Curves.ease);
|
duration: const Duration(milliseconds: 300), curve: Curves.ease);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void toggeleWatchLaterVisible(bool val) {
|
||||||
|
if (sourceType.value == 'watchLater' || sourceType.value == 'fav') {
|
||||||
|
isWatchLaterVisible.value = !isWatchLaterVisible.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取稍后再看列表
|
||||||
|
Future fetchMediaList() async {
|
||||||
|
final Map argMap = Get.arguments;
|
||||||
|
var count = argMap['count'];
|
||||||
|
var res = await UserHttp.getMediaList(
|
||||||
|
type: 2,
|
||||||
|
bizId: userInfo.mid,
|
||||||
|
ps: count,
|
||||||
|
);
|
||||||
|
if (res['status']) {
|
||||||
|
mediaList = res['data'].reversed.toList();
|
||||||
|
} else {
|
||||||
|
SmartDialog.showToast(res['msg']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 稍后再看面板展开
|
||||||
|
showMediaListPanel() {
|
||||||
|
replyReplyBottomSheetCtr =
|
||||||
|
scaffoldKey.currentState?.showBottomSheet((BuildContext context) {
|
||||||
|
return MediaListPanel(
|
||||||
|
sheetHeight: sheetHeight.value,
|
||||||
|
mediaList: mediaList,
|
||||||
|
changeMediaList: changeMediaList,
|
||||||
|
panelTitle: watchLaterTitle.value,
|
||||||
|
bvid: bvid,
|
||||||
|
mediaId: Get.arguments['mediaId'],
|
||||||
|
hasMore: mediaList.length != Get.arguments['count'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
replyReplyBottomSheetCtr?.closed.then((value) {
|
||||||
|
isWatchLaterVisible.value = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换稍后再看
|
||||||
|
Future changeMediaList(bvidVal, cidVal, aidVal, coverVal) async {
|
||||||
|
final VideoIntroController videoIntroCtr =
|
||||||
|
Get.find<VideoIntroController>(tag: heroTag);
|
||||||
|
bvid = bvidVal;
|
||||||
|
oid.value = aidVal ?? IdUtils.bv2av(bvid);
|
||||||
|
cid.value = cidVal;
|
||||||
|
danmakuCid.value = cidVal;
|
||||||
|
cover.value = coverVal;
|
||||||
|
queryVideoUrl();
|
||||||
|
clearSubtitleContent();
|
||||||
|
await getSubtitle();
|
||||||
|
setSubtitleContent();
|
||||||
|
// 重新请求评论
|
||||||
|
try {
|
||||||
|
/// 未渲染回复组件时可能异常
|
||||||
|
final VideoReplyController videoReplyCtr =
|
||||||
|
Get.find<VideoReplyController>(tag: heroTag);
|
||||||
|
videoReplyCtr.aid = aidVal;
|
||||||
|
videoReplyCtr.queryReplyList(type: 'init');
|
||||||
|
} catch (_) {}
|
||||||
|
videoIntroCtr.lastPlayCid.value = cidVal;
|
||||||
|
videoIntroCtr.bvid = bvidVal;
|
||||||
|
replyReplyBottomSheetCtr!.close();
|
||||||
|
await videoIntroCtr.queryVideoIntro();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取收藏夹视频列表
|
||||||
|
Future queryFavVideoList() async {
|
||||||
|
final Map argMap = Get.arguments;
|
||||||
|
var mediaId = argMap['mediaId'];
|
||||||
|
var oid = argMap['oid'];
|
||||||
|
var res = await UserHttp.parseFavVideo(
|
||||||
|
mediaId: mediaId,
|
||||||
|
oid: oid,
|
||||||
|
bvid: bvid,
|
||||||
|
);
|
||||||
|
if (res['status']) {
|
||||||
|
mediaList = res['data'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听tabBarView切换
|
||||||
|
void onTabChanged() {
|
||||||
|
isWatchLaterVisible.value = tabCtr.index == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
super.onClose();
|
||||||
|
plPlayerController.dispose();
|
||||||
|
tabCtr.removeListener(() {
|
||||||
|
onTabChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ class VideoIntroController extends GetxController {
|
|||||||
// 视频详情 请求返回
|
// 视频详情 请求返回
|
||||||
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
|
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
|
||||||
// up主粉丝数
|
// up主粉丝数
|
||||||
Map userStat = {'follower': '-'};
|
RxInt follower = 0.obs;
|
||||||
// 是否点赞
|
// 是否点赞
|
||||||
RxBool hasLike = false.obs;
|
RxBool hasLike = false.obs;
|
||||||
// 是否投币
|
// 是否投币
|
||||||
@ -115,7 +115,7 @@ class VideoIntroController extends GetxController {
|
|||||||
Future queryUserStat() async {
|
Future queryUserStat() async {
|
||||||
var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!);
|
var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!);
|
||||||
if (result['status']) {
|
if (result['status']) {
|
||||||
userStat = result['data'];
|
follower.value = result['data']['follower'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,7 +519,10 @@ class VideoIntroController extends GetxController {
|
|||||||
// 设置关注分组
|
// 设置关注分组
|
||||||
void setFollowGroup() {
|
void setFollowGroup() {
|
||||||
showFlexibleBottomSheet(
|
showFlexibleBottomSheet(
|
||||||
bottomSheetColor: Colors.transparent,
|
bottomSheetBorderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
),
|
||||||
minHeight: 0.6,
|
minHeight: 0.6,
|
||||||
initHeight: 0.6,
|
initHeight: 0.6,
|
||||||
maxHeight: 1,
|
maxHeight: 1,
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import 'package:pilipala/models/video_detail_res.dart';
|
|||||||
import 'package:pilipala/pages/video/detail/introduction/controller.dart';
|
import 'package:pilipala/pages/video/detail/introduction/controller.dart';
|
||||||
import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
|
import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
|
||||||
import 'package:pilipala/utils/feed_back.dart';
|
import 'package:pilipala/utils/feed_back.dart';
|
||||||
import 'package:pilipala/utils/global_data.dart';
|
import 'package:pilipala/utils/global_data_cache.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
import 'package:pilipala/utils/utils.dart';
|
import 'package:pilipala/utils/utils.dart';
|
||||||
import '../../../../http/user.dart';
|
import '../../../../http/user.dart';
|
||||||
@ -144,7 +144,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|||||||
final Box<dynamic> setting = GStrorage.setting;
|
final Box<dynamic> setting = GStrorage.setting;
|
||||||
late double sheetHeight;
|
late double sheetHeight;
|
||||||
late final dynamic owner;
|
late final dynamic owner;
|
||||||
late final dynamic follower;
|
|
||||||
late int mid;
|
late int mid;
|
||||||
late String memberHeroTag;
|
late String memberHeroTag;
|
||||||
late bool enableAi;
|
late bool enableAi;
|
||||||
@ -177,7 +176,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|||||||
sheetHeight = localCache.get('sheetHeight');
|
sheetHeight = localCache.get('sheetHeight');
|
||||||
|
|
||||||
owner = widget.videoDetail!.owner;
|
owner = widget.videoDetail!.owner;
|
||||||
follower = Utils.numFormat(videoIntroController.userStat['follower']);
|
|
||||||
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
|
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
|
||||||
_expandableCtr = ExpandableController(initialExpanded: false);
|
_expandableCtr = ExpandableController(initialExpanded: false);
|
||||||
|
|
||||||
@ -230,7 +228,10 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|||||||
|
|
||||||
void _showFavPanel() {
|
void _showFavPanel() {
|
||||||
showFlexibleBottomSheet(
|
showFlexibleBottomSheet(
|
||||||
bottomSheetColor: Colors.transparent,
|
bottomSheetBorderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
),
|
||||||
minHeight: 0.6,
|
minHeight: 0.6,
|
||||||
initHeight: 0.6,
|
initHeight: 0.6,
|
||||||
maxHeight: 1,
|
maxHeight: 1,
|
||||||
@ -321,7 +322,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|||||||
expanded: Text(
|
expanded: Text(
|
||||||
widget.videoDetail!.title!,
|
widget.videoDetail!.title!,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
maxLines: 4,
|
maxLines: 10,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -346,13 +347,11 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
StatView(
|
StatView(
|
||||||
theme: 'gray',
|
|
||||||
view: widget.videoDetail!.stat!.view,
|
view: widget.videoDetail!.stat!.view,
|
||||||
size: 'medium',
|
size: 'medium',
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
StatDanMu(
|
StatDanMu(
|
||||||
theme: 'gray',
|
|
||||||
danmu: widget.videoDetail!.stat!.danmaku,
|
danmu: widget.videoDetail!.stat!.danmaku,
|
||||||
size: 'medium',
|
size: 'medium',
|
||||||
),
|
),
|
||||||
@ -469,15 +468,18 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|||||||
fadeOutDuration: Duration.zero,
|
fadeOutDuration: Duration.zero,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(owner.name, style: const TextStyle(fontSize: 13)),
|
Text(widget.videoDetail!.owner!.name!,
|
||||||
|
style: const TextStyle(fontSize: 13)),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Obx(
|
||||||
follower,
|
() => Text(
|
||||||
|
Utils.numFormat(videoIntroController.follower.value),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: t.textTheme.labelSmall!.fontSize,
|
fontSize: t.textTheme.labelSmall!.fontSize,
|
||||||
color: outline,
|
color: outline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Obx(
|
Obx(
|
||||||
() {
|
() {
|
||||||
@ -569,7 +571,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget actionGrid(BuildContext context, videoIntroController) {
|
Widget actionGrid(BuildContext context, videoIntroController) {
|
||||||
final actionTypeSort = GlobalData().actionTypeSort;
|
final actionTypeSort = GlobalDataCache().actionTypeSort;
|
||||||
|
|
||||||
Widget progressWidget(progress) {
|
Widget progressWidget(progress) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/common/widgets/http_error.dart';
|
import 'package:pilipala/common/widgets/http_error.dart';
|
||||||
@ -26,16 +27,7 @@ class _FavPanelState extends State<FavPanel> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Column(
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(16),
|
|
||||||
topRight: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
AppBar(
|
AppBar(
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
@ -61,28 +53,29 @@ class _FavPanelState extends State<FavPanel> {
|
|||||||
return Obx(
|
return Obx(
|
||||||
() => ListView.builder(
|
() => ListView.builder(
|
||||||
controller: widget.scrollController,
|
controller: widget.scrollController,
|
||||||
itemCount:
|
itemCount: widget.ctr!.favFolderData.value.list!.length,
|
||||||
widget.ctr!.favFolderData.value.list!.length,
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item =
|
final item =
|
||||||
widget.ctr!.favFolderData.value.list![index];
|
widget.ctr!.favFolderData.value.list![index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
onTap: () => widget.ctr!
|
onTap: () =>
|
||||||
.onChoose(item.favState != 1, index),
|
widget.ctr!.onChoose(item.favState != 1, index),
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: const Icon(Icons.folder_outlined),
|
leading: Icon([23, 1].contains(item.attr)
|
||||||
|
? Icons.lock_outline
|
||||||
|
: Icons.folder_outlined),
|
||||||
minLeadingWidth: 0,
|
minLeadingWidth: 0,
|
||||||
title: Text(widget.ctr!.favFolderData.value
|
title: Text(item.title!),
|
||||||
.list![index].title!),
|
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'${item.mediaCount}个内容 ',
|
'${item.mediaCount}个内容 - ${[
|
||||||
|
23,
|
||||||
|
1
|
||||||
|
].contains(item.attr) ? '私密' : '公开'}',
|
||||||
),
|
),
|
||||||
trailing: Transform.scale(
|
trailing: Transform.scale(
|
||||||
scale: 0.9,
|
scale: 0.9,
|
||||||
child: Checkbox(
|
child: Checkbox(
|
||||||
value: widget.ctr!.favFolderData.value
|
value: item.favState == 1,
|
||||||
.list![index].favState ==
|
|
||||||
1,
|
|
||||||
onChanged: (bool? checkValue) =>
|
onChanged: (bool? checkValue) =>
|
||||||
widget.ctr!.onChoose(checkValue!, index),
|
widget.ctr!.onChoose(checkValue!, index),
|
||||||
),
|
),
|
||||||
@ -123,9 +116,8 @@ class _FavPanelState extends State<FavPanel> {
|
|||||||
onPressed: () => Get.back(),
|
onPressed: () => Get.back(),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.only(left: 30, right: 30),
|
padding: const EdgeInsets.only(left: 30, right: 30),
|
||||||
backgroundColor: Theme.of(context)
|
backgroundColor:
|
||||||
.colorScheme
|
Theme.of(context).colorScheme.onInverseSurface, // 设置按钮背景色
|
||||||
.onInverseSurface, // 设置按钮背景色
|
|
||||||
),
|
),
|
||||||
child: const Text('取消'),
|
child: const Text('取消'),
|
||||||
),
|
),
|
||||||
@ -141,13 +133,12 @@ class _FavPanelState extends State<FavPanel> {
|
|||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).colorScheme.primary, // 设置按钮背景色
|
Theme.of(context).colorScheme.primary, // 设置按钮背景色
|
||||||
),
|
),
|
||||||
child: const Text('确认选择'),
|
child: const Text('确认'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,16 +54,7 @@ class _GroupPanelState extends State<GroupPanel> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Column(
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(16),
|
|
||||||
topRight: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
AppBar(
|
AppBar(
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
@ -71,8 +62,7 @@ class _GroupPanelState extends State<GroupPanel> {
|
|||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () => Get.back(),
|
onPressed: () => Get.back(),
|
||||||
icon: const Icon(Icons.close_outlined)),
|
icon: const Icon(Icons.close_outlined)),
|
||||||
title:
|
title: Text('设置关注分组', style: Theme.of(context).textTheme.titleMedium),
|
||||||
Text('设置关注分组', style: Theme.of(context).textTheme.titleMedium),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Material(
|
child: Material(
|
||||||
@ -108,8 +98,8 @@ class _GroupPanelState extends State<GroupPanel> {
|
|||||||
value: data['data'][index].checked,
|
value: data['data'][index].checked,
|
||||||
onChanged: (bool? checkValue) {
|
onChanged: (bool? checkValue) {
|
||||||
data['data'][index].checked = checkValue;
|
data['data'][index].checked = checkValue;
|
||||||
showDefault = !data['data']
|
showDefault =
|
||||||
.any((e) => e.checked == true);
|
!data['data'].any((e) => e.checked == true);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -159,7 +149,6 @@ class _GroupPanelState extends State<GroupPanel> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,6 +68,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
late final AppLifecycleListener _lifecycleListener;
|
late final AppLifecycleListener _lifecycleListener;
|
||||||
late double statusHeight;
|
late double statusHeight;
|
||||||
|
|
||||||
|
// 稍后再看控制器
|
||||||
|
// late AnimationController _laterCtr;
|
||||||
|
// late Animation<Offset> _laterOffsetAni;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -104,6 +108,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
}
|
}
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
lifecycleListener();
|
lifecycleListener();
|
||||||
|
// watchLaterControllerInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取视频资源,初始化播放器
|
// 获取视频资源,初始化播放器
|
||||||
@ -211,6 +216,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
vdCtr.bottomList.removeAt(3);
|
vdCtr.bottomList.removeAt(3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
vdCtr.toggeleWatchLaterVisible(!isFullScreen);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,6 +242,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
appbarStream.close();
|
appbarStream.close();
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_lifecycleListener.dispose();
|
_lifecycleListener.dispose();
|
||||||
|
// _laterCtr.dispose();
|
||||||
|
// _laterOffsetAni.removeListener(() {});
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -482,6 +490,21 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 稍后再看控制器初始化
|
||||||
|
// void watchLaterControllerInit() {
|
||||||
|
// _laterCtr = AnimationController(
|
||||||
|
// duration: const Duration(milliseconds: 300),
|
||||||
|
// vsync: this,
|
||||||
|
// );
|
||||||
|
// _laterOffsetAni = Tween<Offset>(
|
||||||
|
// begin: const Offset(0.0, 1.0),
|
||||||
|
// end: Offset.zero,
|
||||||
|
// ).animate(CurvedAnimation(
|
||||||
|
// parent: _laterCtr,
|
||||||
|
// curve: Curves.easeInOut,
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sizeContext = MediaQuery.sizeOf(context);
|
final sizeContext = MediaQuery.sizeOf(context);
|
||||||
@ -595,6 +618,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
child: AppBar(
|
child: AppBar(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: ExtendedNestedScrollView(
|
body: ExtendedNestedScrollView(
|
||||||
@ -757,6 +781,62 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 稍后再看列表
|
||||||
|
Obx(
|
||||||
|
() => Visibility(
|
||||||
|
visible: vdCtr.sourceType.value == 'watchLater' ||
|
||||||
|
vdCtr.sourceType.value == 'fav',
|
||||||
|
child: AnimatedPositioned(
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
left: 12,
|
||||||
|
bottom: vdCtr.isWatchLaterVisible.value
|
||||||
|
? MediaQuery.of(context).padding.bottom + 12
|
||||||
|
: -100,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
vdCtr.toggeleWatchLaterVisible(
|
||||||
|
!vdCtr.isWatchLaterVisible.value);
|
||||||
|
vdCtr.showMediaListPanel();
|
||||||
|
},
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||||
|
child: Container(
|
||||||
|
width: Get.width - 24,
|
||||||
|
height: 54,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.secondaryContainer
|
||||||
|
.withOpacity(0.95),
|
||||||
|
borderRadius:
|
||||||
|
const BorderRadius.all(Radius.circular(14)),
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
const Icon(Icons.playlist_play, size: 24),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
vdCtr.watchLaterTitle.value,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSecondaryContainer,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
const Icon(Icons.keyboard_arrow_up_rounded, size: 26),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
229
lib/pages/video/detail/widgets/watch_later_list.dart
Normal file
229
lib/pages/video/detail/widgets/watch_later_list.dart
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import 'package:easy_debounce/easy_throttle.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.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';
|
||||||
|
import 'package:pilipala/common/widgets/stat/danmu.dart';
|
||||||
|
import 'package:pilipala/common/widgets/stat/view.dart';
|
||||||
|
import 'package:pilipala/http/search.dart';
|
||||||
|
import 'package:pilipala/http/user.dart';
|
||||||
|
import 'package:pilipala/models/video/later.dart';
|
||||||
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
|
||||||
|
class MediaListPanel extends StatefulWidget {
|
||||||
|
const MediaListPanel({
|
||||||
|
this.sheetHeight,
|
||||||
|
required this.mediaList,
|
||||||
|
this.changeMediaList,
|
||||||
|
this.panelTitle,
|
||||||
|
this.bvid,
|
||||||
|
this.mediaId,
|
||||||
|
this.hasMore = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double? sheetHeight;
|
||||||
|
final List<MediaVideoItemModel> mediaList;
|
||||||
|
final Function? changeMediaList;
|
||||||
|
final String? panelTitle;
|
||||||
|
final String? bvid;
|
||||||
|
final int? mediaId;
|
||||||
|
final bool hasMore;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MediaListPanel> createState() => _MediaListPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MediaListPanelState extends State<MediaListPanel> {
|
||||||
|
RxList<MediaVideoItemModel> mediaList = <MediaVideoItemModel>[].obs;
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
mediaList.value = widget.mediaList;
|
||||||
|
_scrollController.addListener(() {
|
||||||
|
if (_scrollController.position.pixels >=
|
||||||
|
_scrollController.position.maxScrollExtent - 200) {
|
||||||
|
if (widget.hasMore) {
|
||||||
|
EasyThrottle.throttle(
|
||||||
|
'queryFollowDynamic', const Duration(seconds: 1), () {
|
||||||
|
loadMore();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadMore() async {
|
||||||
|
var res = await UserHttp.getMediaList(
|
||||||
|
type: 3,
|
||||||
|
bizId: widget.mediaId!,
|
||||||
|
ps: 20,
|
||||||
|
oid: mediaList.last.id,
|
||||||
|
);
|
||||||
|
if (res['status']) {
|
||||||
|
if (res['data'].isNotEmpty) {
|
||||||
|
mediaList.addAll(res['data']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SmartDialog.showToast(res['msg']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: widget.sheetHeight,
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
AppBar(
|
||||||
|
toolbarHeight: 45,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
centerTitle: false,
|
||||||
|
title: Text(
|
||||||
|
widget.panelTitle ?? '稍后再看',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, size: 20),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Material(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: Obx(
|
||||||
|
() => ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
itemCount: mediaList.length,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
var item = mediaList[index];
|
||||||
|
return InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
String bvid = item.bvId!;
|
||||||
|
int? aid = item.id;
|
||||||
|
String cover = item.cover ?? '';
|
||||||
|
final int cid =
|
||||||
|
await SearchHttp.ab2c(aid: aid, bvid: bvid);
|
||||||
|
widget.changeMediaList?.call(bvid, cid, aid, cover);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10, vertical: 8),
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context,
|
||||||
|
BoxConstraints boxConstraints) {
|
||||||
|
const double width = 120;
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(minHeight: 88),
|
||||||
|
height: width / StyleString.aspectRatio,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: StyleString.aspectRatio,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context,
|
||||||
|
BoxConstraints boxConstraints) {
|
||||||
|
final double maxWidth =
|
||||||
|
boxConstraints.maxWidth;
|
||||||
|
final double maxHeight =
|
||||||
|
boxConstraints.maxHeight;
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
NetworkImgLayer(
|
||||||
|
src: item.cover ?? '',
|
||||||
|
width: maxWidth,
|
||||||
|
height: maxHeight,
|
||||||
|
),
|
||||||
|
PBadge(
|
||||||
|
text: Utils.timeFormat(
|
||||||
|
item.duration!),
|
||||||
|
right: 6.0,
|
||||||
|
bottom: 6.0,
|
||||||
|
type: 'gray',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
10, 0, 6, 0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.title as String,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: item.bvId == widget.bvid
|
||||||
|
? Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
item.upper?.name as String,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelMedium!
|
||||||
|
.fontSize,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
StatView(
|
||||||
|
view: item.cntInfo!['play']
|
||||||
|
as int),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
StatDanMu(
|
||||||
|
danmu:
|
||||||
|
item.cntInfo!['danmaku']
|
||||||
|
as int),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -217,6 +217,7 @@ class SessionItem extends StatelessWidget {
|
|||||||
final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo?.mid ?? 0);
|
final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo?.mid ?? 0);
|
||||||
final content = sessionItem.lastMsg.content;
|
final content = sessionItem.lastMsg.content;
|
||||||
final msgStatus = sessionItem.lastMsg.msgStatus;
|
final msgStatus = sessionItem.lastMsg.msgStatus;
|
||||||
|
final int msgType = sessionItem.lastMsg.msgType;
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -251,6 +252,8 @@ class SessionItem extends StatelessWidget {
|
|||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
msgStatus == 1
|
msgStatus == 1
|
||||||
? '你撤回了一条消息'
|
? '你撤回了一条消息'
|
||||||
|
: msgType == 2
|
||||||
|
? '[图片]'
|
||||||
: content != null && content != ''
|
: content != null && content != ''
|
||||||
? (content['text'] ??
|
? (content['text'] ??
|
||||||
content['content'] ??
|
content['content'] ??
|
||||||
|
|||||||
@ -22,6 +22,7 @@ class WhisperDetailController extends GetxController {
|
|||||||
final TextEditingController replyContentController = TextEditingController();
|
final TextEditingController replyContentController = TextEditingController();
|
||||||
Box userInfoCache = GStrorage.userInfo;
|
Box userInfoCache = GStrorage.userInfo;
|
||||||
List emoteList = [];
|
List emoteList = [];
|
||||||
|
List<String> picList = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -41,6 +42,18 @@ class WhisperDetailController extends GetxController {
|
|||||||
var res = await MsgHttp.sessionMsg(talkerId: talkerId);
|
var res = await MsgHttp.sessionMsg(talkerId: talkerId);
|
||||||
if (res['status']) {
|
if (res['status']) {
|
||||||
messageList.value = res['data'].messages;
|
messageList.value = res['data'].messages;
|
||||||
|
// 找出图片
|
||||||
|
try {
|
||||||
|
for (var item in messageList) {
|
||||||
|
if (item.msgType == 2) {
|
||||||
|
picList.add(item.content['url']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
picList = picList.reversed.toList();
|
||||||
|
} catch (e) {
|
||||||
|
print('e: $e');
|
||||||
|
}
|
||||||
|
|
||||||
if (messageList.isNotEmpty) {
|
if (messageList.isNotEmpty) {
|
||||||
ackSessionMsg();
|
ackSessionMsg();
|
||||||
if (res['data'].eInfos != null) {
|
if (res['data'].eInfos != null) {
|
||||||
|
|||||||
@ -193,27 +193,21 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
|
|||||||
? const SizedBox()
|
? const SizedBox()
|
||||||
: Align(
|
: Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: ListView.builder(
|
child: ListView.separated(
|
||||||
itemCount: messageList.length,
|
itemCount: messageList.length,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
itemBuilder: (_, int i) {
|
itemBuilder: (_, int i) {
|
||||||
if (i == 0) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
ChatItem(
|
|
||||||
item: messageList[i],
|
|
||||||
e_infos: _whisperDetailController
|
|
||||||
.eInfos),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return ChatItem(
|
return ChatItem(
|
||||||
item: messageList[i],
|
item: messageList[i],
|
||||||
e_infos:
|
e_infos: _whisperDetailController.eInfos,
|
||||||
_whisperDetailController.eInfos);
|
ctr: _whisperDetailController,
|
||||||
}
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, int i) {
|
||||||
|
return i == 0
|
||||||
|
? const SizedBox(height: 20)
|
||||||
|
: const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -2,14 +2,18 @@
|
|||||||
// ignore_for_file: constant_identifier_names
|
// ignore_for_file: constant_identifier_names
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';
|
||||||
import 'package:pilipala/utils/route_push.dart';
|
import 'package:pilipala/utils/route_push.dart';
|
||||||
import 'package:pilipala/utils/utils.dart';
|
import 'package:pilipala/utils/utils.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
import '../../../http/search.dart';
|
import '../../../http/search.dart';
|
||||||
|
import '../controller.dart';
|
||||||
|
|
||||||
enum MsgType {
|
enum MsgType {
|
||||||
invalid(value: 0, label: "空空的~"),
|
invalid(value: 0, label: "空空的~"),
|
||||||
@ -42,10 +46,12 @@ enum MsgType {
|
|||||||
class ChatItem extends StatelessWidget {
|
class ChatItem extends StatelessWidget {
|
||||||
dynamic item;
|
dynamic item;
|
||||||
List? e_infos;
|
List? e_infos;
|
||||||
|
WhisperDetailController ctr;
|
||||||
|
|
||||||
ChatItem({
|
ChatItem({
|
||||||
super.key,
|
super.key,
|
||||||
this.item,
|
required this.item,
|
||||||
|
required this.ctr,
|
||||||
this.e_infos,
|
this.e_infos,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -157,10 +163,48 @@ class ChatItem extends StatelessWidget {
|
|||||||
case MsgType.text:
|
case MsgType.text:
|
||||||
return richTextMessage(context);
|
return richTextMessage(context);
|
||||||
case MsgType.pic:
|
case MsgType.pic:
|
||||||
return NetworkImgLayer(
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
HeroDialogRoute<void>(
|
||||||
|
builder: (BuildContext context) => InteractiveviewerGallery(
|
||||||
|
sources: ctr.picList,
|
||||||
|
initIndex: ctr.picList.indexOf(content['url']),
|
||||||
|
itemBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
int index,
|
||||||
|
bool isFocus,
|
||||||
|
bool enablePageView,
|
||||||
|
) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
if (enablePageView) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Center(
|
||||||
|
child: Hero(
|
||||||
|
tag: ctr.picList[index],
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
|
imageUrl: ctr.picList[index],
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onPageChanged: (int pageIndex) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: NetworkImgLayer(
|
||||||
width: 220,
|
width: 220,
|
||||||
height: 220 * content['height'] / content['width'],
|
height: 220 * content['height'] / content['width'],
|
||||||
src: content['url'],
|
src: content['url'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case MsgType.share_v2:
|
case MsgType.share_v2:
|
||||||
return Column(
|
return Column(
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import 'package:pilipala/plugin/pl_player/index.dart';
|
|||||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||||
import 'package:pilipala/services/service_locator.dart';
|
import 'package:pilipala/services/service_locator.dart';
|
||||||
import 'package:pilipala/utils/feed_back.dart';
|
import 'package:pilipala/utils/feed_back.dart';
|
||||||
|
import 'package:pilipala/utils/global_data_cache.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
import 'package:screen_brightness/screen_brightness.dart';
|
import 'package:screen_brightness/screen_brightness.dart';
|
||||||
import 'package:status_bar_control/status_bar_control.dart';
|
import 'package:status_bar_control/status_bar_control.dart';
|
||||||
@ -277,50 +278,19 @@ class PlPlayerController {
|
|||||||
|
|
||||||
// 添加一个私有构造函数
|
// 添加一个私有构造函数
|
||||||
PlPlayerController._internal(this.videoType) {
|
PlPlayerController._internal(this.videoType) {
|
||||||
isOpenDanmu.value =
|
final cache = GlobalDataCache();
|
||||||
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
|
isOpenDanmu.value = cache.isOpenDanmu;
|
||||||
blockTypes =
|
blockTypes = cache.blockTypes;
|
||||||
localCache.get(LocalCacheKey.danmakuBlockType, defaultValue: []);
|
showArea = cache.showArea;
|
||||||
showArea = localCache.get(LocalCacheKey.danmakuShowArea, defaultValue: 0.5);
|
opacityVal = cache.opacityVal;
|
||||||
// 不透明度
|
fontSizeVal = cache.fontSizeVal;
|
||||||
opacityVal =
|
danmakuDurationVal = cache.danmakuDurationVal;
|
||||||
localCache.get(LocalCacheKey.danmakuOpacity, defaultValue: 1.0);
|
strokeWidth = cache.strokeWidth;
|
||||||
// 字体大小
|
playRepeat = cache.playRepeat;
|
||||||
fontSizeVal =
|
_playbackSpeed.value = cache.playbackSpeed;
|
||||||
localCache.get(LocalCacheKey.danmakuFontScale, defaultValue: 1.0);
|
enableAutoLongPressSpeed = cache.enableAutoLongPressSpeed;
|
||||||
// 弹幕时间
|
_longPressSpeed.value = cache.longPressSpeed;
|
||||||
danmakuDurationVal =
|
speedsList = cache.speedsList;
|
||||||
localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0);
|
|
||||||
// 描边粗细
|
|
||||||
strokeWidth = localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5);
|
|
||||||
playRepeat = PlayRepeat.values.toList().firstWhere(
|
|
||||||
(e) =>
|
|
||||||
e.value ==
|
|
||||||
videoStorage.get(VideoBoxKey.playRepeat,
|
|
||||||
defaultValue: PlayRepeat.pause.value),
|
|
||||||
);
|
|
||||||
_playbackSpeed.value =
|
|
||||||
videoStorage.get(VideoBoxKey.playSpeedDefault, defaultValue: 1.0);
|
|
||||||
enableAutoLongPressSpeed = setting
|
|
||||||
.get(SettingBoxKey.enableAutoLongPressSpeed, defaultValue: false);
|
|
||||||
if (!enableAutoLongPressSpeed) {
|
|
||||||
_longPressSpeed.value = videoStorage
|
|
||||||
.get(VideoBoxKey.longPressSpeedDefault, defaultValue: 2.0);
|
|
||||||
}
|
|
||||||
// 自定义倍速集合
|
|
||||||
speedsList = List<double>.from(videoStorage
|
|
||||||
.get(VideoBoxKey.customSpeedsList, defaultValue: <double>[]));
|
|
||||||
// 默认倍速
|
|
||||||
speedsList = List<double>.from(videoStorage
|
|
||||||
.get(VideoBoxKey.customSpeedsList, defaultValue: <double>[]));
|
|
||||||
//playSpeedSystem
|
|
||||||
final List<double> playSpeedSystem =
|
|
||||||
videoStorage.get(VideoBoxKey.playSpeedSystem, defaultValue: playSpeed);
|
|
||||||
|
|
||||||
// for (final PlaySpeed i in PlaySpeed.values) {
|
|
||||||
speedsList.addAll(playSpeedSystem);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
|
// _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
|
||||||
// if (status == PlayerStatus.playing) {
|
// if (status == PlayerStatus.playing) {
|
||||||
// WakelockPlus.enable();
|
// WakelockPlus.enable();
|
||||||
|
|||||||
@ -19,13 +19,14 @@ import 'package:pilipala/utils/feed_back.dart';
|
|||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
import 'package:screen_brightness/screen_brightness.dart';
|
import 'package:screen_brightness/screen_brightness.dart';
|
||||||
|
|
||||||
import '../../utils/global_data.dart';
|
import '../../utils/global_data_cache.dart';
|
||||||
import 'models/bottom_control_type.dart';
|
import 'models/bottom_control_type.dart';
|
||||||
import 'models/bottom_progress_behavior.dart';
|
import 'models/bottom_progress_behavior.dart';
|
||||||
import 'widgets/app_bar_ani.dart';
|
import 'widgets/app_bar_ani.dart';
|
||||||
import 'widgets/backward_seek.dart';
|
import 'widgets/backward_seek.dart';
|
||||||
import 'widgets/bottom_control.dart';
|
import 'widgets/bottom_control.dart';
|
||||||
import 'widgets/common_btn.dart';
|
import 'widgets/common_btn.dart';
|
||||||
|
import 'widgets/control_bar.dart';
|
||||||
import 'widgets/forward_seek.dart';
|
import 'widgets/forward_seek.dart';
|
||||||
import 'widgets/play_pause_btn.dart';
|
import 'widgets/play_pause_btn.dart';
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ class PLVideoPlayer extends StatefulWidget {
|
|||||||
this.customWidgets,
|
this.customWidgets,
|
||||||
this.showEposideCb,
|
this.showEposideCb,
|
||||||
this.fullScreenCb,
|
this.fullScreenCb,
|
||||||
|
this.alignment = Alignment.center,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -54,6 +56,7 @@ class PLVideoPlayer extends StatefulWidget {
|
|||||||
final List<Widget>? customWidgets;
|
final List<Widget>? customWidgets;
|
||||||
final Function? showEposideCb;
|
final Function? showEposideCb;
|
||||||
final Function? fullScreenCb;
|
final Function? fullScreenCb;
|
||||||
|
final Alignment? alignment;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PLVideoPlayer> createState() => _PLVideoPlayerState();
|
State<PLVideoPlayer> createState() => _PLVideoPlayerState();
|
||||||
@ -87,7 +90,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
late bool enableBackgroundPlay;
|
late bool enableBackgroundPlay;
|
||||||
late double screenWidth;
|
late double screenWidth;
|
||||||
final FullScreenGestureMode fullScreenGestureMode =
|
final FullScreenGestureMode fullScreenGestureMode =
|
||||||
GlobalData().fullScreenGestureMode;
|
GlobalDataCache().fullScreenGestureMode;
|
||||||
|
|
||||||
// 用于记录上一次全屏切换手势触发时间,避免误触
|
// 用于记录上一次全屏切换手势触发时间,避免误触
|
||||||
DateTime? lastFullScreenToggleTime;
|
DateTime? lastFullScreenToggleTime;
|
||||||
@ -132,7 +135,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
screenWidth = Get.size.width;
|
screenWidth = Get.size.width;
|
||||||
animationController = AnimationController(
|
animationController = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: GlobalData().enablePlayerControlAnimation
|
duration: GlobalDataCache().enablePlayerControlAnimation
|
||||||
? const Duration(milliseconds: 150)
|
? const Duration(milliseconds: 150)
|
||||||
: const Duration(milliseconds: 10),
|
: const Duration(milliseconds: 10),
|
||||||
);
|
);
|
||||||
@ -392,6 +395,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
key: ValueKey(_.videoFit.value),
|
key: ValueKey(_.videoFit.value),
|
||||||
controller: videoController,
|
controller: videoController,
|
||||||
controls: NoVideoControls,
|
controls: NoVideoControls,
|
||||||
|
alignment: widget.alignment!,
|
||||||
pauseUponEnteringBackgroundMode: !enableBackgroundPlay,
|
pauseUponEnteringBackgroundMode: !enableBackgroundPlay,
|
||||||
resumeUponEnteringForegroundMode: true,
|
resumeUponEnteringForegroundMode: true,
|
||||||
subtitleViewConfiguration: const SubtitleViewConfiguration(
|
subtitleViewConfiguration: const SubtitleViewConfiguration(
|
||||||
@ -484,104 +488,27 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
|
|
||||||
/// 音量🔊 控制条展示
|
/// 音量🔊 控制条展示
|
||||||
Obx(
|
Obx(
|
||||||
() => Align(
|
() => ControlBar(
|
||||||
child: AnimatedOpacity(
|
visible: _volumeIndicator.value,
|
||||||
curve: Curves.easeInOut,
|
icon: _volumeValue.value < 1.0 / 3.0
|
||||||
opacity: _volumeIndicator.value ? 1.0 : 0.0,
|
? Icons.volume_mute
|
||||||
duration: const Duration(milliseconds: 150),
|
: _volumeValue.value < 2.0 / 3.0
|
||||||
child: Container(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0x88000000),
|
|
||||||
borderRadius: BorderRadius.circular(64.0),
|
|
||||||
),
|
|
||||||
height: 34.0,
|
|
||||||
width: 70.0,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
height: 34.0,
|
|
||||||
width: 28.0,
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Icon(
|
|
||||||
_volumeValue.value == 0.0
|
|
||||||
? Icons.volume_off
|
|
||||||
: _volumeValue.value < 0.5
|
|
||||||
? Icons.volume_down
|
? Icons.volume_down
|
||||||
: Icons.volume_up,
|
: Icons.volume_up,
|
||||||
color: const Color(0xFFFFFFFF),
|
value: _volumeValue.value,
|
||||||
size: 20.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'${(_volumeValue.value * 100.0).round()}%',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13.0,
|
|
||||||
color: Color(0xFFFFFFFF),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6.0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/// 亮度🌞 控制条展示
|
/// 亮度🌞 控制条展示
|
||||||
Obx(
|
Obx(
|
||||||
() => Align(
|
() => ControlBar(
|
||||||
child: AnimatedOpacity(
|
visible: _brightnessIndicator.value,
|
||||||
curve: Curves.easeInOut,
|
icon: _brightnessValue.value < 1.0 / 3.0
|
||||||
opacity: _brightnessIndicator.value ? 1.0 : 0.0,
|
|
||||||
duration: const Duration(milliseconds: 150),
|
|
||||||
child: Container(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0x88000000),
|
|
||||||
borderRadius: BorderRadius.circular(64.0),
|
|
||||||
),
|
|
||||||
height: 34.0,
|
|
||||||
width: 70.0,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
height: 30.0,
|
|
||||||
width: 28.0,
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Icon(
|
|
||||||
_brightnessValue.value < 1.0 / 3.0
|
|
||||||
? Icons.brightness_low
|
? Icons.brightness_low
|
||||||
: _brightnessValue.value < 2.0 / 3.0
|
: _brightnessValue.value < 2.0 / 3.0
|
||||||
? Icons.brightness_medium
|
? Icons.brightness_medium
|
||||||
: Icons.brightness_high,
|
: Icons.brightness_high,
|
||||||
color: const Color(0xFFFFFFFF),
|
value: _brightnessValue.value,
|
||||||
size: 18.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 2.0),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'${(_brightnessValue.value * 100.0).round()}%',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13.0,
|
|
||||||
color: Color(0xFFFFFFFF),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6.0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
50
lib/plugin/pl_player/widgets/control_bar.dart
Normal file
50
lib/plugin/pl_player/widgets/control_bar.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ControlBar extends StatelessWidget {
|
||||||
|
final bool visible;
|
||||||
|
final IconData icon;
|
||||||
|
final double value;
|
||||||
|
|
||||||
|
const ControlBar({
|
||||||
|
Key? key,
|
||||||
|
required this.visible,
|
||||||
|
required this.icon,
|
||||||
|
required this.value,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Color color = const Color(0xFFFFFFFF);
|
||||||
|
return Align(
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
opacity: visible ? 1.0 : 0.0,
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
child: IntrinsicWidth(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(10, 2, 10, 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0x88000000),
|
||||||
|
borderRadius: BorderRadius.circular(64.0),
|
||||||
|
),
|
||||||
|
height: 34.0,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(icon, color: color, size: 18.0),
|
||||||
|
const SizedBox(width: 4.0),
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(minWidth: 30.0),
|
||||||
|
child: Text(
|
||||||
|
'${(value * 100.0).round()}%',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 13.0, color: color),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,11 +3,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:pilipala/pages/fav_edit/index.dart';
|
||||||
import 'package:pilipala/pages/follow_search/view.dart';
|
import 'package:pilipala/pages/follow_search/view.dart';
|
||||||
|
import 'package:pilipala/pages/member_article/index.dart';
|
||||||
import 'package:pilipala/pages/message/at/index.dart';
|
import 'package:pilipala/pages/message/at/index.dart';
|
||||||
import 'package:pilipala/pages/message/like/index.dart';
|
import 'package:pilipala/pages/message/like/index.dart';
|
||||||
import 'package:pilipala/pages/message/reply/index.dart';
|
import 'package:pilipala/pages/message/reply/index.dart';
|
||||||
import 'package:pilipala/pages/message/system/index.dart';
|
import 'package:pilipala/pages/message/system/index.dart';
|
||||||
|
import 'package:pilipala/pages/opus/index.dart';
|
||||||
|
import 'package:pilipala/pages/read/index.dart';
|
||||||
import 'package:pilipala/pages/setting/pages/logs.dart';
|
import 'package:pilipala/pages/setting/pages/logs.dart';
|
||||||
|
|
||||||
import '../pages/about/index.dart';
|
import '../pages/about/index.dart';
|
||||||
@ -183,6 +187,15 @@ class Routes {
|
|||||||
// 系统通知
|
// 系统通知
|
||||||
CustomGetPage(
|
CustomGetPage(
|
||||||
name: '/messageSystem', page: () => const MessageSystemPage()),
|
name: '/messageSystem', page: () => const MessageSystemPage()),
|
||||||
|
// 收藏夹编辑
|
||||||
|
CustomGetPage(name: '/favEdit', page: () => const FavEditPage()),
|
||||||
|
|
||||||
|
// 专栏
|
||||||
|
CustomGetPage(name: '/opus', page: () => const OpusPage()),
|
||||||
|
CustomGetPage(name: '/read', page: () => const ReadPage()),
|
||||||
|
// 用户专栏
|
||||||
|
CustomGetPage(
|
||||||
|
name: '/memberArticle', page: () => const MemberArticlePage()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -82,14 +82,11 @@ class PiliSchame {
|
|||||||
case 'opus':
|
case 'opus':
|
||||||
if (path.startsWith('/detail')) {
|
if (path.startsWith('/detail')) {
|
||||||
var opusId = path.split('/').last;
|
var opusId = path.split('/').last;
|
||||||
Get.toNamed(
|
Get.toNamed('/opus', arguments: {
|
||||||
'/webview',
|
'title': '',
|
||||||
parameters: {
|
'id': opusId,
|
||||||
'url': 'https://www.bilibili.com/opus/$opusId',
|
'articleType': 'opus',
|
||||||
'type': 'url',
|
});
|
||||||
'pageTitle': '',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'search':
|
case 'search':
|
||||||
@ -97,12 +94,14 @@ class PiliSchame {
|
|||||||
break;
|
break;
|
||||||
case 'article':
|
case 'article':
|
||||||
final String id = path.split('/').last.split('?').first;
|
final String id = path.split('/').last.split('?').first;
|
||||||
Get.toNamed('/htmlRender', parameters: {
|
Get.toNamed(
|
||||||
'url': 'https://www.bilibili.com/read/cv$id',
|
'/read',
|
||||||
|
parameters: {
|
||||||
'title': 'cv$id',
|
'title': 'cv$id',
|
||||||
'id': 'cv$id',
|
'id': id,
|
||||||
'dynamicType': 'read'
|
'dynamicType': 'read',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'pgc':
|
case 'pgc':
|
||||||
if (path.contains('ep')) {
|
if (path.contains('ep')) {
|
||||||
@ -243,12 +242,12 @@ class PiliSchame {
|
|||||||
break;
|
break;
|
||||||
case 'read':
|
case 'read':
|
||||||
print('专栏');
|
print('专栏');
|
||||||
String id = 'cv${Utils.matchNum(query!['id']!).first}';
|
String id = Utils.matchNum(query!['id']!).first.toString();
|
||||||
Get.toNamed('/htmlRender', parameters: {
|
Get.toNamed('/read', parameters: {
|
||||||
'url': value.dataString!,
|
'url': value.dataString!,
|
||||||
'title': '',
|
'title': '',
|
||||||
'id': id,
|
'id': id,
|
||||||
'dynamicType': 'read'
|
'articleType': 'read'
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'space':
|
case 'space':
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:pilipala/utils/storage.dart';
|
|
||||||
import '../models/common/index.dart';
|
|
||||||
|
|
||||||
Box setting = GStrorage.setting;
|
|
||||||
|
|
||||||
class GlobalData {
|
|
||||||
int imgQuality = 10;
|
|
||||||
FullScreenGestureMode fullScreenGestureMode =
|
|
||||||
FullScreenGestureMode.values.last;
|
|
||||||
bool enablePlayerControlAnimation = true;
|
|
||||||
final bool enableMYBar =
|
|
||||||
setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
|
|
||||||
List<String> actionTypeSort = setting.get(SettingBoxKey.actionTypeSort,
|
|
||||||
defaultValue: ['like', 'coin', 'collect', 'watchLater', 'share']);
|
|
||||||
// 私有构造函数
|
|
||||||
GlobalData._();
|
|
||||||
|
|
||||||
// 单例实例
|
|
||||||
static final GlobalData _instance = GlobalData._();
|
|
||||||
|
|
||||||
// 获取全局实例
|
|
||||||
factory GlobalData() => _instance;
|
|
||||||
}
|
|
||||||
106
lib/utils/global_data_cache.dart
Normal file
106
lib/utils/global_data_cache.dart
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:pilipala/models/user/info.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_player/models/play_speed.dart';
|
||||||
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
import '../models/common/index.dart';
|
||||||
|
|
||||||
|
Box setting = GStrorage.setting;
|
||||||
|
Box localCache = GStrorage.localCache;
|
||||||
|
Box videoStorage = GStrorage.video;
|
||||||
|
Box userInfoCache = GStrorage.userInfo;
|
||||||
|
|
||||||
|
class GlobalDataCache {
|
||||||
|
late int imgQuality;
|
||||||
|
late FullScreenGestureMode fullScreenGestureMode;
|
||||||
|
late bool enablePlayerControlAnimation;
|
||||||
|
late List<String> actionTypeSort;
|
||||||
|
|
||||||
|
/// 播放器相关
|
||||||
|
// 弹幕开关
|
||||||
|
late bool isOpenDanmu;
|
||||||
|
// 弹幕屏蔽类型
|
||||||
|
late List<dynamic> blockTypes;
|
||||||
|
// 弹幕展示区域
|
||||||
|
late double showArea;
|
||||||
|
// 弹幕透明度
|
||||||
|
late double opacityVal;
|
||||||
|
// 弹幕字体大小
|
||||||
|
late double fontSizeVal;
|
||||||
|
// 弹幕显示时间
|
||||||
|
late double danmakuDurationVal;
|
||||||
|
// 弹幕描边宽度
|
||||||
|
late double strokeWidth;
|
||||||
|
// 播放器循环模式
|
||||||
|
late PlayRepeat playRepeat;
|
||||||
|
// 播放器默认播放速度
|
||||||
|
late double playbackSpeed;
|
||||||
|
// 播放器自动长按速度
|
||||||
|
late bool enableAutoLongPressSpeed;
|
||||||
|
// 播放器长按速度
|
||||||
|
late double longPressSpeed;
|
||||||
|
// 播放器速度列表
|
||||||
|
late List<double> speedsList;
|
||||||
|
// 用户信息
|
||||||
|
UserInfoData? userInfo;
|
||||||
|
|
||||||
|
// 私有构造函数
|
||||||
|
GlobalDataCache._();
|
||||||
|
|
||||||
|
// 单例实例
|
||||||
|
static final GlobalDataCache _instance = GlobalDataCache._();
|
||||||
|
|
||||||
|
// 获取全局实例
|
||||||
|
factory GlobalDataCache() => _instance;
|
||||||
|
|
||||||
|
// 异步初始化方法
|
||||||
|
Future<void> initialize() async {
|
||||||
|
imgQuality = await setting.get(SettingBoxKey.defaultPicQa,
|
||||||
|
defaultValue: 10); // 设置全局变量
|
||||||
|
fullScreenGestureMode = FullScreenGestureMode.values[setting.get(
|
||||||
|
SettingBoxKey.fullScreenGestureMode,
|
||||||
|
defaultValue: FullScreenGestureMode.values.last.index) as int];
|
||||||
|
enablePlayerControlAnimation = setting
|
||||||
|
.get(SettingBoxKey.enablePlayerControlAnimation, defaultValue: true);
|
||||||
|
actionTypeSort = await setting.get(SettingBoxKey.actionTypeSort,
|
||||||
|
defaultValue: ['like', 'coin', 'collect', 'watchLater', 'share']);
|
||||||
|
|
||||||
|
isOpenDanmu =
|
||||||
|
await setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
|
||||||
|
blockTypes =
|
||||||
|
await localCache.get(LocalCacheKey.danmakuBlockType, defaultValue: []);
|
||||||
|
showArea =
|
||||||
|
await localCache.get(LocalCacheKey.danmakuShowArea, defaultValue: 0.5);
|
||||||
|
opacityVal =
|
||||||
|
await localCache.get(LocalCacheKey.danmakuOpacity, defaultValue: 1.0);
|
||||||
|
fontSizeVal =
|
||||||
|
await localCache.get(LocalCacheKey.danmakuFontScale, defaultValue: 1.0);
|
||||||
|
danmakuDurationVal =
|
||||||
|
await localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0);
|
||||||
|
strokeWidth =
|
||||||
|
await localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5);
|
||||||
|
|
||||||
|
var defaultPlayRepeat = await videoStorage.get(VideoBoxKey.playRepeat,
|
||||||
|
defaultValue: PlayRepeat.pause.value);
|
||||||
|
playRepeat = PlayRepeat.values
|
||||||
|
.toList()
|
||||||
|
.firstWhere((e) => e.value == defaultPlayRepeat);
|
||||||
|
playbackSpeed =
|
||||||
|
await videoStorage.get(VideoBoxKey.playSpeedDefault, defaultValue: 1.0);
|
||||||
|
enableAutoLongPressSpeed = await setting
|
||||||
|
.get(SettingBoxKey.enableAutoLongPressSpeed, defaultValue: false);
|
||||||
|
if (!enableAutoLongPressSpeed) {
|
||||||
|
longPressSpeed = await videoStorage.get(VideoBoxKey.longPressSpeedDefault,
|
||||||
|
defaultValue: 2.0);
|
||||||
|
} else {
|
||||||
|
longPressSpeed = 2.0;
|
||||||
|
}
|
||||||
|
speedsList = List<double>.from(await videoStorage
|
||||||
|
.get(VideoBoxKey.customSpeedsList, defaultValue: <double>[]));
|
||||||
|
final List<double> playSpeedSystem = await videoStorage
|
||||||
|
.get(VideoBoxKey.playSpeedSystem, defaultValue: playSpeed);
|
||||||
|
speedsList.addAll(playSpeedSystem);
|
||||||
|
|
||||||
|
userInfo = userInfoCache.get('userInfoCache');
|
||||||
|
}
|
||||||
|
}
|
||||||
14
lib/utils/highlight.dart
Normal file
14
lib/utils/highlight.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:re_highlight/languages/all.dart';
|
||||||
|
import 'package:re_highlight/re_highlight.dart';
|
||||||
|
import 'package:re_highlight/styles/all.dart';
|
||||||
|
|
||||||
|
TextSpan? highlightExistingText(String text, List<String> languages) {
|
||||||
|
final Highlight highlight = Highlight();
|
||||||
|
highlight.registerLanguages(builtinAllLanguages);
|
||||||
|
final HighlightResult result = highlight.highlightAuto(text, languages);
|
||||||
|
final TextSpanRenderer renderer =
|
||||||
|
TextSpanRenderer(const TextStyle(), builtinAllThemes['github']!);
|
||||||
|
result.render(renderer);
|
||||||
|
return renderer.span;
|
||||||
|
}
|
||||||
@ -2,8 +2,6 @@ import 'dart:io';
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:pilipala/models/user/info.dart';
|
import 'package:pilipala/models/user/info.dart';
|
||||||
import '../models/common/gesture_mode.dart';
|
|
||||||
import 'global_data.dart';
|
|
||||||
|
|
||||||
class GStrorage {
|
class GStrorage {
|
||||||
static late final Box<dynamic> userInfo;
|
static late final Box<dynamic> userInfo;
|
||||||
@ -42,13 +40,6 @@ class GStrorage {
|
|||||||
);
|
);
|
||||||
// 视频设置
|
// 视频设置
|
||||||
video = await Hive.openBox('video');
|
video = await Hive.openBox('video');
|
||||||
GlobalData().imgQuality =
|
|
||||||
setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10); // 设置全局变量
|
|
||||||
GlobalData().fullScreenGestureMode = FullScreenGestureMode.values[
|
|
||||||
setting.get(SettingBoxKey.fullScreenGestureMode,
|
|
||||||
defaultValue: FullScreenGestureMode.values.last.index) as int];
|
|
||||||
GlobalData().enablePlayerControlAnimation = setting
|
|
||||||
.get(SettingBoxKey.enablePlayerControlAnimation, defaultValue: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void regAdapter() {
|
static void regAdapter() {
|
||||||
|
|||||||
@ -1234,6 +1234,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.0"
|
||||||
|
re_highlight:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: re_highlight
|
||||||
|
sha256: "6c4ac3f76f939fb7ca9df013df98526634e17d8f7460e028bd23a035870024f2"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.3"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -149,6 +149,8 @@ dependencies:
|
|||||||
bottom_sheet: ^4.0.4
|
bottom_sheet: ^4.0.4
|
||||||
web_socket_channel: ^2.4.5
|
web_socket_channel: ^2.4.5
|
||||||
brotli: ^0.6.0
|
brotli: ^0.6.0
|
||||||
|
# 文本语法高亮
|
||||||
|
re_highlight: ^0.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user