opt: item

opt: util

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-04-16 23:48:25 +08:00
parent 12c711424b
commit d886569dc3
85 changed files with 1983 additions and 1964 deletions

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/models/common/reply_type.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -84,7 +85,7 @@ class PiliScheme {
String? id = uriDigitRegExp.firstMatch(path)?.group(1);
if (id != null) {
bool isEp = path.contains('/ep/');
Utils.viewBangumi(
PageUtils.viewBangumi(
seasonId: isEp ? null : id,
epId: isEp ? id : null,
progress: uri.queryParameters['start_progress']);
@@ -95,7 +96,7 @@ class PiliScheme {
// bilibili://space/12345678?frommodule=XX&h5awaken=random
String? mid = uriDigitRegExp.firstMatch(path)?.group(1);
if (mid != null) {
Utils.toDupNamed('/member?mid=$mid', off: off);
PageUtils.toDupNamed('/member?mid=$mid', off: off);
return true;
}
return false;
@@ -159,7 +160,7 @@ class PiliScheme {
if (aid != null || bvid != null) {
if (queryParameters['cid'] != null) {
bvid ??= IdUtils.av2bv(int.parse(aid!));
Utils.toViewPage(
PageUtils.toVideoPage(
'bvid=$bvid&cid=${queryParameters['cid']}',
arguments: {
'pic': null,
@@ -185,7 +186,7 @@ class PiliScheme {
// bilibili://live/12345678?extra_jump_from=1&from=1&is_room_feed=1&h5awaken=random
String? roomId = uriDigitRegExp.firstMatch(path)?.group(1);
if (roomId != null) {
Utils.toDupNamed('/liveRoom?roomid=$roomId', off: off);
PageUtils.toDupNamed('/liveRoom?roomid=$roomId', off: off);
return true;
}
return false;
@@ -194,7 +195,7 @@ class PiliScheme {
if (path.startsWith('/season')) {
String? seasonId = uriDigitRegExp.firstMatch(path)?.group(1);
if (seasonId != null) {
Utils.viewBangumi(seasonId: seasonId, epId: null);
PageUtils.viewBangumi(seasonId: seasonId, epId: null);
return true;
}
}
@@ -203,7 +204,7 @@ class PiliScheme {
// bilibili://opus/detail/12345678?h5awaken=random
// String? id = uriDigitRegExp.firstMatch(path)?.group(1);
// if (id != null) {
// Utils.toDupNamed(
// PageUtils.toDupNamed(
// '/htmlRender',
// parameters: {
// 'url': 'https://www.bilibili.com/opus/$id',
@@ -219,7 +220,7 @@ class PiliScheme {
bool hasMatch = await _onPushDynDetail(path, off);
return hasMatch;
case 'search':
Utils.toDupNamed(
PageUtils.toDupNamed(
'/searchResult',
parameters: {'keyword': ''},
off: off,
@@ -229,7 +230,7 @@ class PiliScheme {
// bilibili://article/40679479?jump_opus=1&jump_opus_type=1&opus_type=article&h5awaken=random
String? id = uriDigitRegExp.firstMatch(path)?.group(1);
if (id != null) {
Utils.toDupNamed(
PageUtils.toDupNamed(
'/htmlRender',
parameters: {
'url': 'www.bilibili.com/read/cv$id',
@@ -351,7 +352,7 @@ class PiliScheme {
.firstMatch(path)
?.group(1);
if (cvid != null) {
Utils.toDupNamed(
PageUtils.toDupNamed(
'/htmlRender',
parameters: {
'url': 'https://www.bilibili.com/read/cv$cvid',
@@ -424,7 +425,7 @@ class PiliScheme {
dynamic res = await DynamicsHttp.dynamicDetail(rid: rid, type: 2);
SmartDialog.dismiss();
if (res['status']) {
Utils.toDupNamed(
PageUtils.toDupNamed(
'/dynamicDetail',
arguments: {
'item': res['data'],
@@ -442,7 +443,7 @@ class PiliScheme {
case 'medialist':
String? mediaId = uriDigitRegExp.firstMatch(path)?.group(1);
if (mediaId != null) {
Utils.toDupNamed(
PageUtils.toDupNamed(
'/favDetail',
parameters: {
'mediaId': mediaId,
@@ -546,7 +547,7 @@ class PiliScheme {
if (host.contains('live.bilibili.com')) {
String? roomId = uriDigitRegExp.firstMatch(path)?.group(1);
if (roomId != null) {
Utils.toDupNamed('/liveRoom?roomid=$roomId', off: off);
PageUtils.toDupNamed('/liveRoom?roomid=$roomId', off: off);
return true;
}
launchURL();
@@ -556,7 +557,7 @@ class PiliScheme {
if (host.contains('space.bilibili.com')) {
String? mid = uriDigitRegExp.firstMatch(path)?.group(1);
if (mid != null) {
Utils.toDupNamed('/member?mid=$mid', off: off);
PageUtils.toDupNamed('/member?mid=$mid', off: off);
return true;
}
launchURL();
@@ -579,7 +580,7 @@ class PiliScheme {
.firstMatch(uri.query)
?.group(1);
if (id != null) {
Utils.toDupNamed(
PageUtils.toDupNamed(
'/htmlRender',
parameters: {
'url': 'https://www.bilibili.com/read/cv$id',
@@ -597,7 +598,7 @@ class PiliScheme {
// case 'opus':
// String? id = uriDigitRegExp.firstMatch(path)?.group(1);
// if (id != null) {
// Utils.toDupNamed(
// PageUtils.toDupNamed(
// '/htmlRender',
// parameters: {
// 'url': 'https://www.bilibili.com/opus/$id',
@@ -635,7 +636,7 @@ class PiliScheme {
if (id != null) {
bool isSeason = id.startsWith('ss');
id = id.substring(2);
Utils.viewBangumi(
PageUtils.viewBangumi(
seasonId: isSeason ? id : null,
epId: isSeason ? null : id,
progress: uri.queryParameters['start_progress']);
@@ -664,7 +665,7 @@ class PiliScheme {
String? id =
RegExp(r'cv(\d+)', caseSensitive: false).firstMatch(path)?.group(1);
if (id != null) {
Utils.toDupNamed(
PageUtils.toDupNamed(
'/htmlRender',
parameters: {
'url': 'https://www.bilibili.com/read/cv$id',
@@ -682,7 +683,7 @@ class PiliScheme {
debugPrint('个人空间');
String? mid = uriDigitRegExp.firstMatch(path)?.group(1);
if (mid != null) {
Utils.toDupNamed(
PageUtils.toDupNamed(
'/member?mid=$mid',
off: off,
);
@@ -708,7 +709,7 @@ class PiliScheme {
static Future<bool> _onPushDynDetail(path, off) async {
String? id = uriDigitRegExp.firstMatch(path)?.group(1);
if (id != null) {
Utils.pushDynFromId(id, off: off);
PageUtils.pushDynFromId(id, off: off);
return true;
}
return false;
@@ -719,7 +720,7 @@ class PiliScheme {
bool off,
Map? parameters,
) {
Utils.toDupNamed(
PageUtils.toDupNamed(
'/webview',
parameters: {
'url': url,
@@ -752,7 +753,7 @@ class PiliScheme {
if (showDialog) {
SmartDialog.dismiss();
}
Utils.toViewPage(
PageUtils.toVideoPage(
'bvid=$bvid&cid=$cid',
arguments: {
'pic': null,

View File

@@ -12,7 +12,7 @@ class Grid {
SliverGridDelegateWithExtentAndRatio(
mainAxisSpacing: 2,
maxCrossAxisExtent: Grid.smallCardWidth * 2,
childAspectRatio: StyleString.aspectRatio * 2.4,
childAspectRatio: StyleString.aspectRatio * 2.2,
minHeight: MediaQuery.textScalerOf(context).scale(minHeight),
);
}

747
lib/utils/page_utils.dart Normal file
View File

@@ -0,0 +1,747 @@
import 'dart:math';
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart';
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/search.dart';
import 'package:PiliPlus/models/bangumi/info.dart';
import 'package:PiliPlus/models/common/search_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/models/live/item.dart';
import 'package:PiliPlus/pages/video/detail/introduction/widgets/fav_panel.dart';
import 'package:PiliPlus/pages/video/detail/introduction/widgets/menu_row.dart';
import 'package:PiliPlus/services/shutdown_timer_service.dart';
import 'package:PiliPlus/utils/app_scheme.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/url_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
class PageUtils {
static void scheduleExit(BuildContext context, isFullScreen,
[bool isLive = false]) {
if (!context.mounted) {
return;
}
const List<int> scheduleTimeChoices = [0, 15, 30, 45, 60];
const TextStyle titleStyle = TextStyle(fontSize: 14);
if (isLive) {
shutdownTimerService.waitForPlayingCompleted = false;
}
PageUtils.showVideoBottomSheet(
context,
isFullScreen: () => isFullScreen,
child: StatefulBuilder(
builder: (_, setState) {
return Theme(
data: Theme.of(context),
child: Material(
color: Colors.transparent,
child: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.only(left: 14, right: 14),
child: ListView(
padding:
const EdgeInsets.symmetric(vertical: 0, horizontal: 20),
children: [
const SizedBox(height: 10),
const Center(child: Text('定时关闭', style: titleStyle)),
const SizedBox(height: 10),
...[
...[
...scheduleTimeChoices,
if (scheduleTimeChoices
.contains(
shutdownTimerService.scheduledExitInMinutes)
.not)
shutdownTimerService.scheduledExitInMinutes,
]..sort(),
-1,
].map(
(choice) => ListTile(
dense: true,
onTap: () {
if (choice == -1) {
showDialog(
context: context,
builder: (context) {
String duration = '';
return AlertDialog(
title: const Text('自定义时长'),
content: TextField(
autofocus: true,
onChanged: (value) => duration = value,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'\d+')),
],
decoration: const InputDecoration(
suffixText: 'min'),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline),
),
),
TextButton(
onPressed: () {
Get.back();
int choice =
int.tryParse(duration) ?? 0;
shutdownTimerService
.scheduledExitInMinutes = choice;
shutdownTimerService
.startShutdownTimer();
setState(() {});
},
child: Text('确定'),
),
],
);
},
);
} else {
Get.back();
shutdownTimerService.scheduledExitInMinutes =
choice;
shutdownTimerService.startShutdownTimer();
}
},
contentPadding: const EdgeInsets.only(),
title: Text(choice == -1
? '自定义'
: choice == 0
? "禁用"
: "$choice分钟后"),
trailing: shutdownTimerService.scheduledExitInMinutes ==
choice
? Icon(
Icons.done,
color: Theme.of(context).colorScheme.primary,
)
: null,
),
),
const SizedBox(height: 6),
const Center(
child: SizedBox(
width: 125,
child: Divider(height: 1),
),
),
if (isLive.not) ...[
const SizedBox(height: 10),
ListTile(
dense: true,
onTap: () {
shutdownTimerService.waitForPlayingCompleted =
!shutdownTimerService.waitForPlayingCompleted;
setState(() {});
},
contentPadding: const EdgeInsets.only(),
title: const Text("额外等待视频播放完毕", style: titleStyle),
trailing: Transform.scale(
alignment: Alignment
.centerRight, // 缩放Switch的大小后保持右侧对齐, 避免右侧空隙过大
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value: shutdownTimerService.waitForPlayingCompleted,
onChanged: (value) => setState(() =>
shutdownTimerService.waitForPlayingCompleted =
value),
),
),
),
],
const SizedBox(height: 10),
Row(
children: [
const Text('倒计时结束:', style: titleStyle),
const Spacer(),
ActionRowLineItem(
onTap: () {
shutdownTimerService.exitApp = false;
setState(() {});
// Get.back();
},
text: " 暂停视频 ",
selectStatus: !shutdownTimerService.exitApp,
),
const Spacer(),
// const SizedBox(width: 10),
ActionRowLineItem(
onTap: () {
shutdownTimerService.exitApp = true;
setState(() {});
// Get.back();
},
text: " 退出APP ",
selectStatus: shutdownTimerService.exitApp,
)
],
),
const SizedBox(height: 10),
],
),
),
),
);
},
),
);
}
static Future pushDynFromId(id, {bool off = false}) async {
SmartDialog.showLoading();
dynamic res = await DynamicsHttp.dynamicDetail(id: id);
SmartDialog.dismiss();
if (res['status']) {
DynamicItemModel data = res['data'];
if (data.basic?['comment_type'] == 12) {
toDupNamed(
'/htmlRender',
parameters: {
'url': 'www.bilibili.com/opus/$id',
'title': '',
'id': id,
'dynamicType': 'opus'
},
off: off,
);
} else {
toDupNamed(
'/dynamicDetail',
arguments: {
'item': res['data'],
'floor': 1,
'action': 'detail',
},
off: off,
);
}
} else {
SmartDialog.showToast(res['msg']);
}
}
static void showFavBottomSheet({
required BuildContext context,
required dynamic ctr,
}) {
showModalBottomSheet(
context: context,
useSafeArea: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
sheetAnimationStyle: AnimationStyle(curve: Curves.ease),
constraints: BoxConstraints(
maxWidth: min(640, min(Get.width, Get.height)),
),
builder: (BuildContext context) {
return DraggableScrollableSheet(
minChildSize: 0,
maxChildSize: 1,
initialChildSize: 0.7,
snap: true,
expand: false,
snapSizes: const [0.7],
builder: (BuildContext context, ScrollController scrollController) {
return FavPanel(
ctr: ctr,
scrollController: scrollController,
);
},
);
},
);
}
static void reportVideo(int aid) {
Get.toNamed(
'/webview',
parameters: {'url': 'https://www.bilibili.com/appeal/?avid=$aid'},
);
}
static void enterPip(Floating floating, int width, int height) {
Rational aspectRatio = Rational(width, height);
floating.enable(
EnableManual(
aspectRatio: aspectRatio.fitsInAndroidRequirements
? aspectRatio
: height > width
? const Rational.vertical()
: const Rational.landscape(),
),
);
}
static void pushDynDetail(item, floor, {action = 'all'}) async {
feedBack();
/// 点击评论action 直接查看评论
if (action == 'comment') {
toDupNamed(
'/dynamicDetail',
arguments: {
'item': item,
'floor': floor,
'action': action,
},
);
return;
}
debugPrint('pushDynDetail: ${item.type}');
switch (item.type) {
case 'DYNAMIC_TYPE_AV':
if (item.modules.moduleDynamic.major.archive?.type == 2) {
if (item.modules.moduleDynamic.major.archive.jumpUrl
.startsWith('//')) {
item.modules.moduleDynamic.major.archive.jumpUrl =
'https:${item.modules.moduleDynamic.major.archive.jumpUrl}';
}
String? redirectUrl = await UrlUtils.parseRedirectUrl(
item.modules.moduleDynamic.major.archive.jumpUrl, false);
if (redirectUrl != null) {
viewPgcFromUri(redirectUrl);
return;
}
}
try {
String bvid = item.modules.moduleDynamic.major.archive.bvid;
String cover = item.modules.moduleDynamic.major.archive.cover;
int cid = await SearchHttp.ab2c(bvid: bvid);
toVideoPage(
'bvid=$bvid&cid=$cid',
arguments: {
'pic': cover,
'heroTag': Utils.makeHeroTag(bvid),
},
preventDuplicates: false,
);
} catch (err) {
SmartDialog.showToast(err.toString());
}
break;
/// 专栏文章查看
case 'DYNAMIC_TYPE_ARTICLE':
String? url = item?.modules?.moduleDynamic?.major?.opus?.jumpUrl;
if (url != null) {
String? title = item?.modules?.moduleDynamic?.major?.opus?.title;
if (url.contains('opus') || url.contains('read')) {
RegExp digitRegExp = RegExp(r'\d+');
Iterable<Match> matches = digitRegExp.allMatches(url);
String number = matches.first.group(0)!;
if (url.contains('read')) {
number = 'cv$number';
}
toDupNamed('/htmlRender', parameters: {
'url': url.startsWith('//') ? url.split('//').last : url,
'title': title ?? '',
'id': number,
'dynamicType': url.split('//').last.split('/')[1]
});
} else {
handleWebview('https:$url');
}
}
break;
case 'DYNAMIC_TYPE_PGC':
debugPrint('番剧');
SmartDialog.showToast('暂未支持的类型,请联系开发者');
break;
case 'DYNAMIC_TYPE_LIVE_RCMD':
DynamicLiveModel liveRcmd = item.modules.moduleDynamic.major.liveRcmd;
ModuleAuthorModel author = item.modules.moduleAuthor;
LiveItemModel liveItem = LiveItemModel.fromJson({
'title': liveRcmd.title,
'uname': author.name,
'cover': liveRcmd.cover,
'mid': author.mid,
'face': author.face,
'roomid': liveRcmd.roomId,
'watched_show': liveRcmd.watchedShow,
});
toDupNamed('/liveRoom?roomid=${liveItem.roomId}');
break;
/// 合集查看
case 'DYNAMIC_TYPE_UGC_SEASON':
DynamicArchiveModel ugcSeason =
item.modules.moduleDynamic.major.ugcSeason;
int aid = ugcSeason.aid!;
String bvid = IdUtils.av2bv(aid);
String cover = ugcSeason.cover!;
int cid = await SearchHttp.ab2c(bvid: bvid);
toVideoPage(
'bvid=$bvid&cid=$cid',
arguments: {
'pic': cover,
'heroTag': Utils.makeHeroTag(bvid),
},
preventDuplicates: false,
);
break;
/// 番剧查看
case 'DYNAMIC_TYPE_PGC_UNION':
debugPrint('DYNAMIC_TYPE_PGC_UNION 番剧');
DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;
if (pgc.epid != null) {
viewBangumi(epId: pgc.epid);
}
break;
case 'DYNAMIC_TYPE_MEDIALIST':
if (item.modules?.moduleDynamic?.major?.medialist != null) {
final String? url =
item.modules.moduleDynamic.major.medialist['jump_url'];
if (url?.contains('medialist/detail/ml') == true) {
Get.toNamed(
'/favDetail',
parameters: {
'heroTag':
'${item.modules.moduleDynamic.major.medialist['cover']}',
'mediaId':
'${item.modules.moduleDynamic.major.medialist['id']}',
},
);
} else if (url != null) {
handleWebview(url.http2https);
}
}
break;
// 纯文字动态查看
// case 'DYNAMIC_TYPE_WORD':
// # 装扮/剧集点评/普通分享
// case 'DYNAMIC_TYPE_COMMON_SQUARE':
// 转发的动态
// case 'DYNAMIC_TYPE_FORWARD':
// 图文动态查看
// case 'DYNAMIC_TYPE_DRAW':
default:
toDupNamed(
'/dynamicDetail',
arguments: {
'item': item,
'floor': floor,
},
);
break;
}
}
static void onHorizontalPreview(
GlobalKey<ScaffoldState> key,
transitionAnimationController,
ctr,
List<String> imgList,
index,
onClose,
) {
key.currentState?.showBottomSheet(
(context) {
return FadeTransition(
opacity: Tween<double>(begin: 0, end: 1).animate(ctr),
child: InteractiveviewerGallery(
sources: imgList.map((url) => SourceModel(url: url)).toList(),
initIndex: index,
setStatusBar: false,
onClose: onClose,
),
);
},
enableDrag: false,
elevation: 0,
backgroundColor: Colors.transparent,
transitionAnimationController: transitionAnimationController,
sheetAnimationStyle: AnimationStyle(duration: Duration.zero),
);
}
static void inAppWebview(
String url, {
bool off = false,
}) {
if (GStorage.openInBrowser) {
launchURL(url);
} else {
if (off) {
Get.offNamed(
'/webview',
parameters: {'url': url},
arguments: {'inApp': true},
);
} else {
Get.toNamed(
'/webview',
parameters: {'url': url},
arguments: {'inApp': true},
);
}
}
}
static launchURL(String url) async {
try {
final Uri uri = Uri.parse(url);
if (!await launchUrl(
uri,
mode: LaunchMode.externalApplication,
)) {
SmartDialog.showToast('Could not launch $url');
}
} catch (e) {
SmartDialog.showToast(e.toString());
}
}
static void handleWebview(
String url, {
bool off = false,
bool inApp = false,
Map? parameters,
}) async {
if (inApp.not && GStorage.openInBrowser) {
if ((await PiliScheme.routePushFromUrl(url, selfHandle: true)).not) {
launchURL(url);
}
} else {
if (off) {
Get.offNamed(
'/webview',
parameters: {
'url': url,
if (parameters != null) ...parameters,
},
);
} else {
PiliScheme.routePushFromUrl(url, parameters: parameters);
}
}
}
static void showVideoBottomSheet(
BuildContext context, {
required Widget child,
required Function isFullScreen,
double? padding,
}) {
if (!context.mounted) {
return;
}
Get.generalDialog(
barrierLabel: '',
barrierDismissible: true,
pageBuilder: (buildContext, animation, secondaryAnimation) {
return MediaQuery.orientationOf(Get.context!) == Orientation.portrait
? SafeArea(
child: Column(
children: [
const Spacer(flex: 3),
Expanded(flex: 7, child: child),
if (isFullScreen() && padding != null)
SizedBox(height: padding),
],
),
)
: SafeArea(
child: Row(
children: [
const Spacer(),
Expanded(child: child),
],
),
);
},
transitionDuration: const Duration(milliseconds: 350),
transitionBuilder: (context, animation, secondaryAnimation, child) {
Offset begin =
MediaQuery.orientationOf(Get.context!) == Orientation.portrait
? Offset(0.0, 1.0)
: Offset(1.0, 0.0);
var tween = Tween(begin: begin, end: Offset.zero)
.chain(CurveTween(curve: Curves.easeInOut));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
routeSettings: RouteSettings(arguments: Get.arguments),
);
}
static void toVideoPage(
String page, {
dynamic arguments,
int? id,
bool preventDuplicates = true,
Map<String, String>? parameters,
bool off = false,
}) {
if (off) {
Get.offNamed(
'/videoV?$page',
arguments: arguments,
id: id,
preventDuplicates: preventDuplicates,
parameters: parameters,
);
} else {
Get.toNamed(
'/videoV?$page',
arguments: arguments,
id: id,
preventDuplicates: preventDuplicates,
parameters: parameters,
);
}
}
static bool viewPgcFromUri(String uri) {
String? id = RegExp(r'(ep|ss)\d+').firstMatch(uri)?.group(0);
if (id != null) {
bool isSeason = id.startsWith('ss');
id = id.substring(2);
viewBangumi(
seasonId: isSeason ? id : null,
epId: isSeason ? null : id,
);
return true;
}
return false;
}
static void viewBangumi(
{dynamic seasonId, dynamic epId, dynamic progress}) async {
try {
SmartDialog.showLoading(msg: '资源获取中');
var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
SmartDialog.dismiss();
if (result['status']) {
BangumiInfoModel data = result['data'];
// epId episode -> progress episode -> first episode
EpisodeItem? episode;
if (epId != null) {
if (data.episodes?.isNotEmpty == true) {
episode = data.episodes!.firstWhereOrNull(
(item) {
return item.epId.toString() == epId.toString();
},
);
}
if (episode == null && data.section?.isNotEmpty == true) {
for (Section item in data.section!) {
if (item.episodes?.isNotEmpty == true) {
for (EpisodeItem item in item.episodes!) {
if (item.epId.toString() == epId.toString()) {
// view as normal video
toVideoPage(
'bvid=${item.bvid}&cid=${item.cid}&seasonId=${data.seasonId}&epId=${item.epId}',
arguments: {
'pgcApi': true,
'pic': item.cover,
'heroTag': Utils.makeHeroTag(item.cid),
'videoType': SearchType.video,
if (progress != null) 'progress': int.tryParse(progress)
},
preventDuplicates: false,
);
return;
}
}
}
}
}
}
if (data.episodes.isNullOrEmpty) {
SmartDialog.showToast('资源加载失败');
return;
}
episode ??= data.userStatus?.progress?.lastEpId != null
? data.episodes!.firstWhereOrNull(
(item) => item.epId == data.userStatus?.progress?.lastEpId,
) ??
data.episodes!.first
: data.episodes!.first;
toVideoPage(
'bvid=${episode.bvid}&cid=${episode.cid}&seasonId=${data.seasonId}&epId=${episode.epId}&type=${data.type}',
arguments: {
'pic': episode.cover,
'heroTag': Utils.makeHeroTag(episode.cid),
'videoType': SearchType.media_bangumi,
'bangumiItem': data,
if (progress != null) 'progress': int.tryParse(progress)
},
preventDuplicates: false,
);
} else {
SmartDialog.showToast(result['msg']);
}
} catch (e) {
SmartDialog.dismiss();
SmartDialog.showToast('$e');
debugPrint('$e');
}
}
static void toDupNamed(
String page, {
dynamic arguments,
Map<String, String>? parameters,
bool off = false,
}) {
if (off) {
Get.offNamed(
page,
arguments: arguments,
parameters: parameters,
preventDuplicates: false,
);
} else {
Get.toNamed(
page,
arguments: arguments,
parameters: parameters,
preventDuplicates: false,
);
}
}
}

View File

@@ -0,0 +1,386 @@
import 'dart:convert';
import 'dart:math';
import 'package:PiliPlus/common/widgets/radio_widget.dart';
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/models/user/fav_folder.dart';
import 'package:PiliPlus/pages/common/multi_select_controller.dart';
import 'package:PiliPlus/pages/dynamics/tab/controller.dart';
import 'package:PiliPlus/pages/later/controller.dart';
import 'package:PiliPlus/pages/video/detail/introduction/widgets/group_panel.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart' as html_parser;
class RequestUtils {
static Future actionRelationMod({
required BuildContext context,
required dynamic mid,
required bool isFollow,
required ValueChanged<int>? callback,
Map? followStatus,
}) async {
if (mid == null) {
return;
}
feedBack();
if (!isFollow) {
var res = await VideoHttp.relationMod(
mid: mid,
act: 1,
reSrc: 11,
);
SmartDialog.showToast(res['status'] ? "关注成功" : res['msg']);
if (res['status']) {
callback?.call(2);
}
} else {
if (followStatus == null) {
Map<String, dynamic> result = await UserHttp.hasFollow(mid);
if (result['status']) {
followStatus = result['data'];
} else {
SmartDialog.showToast(result['msg']);
return;
}
}
if (context.mounted) {
showDialog(
context: context,
builder: (context) {
bool isSpecialFollowed = followStatus!['special'] == 1;
String text = isSpecialFollowed ? '移除特别关注' : '加入特别关注';
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
dense: true,
onTap: () async {
Get.back();
final res = await MemberHttp.specialAction(
fid: mid,
isAdd: !isSpecialFollowed,
);
if (res['status']) {
SmartDialog.showToast('$text成功');
callback?.call(-10);
} else {
SmartDialog.showToast(res['msg']);
}
},
title: Text(
text,
style: const TextStyle(fontSize: 14),
),
),
ListTile(
dense: true,
onTap: () async {
Get.back();
var result = await showModalBottomSheet<List?>(
context: context,
useSafeArea: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
sheetAnimationStyle: AnimationStyle(curve: Curves.ease),
constraints: BoxConstraints(
maxWidth: min(640, min(Get.width, Get.height)),
),
builder: (BuildContext context) {
return DraggableScrollableSheet(
minChildSize: 0,
maxChildSize: 1,
initialChildSize: 0.7,
snap: true,
expand: false,
snapSizes: const [0.7],
builder: (BuildContext context,
ScrollController scrollController) {
return GroupPanel(
mid: mid,
tags: followStatus!['tag'],
scrollController: scrollController,
);
},
);
},
);
followStatus!['tag'] = result;
if (result != null) {
callback?.call(2);
}
},
title: const Text(
'设置分组',
style: TextStyle(fontSize: 14),
),
),
ListTile(
dense: true,
onTap: () async {
Get.back();
var res = await VideoHttp.relationMod(
mid: mid,
act: 2,
reSrc: 11,
);
SmartDialog.showToast(
res['status'] ? "取消关注成功" : res['msg']);
if (res['status']) {
callback?.call(0);
}
},
title: const Text(
'取消关注',
style: TextStyle(fontSize: 14),
),
),
],
),
);
},
);
}
}
}
static ReplyInfo replyCast(res) {
Map? emote = res['content']['emote'];
emote?.forEach((key, value) {
value['size'] = value['meta']['size'];
});
return ReplyInfo.create()
..mergeFromProto3Json(
res
..['id'] = res['rpid']
..['member']['name'] = res['member']['uname']
..['member']['face'] = res['member']['avatar']
..['member']['level'] = res['member']['level_info']['current_level']
..['member']['vipStatus'] = res['member']['vip']['vipStatus']
..['member']['vipType'] = res['member']['vip']['vipType']
..['member']['officialVerifyType'] =
res['member']['official_verify']['type']
..['content']['emote'] = emote,
ignoreUnknownFields: true,
);
}
static Future<dynamic> getWwebid(mid) async {
try {
dynamic response = await Request().get(
'${HttpString.spaceBaseUrl}/$mid/dynamic',
options: Options(
extra: {'account': AnonymousAccount()},
),
);
dom.Document document = html_parser.parse(response.data);
dom.Element? scriptElement =
document.querySelector('script#__RENDER_DATA__');
return jsonDecode(
Uri.decodeComponent(scriptElement?.text ?? ''))['access_id'];
} catch (e) {
debugPrint('failed to get wwebid: $e');
return null;
}
}
static Future insertCreatedDyn(result) async {
try {
dynamic id = result['data']['dyn_id'];
if (id != null) {
await Future.delayed(const Duration(milliseconds: 200));
dynamic res = await DynamicsHttp.dynamicDetail(id: id);
if (res['status']) {
final ctr = Get.find<DynamicsTabController>(tag: 'all');
if (ctr.loadingState.value is Success) {
List<DynamicItemModel>? list =
(ctr.loadingState.value as Success).response;
if (list != null) {
list.insert(0, res['data']);
ctr.loadingState.refresh();
return;
}
}
ctr.loadingState.value = LoadingState.success([res['data']]);
}
}
} catch (e) {
debugPrint('create dyn $e');
}
}
static Future checkCreatedDyn({id, dynText, isManual}) async {
if (isManual == true || GStorage.enableCreateDynAntifraud) {
try {
if (id != null) {
if (isManual != true) {
await Future.delayed(const Duration(seconds: 5));
}
dynamic res =
await DynamicsHttp.dynamicDetail(id: id, clearCookie: true);
showDialog(
context: Get.context!,
builder: (context) => AlertDialog(
title: Text('动态检查结果'),
content: SelectableText(
'${res['status'] ? '无账号状态下找到了你的动态,动态正常!' : '你的动态被shadow ban仅自己可见'}${dynText != null ? ' \n\n动态内容: $dynText' : ''}'),
),
);
}
} catch (e) {
debugPrint('check dyn error: $e');
}
}
}
// 动态点赞
static Future onLikeDynamic(item, VoidCallback callback) async {
feedBack();
String dynamicId = item.idStr!;
// 1 已点赞 2 不喜欢 0 未操作
item.modules?.moduleStat ??= ModuleStatModel();
item.modules?.moduleStat.like ??= Like();
Like like = item.modules.moduleStat.like;
int count = like.count == '点赞' ? 0 : int.parse(like.count ?? '0');
bool status = like.status ?? false;
int up = status ? 2 : 1;
var res = await DynamicsHttp.likeDynamic(dynamicId: dynamicId, up: up);
if (res['status']) {
SmartDialog.showToast(!status ? '点赞成功' : '取消赞');
if (up == 1) {
item.modules.moduleStat.like.count = (count + 1).toString();
item.modules.moduleStat.like.status = true;
} else {
if (count == 1) {
item.modules.moduleStat.like.count = '点赞';
} else {
item.modules.moduleStat.like.count = (count - 1).toString();
}
item.modules.moduleStat.like.status = false;
}
callback();
} else {
SmartDialog.showToast(res['msg']);
}
}
static void onCopyOrMove<R, T extends MultiSelectData>({
required BuildContext context,
required bool isCopy,
required MultiSelectController<R, T> ctr,
required dynamic mediaId,
required dynamic mid,
}) {
VideoHttp.allFavFolders(mid).then((res) {
if (context.mounted &&
res['status'] &&
(res['data'].list as List?)?.isNotEmpty == true) {
List<FavFolderItemData> list = res['data'].list;
dynamic checkedId;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('${isCopy ? '复制' : '移动'}'),
contentPadding: const EdgeInsets.only(top: 5),
content: SingleChildScrollView(
child: Builder(
builder: (context) => Column(
children: List.generate(list.length, (index) {
return RadioWidget(
padding: const EdgeInsets.only(left: 14),
title: list[index].title ?? '',
groupValue: checkedId,
value: list[index].id,
onChanged: (value) {
checkedId = value;
if (context.mounted) {
(context as Element).markNeedsBuild();
}
},
);
}),
),
),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style:
TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () {
if (checkedId != null) {
List resources = ((ctr.loadingState.value as Success)
.response as List<T>)
.where((e) => e.checked == true)
.toList();
SmartDialog.showLoading();
VideoHttp.copyOrMoveFav(
isCopy: isCopy,
isFav: ctr is! LaterController,
srcMediaId: mediaId,
tarMediaId: checkedId,
resources: resources
.map((item) => ctr is LaterController
? item.aid
: '${item.id}:${item.type}')
.toList(),
mid: isCopy ? mid : null,
).then((res) {
if (res['status']) {
ctr.handleSelect(false);
if (isCopy.not) {
List<T> dataList =
(ctr.loadingState.value as Success).response;
List<T> remainList = dataList
.toSet()
.difference(resources.toSet())
.toList();
ctr.loadingState.value =
LoadingState.success(remainList);
}
SmartDialog.dismiss();
SmartDialog.showToast('${isCopy ? '复制' : '移动'}成功');
Get.back();
} else {
SmartDialog.dismiss();
SmartDialog.showToast('${res['msg']}');
}
});
}
},
child: Text('确认'),
),
],
);
},
);
} else {
SmartDialog.showToast('${res['msg']}');
}
});
}
}

View File

@@ -25,7 +25,7 @@ import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/accounts/account_adapter.dart';
import 'package:PiliPlus/utils/accounts/cookie_jar_adapter.dart';
import 'package:PiliPlus/utils/accounts/account_type_adapter.dart';
import 'package:PiliPlus/utils/login.dart';
import 'package:PiliPlus/utils/login_utils.dart';
import 'package:PiliPlus/utils/set_int_adapter.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:flutter/material.dart';

155
lib/utils/theme_utils.dart Normal file
View File

@@ -0,0 +1,155 @@
import 'package:PiliPlus/main.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:flex_seed_scheme/flex_seed_scheme.dart';
import 'package:flutter/material.dart';
class ThemeUtils {
static ThemeData getThemeData({
required ColorScheme colorScheme,
required bool isDynamic,
bool isDark = false,
required FlexSchemeVariant variant,
}) {
final appFontWeight =
GStorage.appFontWeight.clamp(-1, FontWeight.values.length - 1);
final fontWeight =
appFontWeight == -1 ? null : FontWeight.values[appFontWeight];
late final textStyle = TextStyle(fontWeight: fontWeight);
ThemeData themeData = ThemeData(
colorScheme: colorScheme,
useMaterial3: true,
textTheme: fontWeight == null
? null
: TextTheme(
displayLarge: textStyle,
displayMedium: textStyle,
displaySmall: textStyle,
headlineLarge: textStyle,
headlineMedium: textStyle,
headlineSmall: textStyle,
titleLarge: textStyle,
titleMedium: textStyle,
titleSmall: textStyle,
bodyLarge: textStyle,
bodyMedium: textStyle,
bodySmall: textStyle,
labelLarge: textStyle,
labelMedium: textStyle,
labelSmall: textStyle,
),
tabBarTheme:
fontWeight == null ? null : TabBarTheme(labelStyle: textStyle),
appBarTheme: AppBarTheme(
elevation: 0,
titleSpacing: 0,
centerTitle: false,
scrolledUnderElevation: 0,
backgroundColor: isDynamic ? null : colorScheme.surface,
titleTextStyle: TextStyle(
fontSize: 16,
color: colorScheme.onSurface,
fontWeight: fontWeight,
),
),
navigationBarTheme: NavigationBarThemeData(
surfaceTintColor: isDynamic ? colorScheme.onSurfaceVariant : null,
),
snackBarTheme: SnackBarThemeData(
actionTextColor: colorScheme.primary,
backgroundColor: colorScheme.secondaryContainer,
closeIconColor: colorScheme.secondary,
contentTextStyle: TextStyle(color: colorScheme.secondary),
elevation: 20,
),
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: ZoomPageTransitionsBuilder(
allowEnterRouteSnapshotting: false,
),
},
),
popupMenuTheme: PopupMenuThemeData(
surfaceTintColor: isDynamic ? colorScheme.onSurfaceVariant : null,
),
cardTheme: CardTheme(
elevation: 1,
surfaceTintColor: isDynamic
? colorScheme.onSurfaceVariant
: isDark
? colorScheme.onSurfaceVariant
: null,
shadowColor: Colors.transparent,
),
// dialogTheme: DialogTheme(
// surfaceTintColor: isDark ? colorScheme.onSurfaceVariant : null,
// ),
progressIndicatorTheme: ProgressIndicatorThemeData(
refreshBackgroundColor: colorScheme.onSecondary,
),
dialogTheme: DialogTheme(
titleTextStyle: TextStyle(
fontSize: 18,
color: colorScheme.onSurface,
fontWeight: fontWeight,
),
),
);
if (isDark && GStorage.isPureBlackTheme) {
themeData = darkenTheme(themeData);
}
if (isDark && GStorage.darkVideoPage) {
MyApp.darkThemeData = themeData;
}
return themeData;
}
static darkenTheme(ThemeData themeData) {
Color color = themeData.colorScheme.surfaceContainerHighest.darken(0.7);
return themeData.copyWith(
scaffoldBackgroundColor: Colors.black,
appBarTheme: themeData.appBarTheme.copyWith(
backgroundColor: Colors.black,
),
cardTheme: themeData.cardTheme.copyWith(
color: Colors.black,
),
dialogTheme: themeData.dialogTheme.copyWith(
backgroundColor: color,
),
bottomSheetTheme:
themeData.bottomSheetTheme.copyWith(backgroundColor: color),
bottomNavigationBarTheme:
themeData.bottomNavigationBarTheme.copyWith(backgroundColor: color),
navigationBarTheme:
themeData.navigationBarTheme.copyWith(backgroundColor: color),
navigationRailTheme:
themeData.navigationRailTheme.copyWith(backgroundColor: Colors.black),
colorScheme: themeData.colorScheme.copyWith(
primary: themeData.colorScheme.primary.darken(0.1),
onPrimary: themeData.colorScheme.onPrimary.darken(0.1),
primaryContainer: themeData.colorScheme.primaryContainer.darken(0.1),
onPrimaryContainer:
themeData.colorScheme.onPrimaryContainer.darken(0.1),
inversePrimary: themeData.colorScheme.inversePrimary.darken(0.1),
secondary: themeData.colorScheme.secondary.darken(0.1),
onSecondary: themeData.colorScheme.onSecondary.darken(0.1),
secondaryContainer:
themeData.colorScheme.secondaryContainer.darken(0.1),
onSecondaryContainer:
themeData.colorScheme.onSecondaryContainer.darken(0.1),
error: themeData.colorScheme.error.darken(0.1),
surface: Colors.black,
onSurface: themeData.colorScheme.onSurface.darken(0.15),
surfaceTint: themeData.colorScheme.surfaceTint.darken(),
inverseSurface: themeData.colorScheme.inverseSurface.darken(),
onInverseSurface: themeData.colorScheme.onInverseSurface.darken(),
surfaceContainer: themeData.colorScheme.surfaceContainer.darken(),
surfaceContainerHigh:
themeData.colorScheme.surfaceContainerHigh.darken(),
surfaceContainerHighest:
themeData.colorScheme.surfaceContainerHighest.darken(0.4),
),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -65,7 +66,7 @@ class UrlUtils {
String? bvid = matchRes['BV'];
bvid ??= IdUtils.av2bv(aid!);
final int cid = await SearchHttp.ab2c(aid: aid, bvid: bvid);
Utils.toViewPage(
PageUtils.toVideoPage(
'bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'pic': '',
@@ -75,7 +76,7 @@ class UrlUtils {
);
} else {
if (redirectUrl.isNotEmpty) {
Utils.handleWebview(redirectUrl);
PageUtils.handleWebview(redirectUrl);
} else {
SmartDialog.showToast('matchUrlPush: $pathSegment');
}

File diff suppressed because it is too large Load Diff