Merge branch 'main' into fix

This commit is contained in:
guozhigq
2024-10-27 23:36:33 +08:00
50 changed files with 1349 additions and 704 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -15,4 +15,5 @@ class Constants {
// 59b43e04ad6965f34319062b478f83dd TV端 // 59b43e04ad6965f34319062b478f83dd TV端
static const String appSec = '59b43e04ad6965f34319062b478f83dd'; static const String appSec = '59b43e04ad6965f34319062b478f83dd';
static const String thirdSign = '04224646d1fea004e79606d3b038c84a'; static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
static const List<int> publicFavFolder = <int>[0, 2, 22];
} }

View File

@ -430,7 +430,7 @@ class EpisodeGridItem extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: isCurrentIndex color: isCurrentIndex
? colorScheme.primaryContainer.withOpacity(0.6) ? colorScheme.primaryContainer.withOpacity(0.6)
: colorScheme.secondaryContainer.withOpacity(0.4), : colorScheme.onInverseSurface.withOpacity(0.6),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all( border: Border.all(
color: isCurrentIndex color: isCurrentIndex

View File

@ -3,14 +3,9 @@ import 'package:pilipala/common/constants.dart';
import 'skeleton.dart'; import 'skeleton.dart';
class MediaBangumiSkeleton extends StatefulWidget { class MediaBangumiSkeleton extends StatelessWidget {
const MediaBangumiSkeleton({super.key}); const MediaBangumiSkeleton({super.key});
@override
State<MediaBangumiSkeleton> createState() => _MediaBangumiSkeletonState();
}
class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface; Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
@ -35,25 +30,25 @@ class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
color: Theme.of(context).colorScheme.onInverseSurface, color: bgColor,
width: 200, width: 200,
height: 20, height: 20,
margin: const EdgeInsets.only(bottom: 15), margin: const EdgeInsets.only(bottom: 15),
), ),
Container( Container(
color: Theme.of(context).colorScheme.onInverseSurface, color: bgColor,
width: 150, width: 150,
height: 13, height: 13,
margin: const EdgeInsets.only(bottom: 5), margin: const EdgeInsets.only(bottom: 5),
), ),
Container( Container(
color: Theme.of(context).colorScheme.onInverseSurface, color: bgColor,
width: 150, width: 150,
height: 13, height: 13,
margin: const EdgeInsets.only(bottom: 5), margin: const EdgeInsets.only(bottom: 5),
), ),
Container( Container(
color: Theme.of(context).colorScheme.onInverseSurface, color: bgColor,
width: 150, width: 150,
height: 13, height: 13,
), ),
@ -64,7 +59,7 @@ class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: borderRadius:
const BorderRadius.all(Radius.circular(20)), const BorderRadius.all(Radius.circular(20)),
color: Theme.of(context).colorScheme.onInverseSurface, color: bgColor,
), ),
), ),
], ],

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import '../constants.dart';
class UserListSkeleton extends StatelessWidget {
const UserListSkeleton({super.key});
@override
Widget build(BuildContext context) {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace, vertical: 7),
child: Row(
children: [
ClipOval(
child: Container(width: 42, height: 42, color: bgColor),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(color: bgColor, width: 60, height: 13),
const SizedBox(width: 10),
Container(color: bgColor, width: 40, height: 13),
],
),
const SizedBox(height: 6),
Container(
color: bgColor,
width: 100,
height: 13,
),
],
),
),
],
));
}
}

View File

@ -4,7 +4,7 @@ import 'package:flutter_svg/flutter_svg.dart';
class HttpError extends StatelessWidget { class HttpError extends StatelessWidget {
const HttpError({ const HttpError({
required this.errMsg, required this.errMsg,
required this.fn, this.fn,
this.btnText, this.btnText,
this.isShowBtn = true, this.isShowBtn = true,
this.isInSliver = true, this.isInSliver = true,
@ -23,7 +23,6 @@ class HttpError extends StatelessWidget {
final errorContent = SizedBox( final errorContent = SizedBox(
height: 400, height: 400,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
SvgPicture.asset("assets/images/error.svg", height: 200), SvgPicture.asset("assets/images/error.svg", height: 200),
@ -50,7 +49,7 @@ class HttpError extends StatelessWidget {
if (isInSliver) { if (isInSliver) {
return SliverToBoxAdapter(child: errorContent); return SliverToBoxAdapter(child: errorContent);
} else { } else {
return Center(child: errorContent); return Align(alignment: Alignment.topCenter, child: errorContent);
} }
} }
} }

View File

@ -301,10 +301,6 @@ class Api {
static const String bangumiList = static const String bangumiList =
'/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1&copyright=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1'; '/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1&copyright=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1';
// 我的订阅
static const String bangumiFollow =
'/x/space/bangumi/follow/list?type=1&follow_status=0&pn=1&ps=15&ts=1691544359969';
// 黑名单 // 黑名单
static const String blackLst = '/x/relation/blacks'; static const String blackLst = '/x/relation/blacks';
@ -604,4 +600,10 @@ class Api {
/// 图片上传 /// 图片上传
static const String uploadImage = '/x/dynamic/feed/draw/upload_bfs'; static const String uploadImage = '/x/dynamic/feed/draw/upload_bfs';
/// 更新追番状态
static const String updateBangumiStatus = '/pgc/web/follow/status/update';
/// 番剧点赞投币收藏状态
static const String bangumiActionStatus = '/pgc/season/episode/community';
} }

View File

@ -1,5 +1,8 @@
import 'dart:convert';
import '../models/bangumi/list.dart'; import '../models/bangumi/list.dart';
import 'index.dart'; import 'index.dart';
import 'package:html/parser.dart' as html_parser;
import 'package:html/dom.dart' as html_dom;
class BangumiHttp { class BangumiHttp {
static Future bangumiList({int? page}) async { static Future bangumiList({int? page}) async {
@ -18,8 +21,19 @@ class BangumiHttp {
} }
} }
static Future bangumiFollow({int? mid}) async { static Future getRecentBangumi({
var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid}); int? mid,
int type = 1,
int pn = 1,
int ps = 20,
}) async {
var res = await Request().get(Api.getRecentBangumiApi, data: {
'vmid': mid,
'type': type,
'follow_status': 0,
'pn': pn,
'ps': ps,
});
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
@ -33,4 +47,62 @@ class BangumiHttp {
}; };
} }
} }
// 获取追番状态
static Future bangumiStatus({required int seasonId}) async {
var res = await Request()
.get('https://www.bilibili.com/bangumi/play/ss$seasonId');
html_dom.Document document = html_parser.parse(res.data);
// 查找 id 为 __NEXT_DATA__ 的 script 元素
html_dom.Element? scriptElement =
document.querySelector('script#\\__NEXT_DATA__');
if (scriptElement != null) {
// 提取 script 元素的内容
String scriptContent = scriptElement.text;
final dynamic scriptContentJson = jsonDecode(scriptContent);
Map followState = scriptContentJson['props']['pageProps']['followState'];
return {
'status': true,
'data': {
'isFollowed': followState['isFollowed'],
'followStatus': followState['followStatus']
}
};
} else {
print('Script element with id "__NEXT_DATA__" not found.');
}
}
// 更新追番状态
static Future updateBangumiStatus({
required int seasonId,
required int status,
}) async {
var res = await Request().post(Api.updateBangumiStatus, data: {
'season_id': seasonId,
'status': status,
});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取番剧点赞投币收藏状态
static Future bangumiActionStatus({required int epId}) async {
var res = await Request().get(
Api.bangumiActionStatus,
data: {'ep_id': epId},
);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
} }

View File

@ -47,6 +47,7 @@ class BangumiListItemModel {
this.title, this.title,
this.titleIcon, this.titleIcon,
this.progress, this.progress,
this.progressIndex,
}); });
String? badge; String? badge;
@ -66,8 +67,8 @@ class BangumiListItemModel {
String? subTitle; String? subTitle;
String? title; String? title;
String? titleIcon; String? titleIcon;
String? progress; String? progress;
int? progressIndex;
BangumiListItemModel.fromJson(Map<String, dynamic> json) { BangumiListItemModel.fromJson(Map<String, dynamic> json) {
badge = json['badge'] == '' ? null : json['badge']; badge = json['badge'] == '' ? null : json['badge'];
@ -87,7 +88,9 @@ class BangumiListItemModel {
subTitle = json['sub_title']; subTitle = json['sub_title'];
title = json['title']; title = json['title'];
titleIcon = json['title_icon']; titleIcon = json['title_icon'];
progress = json['progress']; progress = json['progress'];
progressIndex = int.parse(
RegExp(r'第(\d+)话').firstMatch(json['progress'] ?? '第1话')?.group(1) ??
'0');
} }
} }

View File

@ -63,7 +63,7 @@ class LiveFollowingItemModel {
String? roomNews; String? roomNews;
String? watchIcon; String? watchIcon;
String? textSmall; String? textSmall;
String? roomCover; String? cover;
String? pic; String? pic;
int? parentAreaId; int? parentAreaId;
int? areaId; int? areaId;
@ -90,7 +90,7 @@ class LiveFollowingItemModel {
this.roomNews, this.roomNews,
this.watchIcon, this.watchIcon,
this.textSmall, this.textSmall,
this.roomCover, this.cover,
this.pic, this.pic,
this.parentAreaId, this.parentAreaId,
this.areaId, this.areaId,
@ -108,7 +108,8 @@ class LiveFollowingItemModel {
isAttention = json['is_attention']; isAttention = json['is_attention'];
clipNum = json['clipnum']; clipNum = json['clipnum'];
fansNum = json['fans_num']; fansNum = json['fans_num'];
areaName = json['area_name']; areaName =
json['area_name'] == '' ? json['area_name_v2'] : json['area_name'];
areaValue = json['area_value']; areaValue = json['area_value'];
tags = json['tags']; tags = json['tags'];
recentRecordIdV2 = json['recent_record_id_v2']; recentRecordIdV2 = json['recent_record_id_v2'];
@ -118,7 +119,7 @@ class LiveFollowingItemModel {
roomNews = json['room_news']; roomNews = json['room_news'];
watchIcon = json['watch_icon']; watchIcon = json['watch_icon'];
textSmall = json['text_small']; textSmall = json['text_small'];
roomCover = json['room_cover']; cover = json['room_cover'];
pic = json['room_cover']; pic = json['room_cover'];
parentAreaId = json['parent_area_id']; parentAreaId = json['parent_area_id'];
areaId = json['area_id']; areaId = json['area_id'];

View File

@ -8,6 +8,7 @@ class MemberInfoModel {
this.level, this.level,
this.isFollowed, this.isFollowed,
this.topPhoto, this.topPhoto,
this.silence,
this.official, this.official,
this.vip, this.vip,
this.liveRoom, this.liveRoom,
@ -21,6 +22,7 @@ class MemberInfoModel {
int? level; int? level;
bool? isFollowed; bool? isFollowed;
String? topPhoto; String? topPhoto;
int? silence;
Map? official; Map? official;
Vip? vip; Vip? vip;
LiveRoom? liveRoom; LiveRoom? liveRoom;
@ -34,6 +36,7 @@ class MemberInfoModel {
level = json['level']; level = json['level'];
isFollowed = json['is_followed']; isFollowed = json['is_followed'];
topPhoto = json['top_photo']; topPhoto = json['top_photo'];
silence = json['silence'] ?? 0;
official = json['official']; official = json['official'];
vip = Vip.fromJson(json['vip']); vip = Vip.fromJson(json['vip']);
liveRoom = liveRoom =

View File

@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/github/latest.dart'; import 'package:pilipala/models/github/latest.dart';
import 'package:pilipala/plugin/pl_gallery/index.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../utils/cache_manage.dart'; import '../../utils/cache_manage.dart';
@ -124,7 +125,7 @@ class _AboutPageState extends State<AboutPage> {
onTap: () => _aboutController.webSiteUrl(), onTap: () => _aboutController.webSiteUrl(),
title: const Text('访问官网'), title: const Text('访问官网'),
trailing: Text( trailing: Text(
'https://pilipalanet.mysxl.cn', 'https://pilipala.life',
style: subTitleStyle, style: subTitleStyle,
), ),
), ),
@ -168,7 +169,7 @@ class _AboutPageState extends State<AboutPage> {
onTap: () => _aboutController.tgChanel(), onTap: () => _aboutController.tgChanel(),
title: const Text('TG频道'), title: const Text('TG频道'),
trailing: Text( trailing: Text(
'https://t.me/+lm_oOVmF0RJiODk1', 'https://t.me/+1DFtqS6usUM5MDNl',
style: subTitleStyle, style: subTitleStyle,
), ),
), ),
@ -321,29 +322,35 @@ class AboutController extends GetxController {
// tg频道 // tg频道
tgChanel() { tgChanel() {
Clipboard.setData( Clipboard.setData(
const ClipboardData(text: 'https://t.me/+lm_oOVmF0RJiODk1'), const ClipboardData(text: 'https://t.me/+1DFtqS6usUM5MDNl'),
); );
SmartDialog.showToast( SmartDialog.showToast(
'已复制,即将在浏览器打开', '已复制,即将在浏览器打开',
displayTime: const Duration(milliseconds: 500), displayTime: const Duration(milliseconds: 500),
).then( ).then(
(value) => launchUrl( (value) => launchUrl(
Uri.parse('https://t.me/+lm_oOVmF0RJiODk1'), Uri.parse('https://t.me/+1DFtqS6usUM5MDNl'),
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
), ),
); );
} }
aPay() { aPay() {
try { const List<String> sources = [
launchUrl( 'assets/images/pay/wechat.png',
Uri.parse( 'assets/images/pay/alipay.jpg'
'alipayqr://platformapi/startapp?saId=10000007&qrcode=https://qr.alipay.com/fkx14623ddwl1ping3ddd73'), ];
mode: LaunchMode.externalApplication, Navigator.of(Get.context!).push(
); HeroDialogRoute<void>(
} catch (e) { builder: (BuildContext context) => InteractiveviewerGallery(
print(e); sources: sources,
} initIndex: 0,
itemBuilder: (context, index, isFocus, enablePageView) =>
Image.asset(sources[index]),
actionType: const [ImgActionType.save],
),
),
);
} }
// 官网 // 官网

View File

@ -9,6 +9,7 @@ class BangumiController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
RxList<BangumiListItemModel> bangumiList = <BangumiListItemModel>[].obs; RxList<BangumiListItemModel> bangumiList = <BangumiListItemModel>[].obs;
RxList<BangumiListItemModel> bangumiFollowList = <BangumiListItemModel>[].obs; RxList<BangumiListItemModel> bangumiFollowList = <BangumiListItemModel>[].obs;
RxInt total = 0.obs;
int _currentPage = 1; int _currentPage = 1;
bool isLoadingMore = true; bool isLoadingMore = true;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
@ -54,9 +55,10 @@ class BangumiController extends GetxController {
if (userInfo == null) { if (userInfo == null) {
return; return;
} }
var result = await BangumiHttp.bangumiFollow(mid: userInfo.mid); var result = await BangumiHttp.getRecentBangumi(mid: userInfo.mid);
if (result['status']) { if (result['status']) {
bangumiFollowList.value = result['data'].list; bangumiFollowList.value = result['data'].list;
total.value = result['data'].total;
} else {} } else {}
return result; return result;
} }

View File

@ -2,6 +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:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/bangumi.dart';
import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
@ -52,28 +53,34 @@ class BangumiIntroController extends GetxController {
Rx<FavFolderData> favFolderData = FavFolderData().obs; Rx<FavFolderData> favFolderData = FavFolderData().obs;
List addMediaIdsNew = []; List addMediaIdsNew = [];
List delMediaIdsNew = []; List delMediaIdsNew = [];
// 关注状态 默认未关注 // 追番状态 1想看 2在看 3已看
RxMap followStatus = {}.obs; RxBool isFollowed = false.obs;
RxInt followStatus = 1.obs;
int _tempThemeValue = -1; int _tempThemeValue = -1;
var userInfo; var userInfo;
PersistentBottomSheetController? bottomSheetController; PersistentBottomSheetController? bottomSheetController;
List<Map<String, dynamic>> followStatusList = [
{'title': '标记为 「想看」', 'status': 1},
{'title': '标记为 「在看」', 'status': 2},
{'title': '标记为 「已看」', 'status': 3},
{'title': '取消追番', 'status': -1},
];
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
userLogin = userInfo != null; userLogin = userInfo != null;
if (userLogin && seasonId != null) {
bangumiStatus();
}
} }
// 获取番剧简介&选集 // 获取番剧简介&选集
Future queryBangumiIntro() async { Future queryBangumiIntro() async {
if (userLogin) { if (userLogin) {
// 获取点赞状态 // 获取点赞投币收藏状态
queryHasLikeVideo(); bangumiActionStatus();
// 获取投币状态
queryHasCoinVideo();
// 获取收藏状态
queryHasFavVideo();
} }
var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId); var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
if (result['status']) { if (result['status']) {
@ -83,26 +90,15 @@ class BangumiIntroController extends GetxController {
return result; return result;
} }
// 获取点赞状态 // 获取番剧点赞投币收藏状态
Future queryHasLikeVideo() async { Future bangumiActionStatus() async {
var result = await VideoHttp.hasLikeVideo(bvid: bvid); var result = await BangumiHttp.bangumiActionStatus(epId: epId!);
// data num 被点赞标志 0未点赞 1已点赞
hasLike.value = result["data"] == 1 ? true : false;
}
// 获取投币状态
Future queryHasCoinVideo() async {
var result = await VideoHttp.hasCoinVideo(bvid: bvid);
hasCoin.value = result["data"]['multiply'] == 0 ? false : true;
}
// 获取收藏状态
Future queryHasFavVideo() async {
var result = await VideoHttp.hasFavVideo(aid: IdUtils.bv2av(bvid));
if (result['status']) { if (result['status']) {
hasFav.value = result["data"]['favoured']; hasLike.value = result['data']['like'] == 1;
hasCoin.value = result['data']['coin_number'] != 0;
hasFav.value = result['data']['favorite'] == 1;
} else { } else {
hasFav.value = false; SmartDialog.showToast(result['msg']);
} }
} }
@ -110,7 +106,7 @@ class BangumiIntroController extends GetxController {
Future actionLikeVideo() async { Future actionLikeVideo() async {
var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value); var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);
if (result['status']) { if (result['status']) {
SmartDialog.showToast(!hasLike.value ? '点赞成功 👍' : '取消赞'); SmartDialog.showToast(!hasLike.value ? '点赞成功' : '取消赞');
hasLike.value = !hasLike.value; hasLike.value = !hasLike.value;
bangumiDetail.value.stat!['likes'] = bangumiDetail.value.stat!['likes'] =
bangumiDetail.value.stat!['likes'] + (!hasLike.value ? 1 : -1); bangumiDetail.value.stat!['likes'] + (!hasLike.value ? 1 : -1);
@ -147,7 +143,7 @@ class BangumiIntroController extends GetxController {
var res = await VideoHttp.coinVideo( var res = await VideoHttp.coinVideo(
bvid: bvid, multiply: _tempThemeValue); bvid: bvid, multiply: _tempThemeValue);
if (res['status']) { if (res['status']) {
SmartDialog.showToast('投币成功 👏'); SmartDialog.showToast('投币成功');
hasCoin.value = true; hasCoin.value = true;
bangumiDetail.value.stat!['coins'] = bangumiDetail.value.stat!['coins'] =
bangumiDetail.value.stat!['coins'] + bangumiDetail.value.stat!['coins'] +
@ -185,9 +181,11 @@ class BangumiIntroController extends GetxController {
addMediaIdsNew = []; addMediaIdsNew = [];
delMediaIdsNew = []; delMediaIdsNew = [];
// 重新获取收藏状态 // 重新获取收藏状态
queryHasFavVideo(); bangumiActionStatus();
SmartDialog.showToast('操作成功'); SmartDialog.showToast('操作成功');
Get.back(); Get.back();
} else {
SmartDialog.showToast(result['msg']);
} }
} }
@ -239,15 +237,22 @@ class BangumiIntroController extends GetxController {
// 追番 // 追番
Future bangumiAdd() async { Future bangumiAdd() async {
var result = var result = await VideoHttp.bangumiAdd(
await VideoHttp.bangumiAdd(seasonId: bangumiDetail.value.seasonId); seasonId: seasonId ?? bangumiDetail.value.seasonId);
if (result['status']) {
followStatus.value = 2;
isFollowed.value = true;
}
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);
} }
// 取消追番 // 取消追番
Future bangumiDel() async { Future bangumiDel() async {
var result = var result = await VideoHttp.bangumiDel(
await VideoHttp.bangumiDel(seasonId: bangumiDetail.value.seasonId); seasonId: seasonId ?? bangumiDetail.value.seasonId);
if (result['status']) {
isFollowed.value = false;
}
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);
} }
@ -315,4 +320,35 @@ class BangumiIntroController extends GetxController {
hiddenEpisodeBottomSheet() { hiddenEpisodeBottomSheet() {
bottomSheetController?.close(); bottomSheetController?.close();
} }
// 获取追番状态
Future bangumiStatus() async {
var result = await BangumiHttp.bangumiStatus(seasonId: seasonId!);
if (result['status']) {
followStatus.value = result['data']['followStatus'];
isFollowed.value = result['data']['isFollowed'];
}
return result;
}
// 更新追番状态
Future updateBangumiStatus(int status) async {
Get.back();
if (status == -1) {
bangumiDel();
} else {
var result = await BangumiHttp.bangumiStatus(seasonId: seasonId!);
if (result['status']) {
followStatus.value = status;
final title = followStatusList.firstWhere(
(e) => e['status'] == status,
orElse: () => {'title': '未知状态'},
)['title'];
SmartDialog.showToast('追番状态$title');
} else {
SmartDialog.showToast(result['msg']);
}
return result;
}
}
} }

View File

@ -239,104 +239,65 @@ class _BangumiInfoState extends State<BangumiInfo> {
Expanded( Expanded(
child: InkWell( child: InkWell(
onTap: () => showIntroDetail(), onTap: () => showIntroDetail(),
borderRadius: BorderRadius.circular(8),
child: SizedBox( child: SizedBox(
height: 115 / 0.75, height: 115 / 0.75,
child: Column( child: Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.fromLTRB(6, 4, 6, 6),
mainAxisSize: MainAxisSize.min, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Row( mainAxisSize: MainAxisSize.min,
children: [ children: [
Expanded( Row(
child: Text( crossAxisAlignment: CrossAxisAlignment.center,
widget.bangumiDetail!.title!, children: [
style: const TextStyle( Expanded(
fontSize: 16, child: Text(
fontWeight: FontWeight.w500, widget.bangumiDetail!.title!,
), style: const TextStyle(
maxLines: 1, fontSize: 18,
overflow: TextOverflow.ellipsis, fontWeight: FontWeight.bold,
), ),
), maxLines: 2,
const SizedBox(width: 20), overflow: TextOverflow.ellipsis,
SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith(
(Set<MaterialState> states) {
return t.colorScheme.primaryContainer
.withOpacity(0.7);
}),
),
onPressed: () =>
bangumiIntroController.bangumiAdd(),
icon: Icon(
Icons.favorite_border_rounded,
color: t.colorScheme.primary,
size: 22,
), ),
), ),
), const SizedBox(width: 20),
], Obx(
), () => BangumiStatusWidget(
Row( ctr: bangumiIntroController,
children: [ isFollowed:
StatView( bangumiIntroController.isFollowed.value,
view: widget.bangumiDetail!.stat!['views'], ),
size: 'medium',
),
const SizedBox(width: 6),
StatDanMu(
danmu: widget.bangumiDetail!.stat!['danmakus'],
size: 'medium',
),
],
),
const SizedBox(height: 6),
Row(
children: [
Text(
(widget.bangumiDetail!.areas!.isNotEmpty
? widget.bangumiDetail!.areas!.first['name']
: ''),
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
), ),
), ],
const SizedBox(width: 6),
Text(
widget.bangumiDetail!.publish!['pub_time_show'],
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
],
),
Text(
widget.bangumiDetail!.newEp!['desc'],
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
), ),
), const SizedBox(height: 4),
const Spacer(), Row(
Text( children: [
'简介:${widget.bangumiDetail!.evaluate!}', StatView(
maxLines: 3, view: widget.bangumiDetail!.stat!['views'],
overflow: TextOverflow.ellipsis, size: 'medium',
style: TextStyle( ),
fontSize: 13, const SizedBox(width: 6),
color: t.colorScheme.outline, StatDanMu(
danmu: widget.bangumiDetail!.stat!['danmakus'],
size: 'medium',
),
],
), ),
), const SizedBox(height: 10),
], Text(
'简介:${widget.bangumiDetail!.evaluate!}',
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
color: t.colorScheme.outline,
),
),
],
),
), ),
), ),
), ),
@ -426,3 +387,97 @@ class _BangumiInfoState extends State<BangumiInfo> {
}); });
} }
} }
// 追番状态
class BangumiStatusWidget extends StatelessWidget {
final BangumiIntroController ctr;
final bool isFollowed;
const BangumiStatusWidget({
Key? key,
required this.ctr,
required this.isFollowed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
void updateFollowStatus() {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return morePanel(context, ctr);
},
);
}
return Obx(
() => SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
return ctr.isFollowed.value
? colorScheme.primaryContainer.withOpacity(0.7)
: colorScheme.outlineVariant.withOpacity(0.7);
}),
),
onPressed:
isFollowed ? () => updateFollowStatus() : () => ctr.bangumiAdd(),
icon: Icon(
ctr.isFollowed.value
? Icons.favorite
: Icons.favorite_border_rounded,
color: ctr.isFollowed.value
? colorScheme.primary
: colorScheme.outline,
size: 22,
),
),
),
);
}
Widget morePanel(BuildContext context, BangumiIntroController ctr) {
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () => Get.back(),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))),
),
),
),
),
...ctr.followStatusList
.map(
(e) => ListTile(
onTap: () => ctr.updateBangumiStatus(e['status']),
selected: ctr.followStatus == e['status'],
title: Text(e['title']),
),
)
.toList(),
const SizedBox(height: 20),
],
),
);
}
}

View File

@ -76,9 +76,14 @@ class _BangumiPageState extends State<BangumiPage>
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Obx(
'最近追番', () => 0 != _bangumidController.total.value
style: Theme.of(context).textTheme.titleMedium, ? Text(
'我的追番(${_bangumidController.total.value})',
style:
Theme.of(context).textTheme.titleMedium,
)
: const SizedBox(),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {

View File

@ -175,59 +175,60 @@ class _BangumiPanelState extends State<BangumiPanel> {
return Container( return Container(
width: 150, width: 150,
margin: const EdgeInsets.only(right: 10), margin: const EdgeInsets.only(right: 10),
child: Material( clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface, color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.hardEdge, ),
child: InkWell( child: InkWell(
onTap: () => changeFucCall(page, i), borderRadius: BorderRadius.circular(8),
child: Padding( onTap: () => changeFucCall(page, i),
padding: const EdgeInsets.symmetric( child: Padding(
vertical: 8, padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 8,
), horizontal: 10,
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: <Widget>[ crossAxisAlignment: CrossAxisAlignment.start,
Row( children: <Widget>[
children: [ Row(
if (isSelected) ...<Widget>[ children: [
Image.asset('assets/images/live.png', if (isSelected) ...<Widget>[
color: primary, height: 12), Image.asset('assets/images/live.png',
const SizedBox(width: 6) color: primary, height: 12),
], const SizedBox(width: 6)
],
Text(
'${i + 1}',
style: TextStyle(
fontSize: 13,
color: isSelected ? primary : onSurface,
),
),
const SizedBox(width: 2),
if (page.badge != null) ...[
const Spacer(),
Text( Text(
'${i + 1}', page.badge!,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 12,
color: isSelected ? primary : onSurface, color: primary,
), ),
), ),
const SizedBox(width: 2), ]
if (page.badge != null) ...[ ],
const Spacer(), ),
Text( const SizedBox(height: 3),
page.badge!, Text(
style: TextStyle( page.longTitle!,
fontSize: 12, maxLines: 1,
color: primary, style: TextStyle(
), fontSize: 13,
), color: isSelected ? primary : onSurface,
]
],
), ),
const SizedBox(height: 3), overflow: TextOverflow.ellipsis,
Text( )
page.longTitle!, ],
maxLines: 1,
style: TextStyle(
fontSize: 13,
color: isSelected ? primary : onSurface,
),
overflow: TextOverflow.ellipsis,
)
],
),
), ),
), ),
), ),

View File

@ -25,6 +25,7 @@ class BangumiCardV extends StatelessWidget {
RoutePush.bangumiPush( RoutePush.bangumiPush(
bangumiItem.seasonId, bangumiItem.seasonId,
null, null,
progressIndex: bangumiItem.progressIndex,
heroTag: heroTag, heroTag: heroTag,
); );
}, },

View File

@ -87,7 +87,9 @@ class _BlackListPageState extends State<BlackListPage> {
itemCount: list.length, itemCount: list.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return ListTile( return ListTile(
onTap: () {}, onTap: () => Get.toNamed(
'/member?mid=${list[index].mid}',
arguments: {'face': list[index].face}),
leading: NetworkImgLayer( leading: NetworkImgLayer(
width: 45, width: 45,
height: 45, height: 45,

View File

@ -96,7 +96,9 @@ class VideoContent extends StatelessWidget {
), ),
const Spacer(), const Spacer(),
Text( Text(
[22, 0].contains(favFolderItem.attr) ? '公开' : '私密', Constants.publicFavFolder.contains(favFolderItem.attr)
? '公开'
: '私密',
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,

View File

@ -14,6 +14,7 @@ class LiveController extends GetxController {
RxList<LiveItemModel> liveList = <LiveItemModel>[].obs; RxList<LiveItemModel> liveList = <LiveItemModel>[].obs;
RxList<LiveFollowingItemModel> liveFollowingList = RxList<LiveFollowingItemModel> liveFollowingList =
<LiveFollowingItemModel>[].obs; <LiveFollowingItemModel>[].obs;
RxInt liveFollowingCount = 0.obs;
bool flag = false; bool flag = false;
OverlayEntry? popupDialog; OverlayEntry? popupDialog;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
@ -27,9 +28,6 @@ class LiveController extends GetxController {
// 获取推荐 // 获取推荐
Future queryLiveList(type) async { Future queryLiveList(type) async {
// if (type == 'init') {
// _currentPage = 1;
// }
var res = await LiveHttp.liveList( var res = await LiveHttp.liveList(
pn: _currentPage, pn: _currentPage,
); );
@ -68,13 +66,14 @@ class LiveController extends GetxController {
// //
Future fetchLiveFollowing() async { Future fetchLiveFollowing() async {
var res = await LiveHttp.liveFollowing(pn: 1, ps: 20); var res = await LiveHttp.liveFollowing(pn: 1, ps: 10);
if (res['status']) { if (res['status']) {
liveFollowingList.value = liveFollowingList.value =
(res['data'].list as List<LiveFollowingItemModel>) (res['data'].list as List<LiveFollowingItemModel>)
.where((LiveFollowingItemModel item) => .where((LiveFollowingItemModel item) =>
item.liveStatus == 1 && item.recordLiveTime == 0) // 根据条件过滤 item.liveStatus == 1 && item.recordLiveTime == 0) // 根据条件过滤
.toList(); .toList();
liveFollowingCount.value = res['data'].liveCount;
} }
return res; return res;
} }

View File

@ -162,34 +162,61 @@ class _LivePageState extends State<LivePage>
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Obx( Row(
() => Text.rich( mainAxisAlignment: MainAxisAlignment.spaceBetween,
TextSpan( children: [
children: [ Obx(
const TextSpan( () => Text.rich(
text: ' 我的关注 ',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
TextSpan( TextSpan(
text: ' ${_liveController.liveFollowingList.length}', children: [
style: TextStyle( const TextSpan(
fontSize: 12, text: ' 我的关注 ',
color: Theme.of(context).colorScheme.primary, style: TextStyle(
), fontWeight: FontWeight.bold,
fontSize: 15,
),
),
TextSpan(
text: ' ${_liveController.liveFollowingCount}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.primary,
),
),
TextSpan(
text: '人正在直播',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
],
), ),
TextSpan( ),
text: '人正在直播', ),
style: TextStyle( InkWell(
fontSize: 12, onTap: () {
Get.toNamed('/liveFollowing');
},
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
child: Row(
children: [
Text(
'查看更多',
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.outline,
),
),
Icon(
Icons.chevron_right,
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
), ],
], ),
), ),
), ],
), ),
FutureBuilder( FutureBuilder(
future: _futureBuilderFuture2, future: _futureBuilderFuture2,
@ -201,8 +228,7 @@ class _LivePageState extends State<LivePage>
Map? data = snapshot.data; Map? data = snapshot.data;
if (data?['status']) { if (data?['status']) {
RxList list = _liveController.liveFollowingList; RxList list = _liveController.liveFollowingList;
// ignore: invalid_use_of_protected_member return LiveFollowingListView(list: list);
return Obx(() => LiveFollowingListView(list: list.value));
} else { } else {
return SizedBox( return SizedBox(
height: 80, height: 80,
@ -230,69 +256,71 @@ class _LivePageState extends State<LivePage>
} }
class LiveFollowingListView extends StatelessWidget { class LiveFollowingListView extends StatelessWidget {
final List list; final RxList list;
const LiveFollowingListView({super.key, required this.list}); const LiveFollowingListView({super.key, required this.list});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return Obx(
height: 100, () => SizedBox(
child: ListView.builder( height: 100,
scrollDirection: Axis.horizontal, child: ListView.builder(
itemBuilder: (context, index) { scrollDirection: Axis.horizontal,
final LiveFollowingItemModel item = list[index]; itemBuilder: (context, index) {
return Padding( final LiveFollowingItemModel item = list[index];
padding: const EdgeInsets.fromLTRB(3, 12, 3, 0), return Padding(
child: Column( padding: const EdgeInsets.fromLTRB(3, 12, 3, 0),
children: [ child: Column(
InkWell( children: [
onTap: () { InkWell(
Get.toNamed( onTap: () {
'/liveRoom?roomid=${item.roomId}', Get.toNamed(
arguments: { '/liveRoom?roomid=${item.roomId}',
'liveItem': item, arguments: {
'heroTag': item.roomId.toString() 'liveItem': item,
}, 'heroTag': item.roomId.toString()
); },
}, );
child: Container( },
width: 54, child: Container(
height: 54, width: 54,
padding: const EdgeInsets.all(2), height: 54,
decoration: BoxDecoration( padding: const EdgeInsets.all(2),
borderRadius: BorderRadius.circular(27), decoration: BoxDecoration(
border: Border.all( borderRadius: BorderRadius.circular(27),
color: Theme.of(context).colorScheme.primary, border: Border.all(
width: 1.5, color: Theme.of(context).colorScheme.primary,
width: 1.5,
),
),
child: NetworkImgLayer(
width: 50,
height: 50,
type: 'avatar',
src: list[index].face,
), ),
), ),
child: NetworkImgLayer( ),
width: 50, const SizedBox(height: 6),
height: 50, SizedBox(
type: 'avatar', width: 62,
src: list[index].face, child: Text(
list[index].uname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12,
),
), ),
), ),
), ],
const SizedBox(height: 6), ),
SizedBox( );
width: 62, },
child: Text( itemCount: list.length,
list[index].uname, ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12,
),
),
),
],
),
);
},
itemCount: list.length,
), ),
); );
} }

View File

@ -2,6 +2,8 @@ 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/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.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/image_save.dart'; import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -9,7 +11,7 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局 // 视频卡片 - 垂直布局
class LiveCardV extends StatelessWidget { class LiveCardV extends StatelessWidget {
final LiveItemModel liveItem; final dynamic liveItem;
final int crossAxisCount; final int crossAxisCount;
const LiveCardV({ const LiveCardV({
@ -64,6 +66,9 @@ class LiveCardV extends StatelessWidget {
), ),
), ),
), ),
if (liveItem is LiveFollowingItemModel &&
liveItem.liveStatus == 1)
const PBadge(top: 8, right: 8, text: '直播中'),
], ],
); );
}), }),
@ -148,7 +153,7 @@ class LiveContent extends StatelessWidget {
} }
class VideoStat extends StatelessWidget { class VideoStat extends StatelessWidget {
final LiveItemModel? liveItem; final dynamic liveItem;
const VideoStat({ const VideoStat({
Key? key, Key? key,
@ -178,25 +183,20 @@ class VideoStat extends StatelessWidget {
liveItem!.areaName!, liveItem!.areaName!,
style: const TextStyle(fontSize: 11, color: Colors.white), style: const TextStyle(fontSize: 11, color: Colors.white),
), ),
Text( if (liveItem is LiveItemModel) ...[
liveItem!.watchedShow!['text_small'], Text(
style: const TextStyle(fontSize: 11, color: Colors.white), liveItem!.watchedShow?['text_small'],
), style: const TextStyle(fontSize: 11, color: Colors.white),
),
],
if (liveItem is LiveFollowingItemModel) ...[
Text(
'${liveItem.textSmall}',
style: const TextStyle(fontSize: 11, color: Colors.white),
),
]
], ],
), ),
// child: RichText(
// maxLines: 1,
// textAlign: TextAlign.justify,
// softWrap: false,
// text: TextSpan(
// style: const TextStyle(fontSize: 11, color: Colors.white),
// children: [
// TextSpan(text: liveItem!.areaName!),
// TextSpan(text: liveItem!.watchedShow!['text_small']),
// ],
// ),
// ),
); );
} }
} }

View File

@ -0,0 +1,50 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/live.dart';
import 'package:pilipala/models/live/follow.dart';
import 'package:pilipala/utils/storage.dart';
class LiveFollowController extends GetxController {
RxInt crossAxisCount = 2.obs;
Box setting = GStrorage.setting;
int _currentPage = 1;
RxInt liveFollowingCount = 0.obs;
RxList<LiveFollowingItemModel> liveFollowingList =
<LiveFollowingItemModel>[].obs;
@override
void onInit() {
super.onInit();
crossAxisCount.value =
setting.get(SettingBoxKey.customRows, defaultValue: 2);
}
Future queryLiveFollowList(type) async {
var res = await LiveHttp.liveFollowing(
pn: _currentPage,
ps: 20,
);
if (res['status']) {
if (type == 'init') {
liveFollowingList.value = res['data'].list;
liveFollowingCount.value = res['data'].liveCount;
} else if (type == 'onLoad') {
liveFollowingList.addAll(res['data'].list);
}
_currentPage += 1;
} else {
SmartDialog.showToast(res['msg']);
}
return res;
}
Future onRefresh() async {
_currentPage = 1;
await queryLiveFollowList('init');
}
void onLoad() async {
queryLiveFollowList('onLoad');
}
}

View File

@ -0,0 +1,4 @@
library live_follow;
export 'view.dart';
export 'controller.dart';

View File

@ -0,0 +1,136 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/video_card_v.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/live/widgets/live_item.dart';
import 'controller.dart';
class LiveFollowPage extends StatefulWidget {
const LiveFollowPage({super.key});
@override
State<LiveFollowPage> createState() => _LiveFollowPageState();
}
class _LiveFollowPageState extends State<LiveFollowPage> {
late Future _futureBuilderFuture;
final ScrollController scrollController = ScrollController();
final LiveFollowController _liveFollowController =
Get.put(LiveFollowController());
@override
void initState() {
super.initState();
_futureBuilderFuture = _liveFollowController.queryLiveFollowList('init');
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'liveFollowList', const Duration(milliseconds: 200), () {
_liveFollowController.onLoad();
});
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0,
scrolledUnderElevation: 0,
titleSpacing: 0,
centerTitle: false,
title: Obx(() => Text(
'${_liveFollowController.liveFollowingCount}人正在直播中',
style: Theme.of(context).textTheme.titleMedium,
)),
),
body: Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(StyleString.imgRadius),
),
child: RefreshIndicator(
onRefresh: () async {
return await _liveFollowController.onRefresh();
},
child: CustomScrollView(
controller: scrollController,
slivers: [
SliverPadding(
padding:
const EdgeInsets.fromLTRB(0, StyleString.safeSpace, 0, 0),
sliver: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data as Map;
if (data['status']) {
return SliverLayoutBuilder(
builder: (context, boxConstraints) {
return Obx(
() => contentGrid(_liveFollowController,
_liveFollowController.liveFollowingList),
);
});
} else {
return HttpError(
errMsg: data['msg'],
fn: () {
setState(() {
_futureBuilderFuture = _liveFollowController
.queryLiveFollowList('init');
});
},
);
}
} else {
return contentGrid(_liveFollowController, []);
}
},
),
),
],
),
),
));
}
Widget contentGrid(ctr, liveList) {
int crossAxisCount = ctr.crossAxisCount.value;
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
mainAxisSpacing: StyleString.safeSpace,
crossAxisSpacing: StyleString.safeSpace,
crossAxisCount: crossAxisCount,
mainAxisExtent:
Get.size.width / crossAxisCount / StyleString.aspectRatio +
MediaQuery.textScalerOf(context).scale(
(crossAxisCount == 1 ? 48 : 68),
),
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return liveList!.isNotEmpty
? LiveCardV(
liveItem: liveList[index],
crossAxisCount: crossAxisCount,
)
: const VideoCardVSkeleton();
},
childCount: liveList!.isNotEmpty ? liveList!.length : 10,
),
);
}
}

View File

@ -122,18 +122,13 @@ class MemberController extends GetxController {
// 合并关注/取关和拉黑逻辑 // 合并关注/取关和拉黑逻辑
Future modifyRelation(String actionType) async { Future modifyRelation(String actionType) async {
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
String contentText; String contentText;
int act; int act;
if (actionType == 'follow') { if (actionType == 'follow') {
contentText = memberInfo.value.isFollowed! ? '确定取消关注UP主?' : '确定关注UP主?'; contentText = memberInfo.value.isFollowed! ? '确定取消关注UP主?' : '确定关注UP主?';
act = memberInfo.value.isFollowed! ? 2 : 1; act = memberInfo.value.isFollowed! ? 2 : 1;
} else if (actionType == 'block') { } else if (actionType == 'block') {
contentText = attribute.value != 128 ? '确定拉黑UP主?' : '确定从黑名单移除UP主'; contentText = attribute.value != 128 ? '确定拉黑UP主?' : '确定从黑名单移除UP主?';
act = attribute.value != 128 ? 5 : 6; act = attribute.value != 128 ? 5 : 6;
} else { } else {
return; return;

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -7,6 +8,7 @@ import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/member/info.dart'; import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'widgets/commen_widget.dart'; import 'widgets/commen_widget.dart';
import 'widgets/conis.dart'; import 'widgets/conis.dart';
@ -154,6 +156,25 @@ class _MemberPageState extends State<MemberPage>
bottom: MediaQuery.of(context).padding.bottom + 20, bottom: MediaQuery.of(context).padding.bottom + 20,
), ),
children: [ children: [
Obx(() {
Rx<MemberInfoModel> memberInfo = _memberController.memberInfo;
return memberInfo.value.silence != null &&
memberInfo.value.silence! == 1
? Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 10, bottom: 10),
color: Theme.of(context).colorScheme.errorContainer,
child: Text(
'该账号封禁中',
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
fontSize: 16,
),
),
)
: const SizedBox();
}),
profileWidget(), profileWidget(),
/// 动态链接 /// 动态链接
@ -318,6 +339,7 @@ class _MemberPageState extends State<MemberPage>
Rx<MemberInfoModel> memberInfo = _memberController.memberInfo; Rx<MemberInfoModel> memberInfo = _memberController.memberInfo;
return Obx( return Obx(
() => Column( () => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ProfilePanel(ctr: _memberController), ProfilePanel(ctr: _memberController),
@ -376,7 +398,7 @@ class _MemberPageState extends State<MemberPage>
.value.vip!.label!['img_label_uri_hans_static'], .value.vip!.label!['img_label_uri_hans_static'],
height: 20, height: 20,
), ),
] ],
], ],
), ),
if (memberInfo.value.official!['title'] != '') ...[ if (memberInfo.value.official!['title'] != '') ...[
@ -393,6 +415,39 @@ class _MemberPageState extends State<MemberPage>
), ),
], ],
const SizedBox(height: 6), const SizedBox(height: 6),
InkWell(
onTap: () {
feedBack();
Clipboard.setData(ClipboardData(
text: memberInfo.value.mid.toString()));
SmartDialog.showToast('uid复制成功');
},
borderRadius: BorderRadius.circular(10),
child: Ink(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
child: SizedBox(
height: 16,
child: Text(
'uid: ${memberInfo.value.mid}',
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
fontSize: 12,
),
),
),
),
),
),
const SizedBox(height: 6),
SelectableText(memberInfo.value.sign ?? ''), SelectableText(memberInfo.value.sign ?? ''),
], ],
), ),

View File

@ -100,13 +100,6 @@ class SystemItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// if (item.content is Map) {
// var res = MessageUtils().extractLinks(item.content['web']);
// print('res: $res');
// } else {
// var res = MessageUtils().extractLinks(item.content);
// print('res: $res');
// }
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(14, 14, 14, 12), padding: const EdgeInsets.fromLTRB(14, 14, 14, 12),
child: Column( child: Column(
@ -156,7 +149,7 @@ class SystemItem extends StatelessWidget {
contentMap['message'].splitMapJoin( contentMap['message'].splitMapJoin(
regExp, regExp,
onMatch: (Match match) { onMatch: (Match match) {
if (!match.group(0)!.startsWith('BV')) { if (match.group(0) != '' && !match.group(0)!.startsWith('BV')) {
spanChilds.add( spanChilds.add(
WidgetSpan( WidgetSpan(
child: Icon(Icons.link, color: colorScheme.primary, size: 16), child: Icon(Icons.link, color: colorScheme.primary, size: 16),

View File

@ -26,7 +26,6 @@ class SSearchController extends GetxController {
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
bool enableHotKey = true; bool enableHotKey = true;
bool enableSearchSuggest = true; bool enableSearchSuggest = true;
late StreamController<bool> clearStream = StreamController<bool>.broadcast();
@override @override
void onInit() { void onInit() {
@ -42,7 +41,6 @@ class SSearchController extends GetxController {
final hint = parameters['hintText']; final hint = parameters['hintText'];
if (hint != null) { if (hint != null) {
hintText = hint; hintText = hint;
searchKeyWord.value = hintText;
} }
} }
historyCacheList = GlobalDataCache().historyCacheList; historyCacheList = GlobalDataCache().historyCacheList;
@ -55,10 +53,8 @@ class SSearchController extends GetxController {
searchKeyWord.value = value; searchKeyWord.value = value;
if (value == '') { if (value == '') {
searchSuggestList.value = []; searchSuggestList.value = [];
clearStream.add(false);
return; return;
} }
clearStream.add(true);
if (enableSearchSuggest) { if (enableSearchSuggest) {
_debouncer.call(() => querySearchSuggest(value)); _debouncer.call(() => querySearchSuggest(value));
} }
@ -68,24 +64,20 @@ class SSearchController extends GetxController {
controller.value.clear(); controller.value.clear();
searchKeyWord.value = ''; searchKeyWord.value = '';
searchSuggestList.value = []; searchSuggestList.value = [];
clearStream.add(false);
} }
// 搜索 // 搜索
void submit() { void submit() {
if (searchKeyWord.value == '') { if (searchKeyWord.value == '' && hintText.isNotEmpty && hintText == '搜索') {
return; return;
} else {
if (searchKeyWord.value == '' && hintText != '搜索') {
searchKeyWord.value = hintText;
controller.value.text = hintText;
}
} }
List arr = historyCacheList.where((e) => e != searchKeyWord.value).toList(); hintText = '搜索';
arr.insert(0, searchKeyWord.value); cacheHistory();
historyCacheList = arr;
historyList.value = historyCacheList;
// 手动刷新
historyList.refresh();
localCache.put('cacheList', historyCacheList);
GlobalDataCache().historyCacheList = historyCacheList;
searchFocusNode.unfocus();
Get.toNamed('/searchResult', parameters: {'keyword': searchKeyWord.value}); Get.toNamed('/searchResult', parameters: {'keyword': searchKeyWord.value});
} }
@ -139,4 +131,15 @@ class SSearchController extends GetxController {
GlobalDataCache().historyCacheList = []; GlobalDataCache().historyCacheList = [];
SmartDialog.showToast('搜索历史已清空'); SmartDialog.showToast('搜索历史已清空');
} }
cacheHistory() {
List arr = historyCacheList.where((e) => e != searchKeyWord.value).toList();
arr.insert(0, searchKeyWord.value);
historyCacheList = arr;
historyList.value = historyCacheList;
historyList.refresh();
localCache.put('cacheList', historyCacheList);
GlobalDataCache().historyCacheList = historyCacheList;
searchFocusNode.unfocus();
}
} }

View File

@ -63,24 +63,35 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
focusNode: _searchController.searchFocusNode, focusNode: _searchController.searchFocusNode,
controller: _searchController.controller.value, controller: _searchController.controller.value,
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
onChanged: (value) => _searchController.onChange(value), onChanged: _searchController.onChange,
decoration: InputDecoration( decoration: InputDecoration(
hintText: _searchController.hintText, hintText: _searchController.hintText,
border: InputBorder.none, border: InputBorder.none,
suffixIcon: StreamBuilder( suffix: Obx(() {
initialData: false, RxString searchKeyWord = _searchController.searchKeyWord;
stream: _searchController.clearStream.stream, if (searchKeyWord.value.isEmpty) {
builder: (_, snapshot) { return const SizedBox();
if (snapshot.data == true) { }
return IconButton( return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (RegExp(r'^\d+$').hasMatch(searchKeyWord.value))
IconButton(
tooltip: '直达up主页',
icon: const Icon(Icons.person_outline, size: 22),
onPressed: () {
_searchController.cacheHistory();
Get.toNamed('/member?mid=${searchKeyWord.value}',
arguments: {'face': null});
},
),
IconButton(
icon: const Icon(Icons.clear, size: 22), icon: const Icon(Icons.clear, size: 22),
onPressed: () => _searchController.onClear(), onPressed: () => _searchController.onClear(),
); ),
} else { ],
return const SizedBox(); );
} }),
},
),
), ),
onSubmitted: (String value) => _searchController.submit(), onSubmitted: (String value) => _searchController.submit(),
), ),

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/search/result.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -31,7 +33,7 @@ class SearchPanelController extends GetxController {
tids: searchType!.type != 'video' ? null : tids.value, tids: searchType!.type != 'video' ? null : tids.value,
); );
if (result['status']) { if (result['status']) {
if (type == 'onRefresh') { if (type == 'init') {
resultList.value = result['data'].list ?? []; resultList.value = result['data'].list ?? [];
} else { } else {
resultList.addAll(result['data'].list ?? []); resultList.addAll(result['data'].list ?? []);
@ -39,12 +41,36 @@ class SearchPanelController extends GetxController {
page.value++; page.value++;
onPushDetail(keyword, resultList); onPushDetail(keyword, resultList);
} }
if (RegExp(r'^\d+$').hasMatch(keyword!) &&
searchType == SearchType.bili_user) {
var res = await MemberHttp.memberInfo(mid: int.parse(keyword!));
if (res['status']) {
try {
final user = SearchUserItemModel(
mid: res['data'].mid,
uname: res['data'].name,
upic: res['data'].face,
level: res['data'].level,
fans: null,
videos: null,
officialVerify: res['data'].official,
);
if (resultList.isEmpty) {
resultList = [user].obs;
} else {
resultList.insert(0, user);
}
} catch (err) {
debugPrint('搜索用户信息失败: $err');
}
}
}
return result; return result;
} }
Future onRefresh() async { Future onRefresh() async {
page.value = 1; page.value = 1;
await onSearch(type: 'onRefresh'); await onSearch();
} }
// 返回顶部并刷新 // 返回顶部并刷新

View File

@ -4,6 +4,7 @@ import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/media_bangumi.dart'; import 'package:pilipala/common/skeleton/media_bangumi.dart';
import 'package:pilipala/common/skeleton/user_list.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
@ -81,11 +82,11 @@ class _SearchPanelState extends State<SearchPanel>
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data != null) { Map? data = snapshot.data;
Map data = snapshot.data; if (data != null && data['status']) {
var ctr = _searchPanelController; var ctr = _searchPanelController;
RxList list = ctr.resultList; RxList list = ctr.resultList;
if (data['status']) { if (list.isNotEmpty) {
return Obx(() { return Obx(() {
switch (widget.searchType) { switch (widget.searchType) {
case SearchType.video: case SearchType.video:
@ -110,21 +111,18 @@ class _SearchPanelState extends State<SearchPanel>
}); });
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: '没有数据',
fn: () { isShowBtn: false,
setState(() { fn: () => {},
_searchPanelController.onSearch();
});
},
isInSliver: false, isInSliver: false,
); );
} }
} else { } else {
return HttpError( return HttpError(
errMsg: '没有相关数据', errMsg: data?['msg'] ?? '请求异常',
fn: () { fn: () {
setState(() { setState(() {
_searchPanelController.onSearch(); _futureBuilderFuture = _searchPanelController.onRefresh();
}); });
}, },
isInSliver: false, isInSliver: false,
@ -143,7 +141,7 @@ class _SearchPanelState extends State<SearchPanel>
case SearchType.media_bangumi: case SearchType.media_bangumi:
return const MediaBangumiSkeleton(); return const MediaBangumiSkeleton();
case SearchType.bili_user: case SearchType.bili_user:
return const VideoCardHSkeleton(); return const UserListSkeleton();
case SearchType.live_room: case SearchType.live_room:
return const VideoCardHSkeleton(); return const VideoCardHSkeleton();
default: default:

View File

@ -1,12 +1,7 @@
import 'package:flutter/material.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/constants.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.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';
@ -30,8 +25,8 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
// }); // });
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.symmetric(
StyleString.safeSpace, 7, StyleString.safeSpace, 7), horizontal: StyleString.safeSpace, vertical: 7),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -12,15 +13,16 @@ Widget searchUserPanel(BuildContext context, ctr, list) {
controller: ctr!.scrollController, controller: ctr!.scrollController,
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
itemCount: list!.length, itemCount: list.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
var i = list![index]; var i = list[index];
String heroTag = Utils.makeHeroTag(i!.mid); String heroTag = Utils.makeHeroTag(i.mid);
return InkWell( return InkWell(
onTap: () => Get.toNamed('/member?mid=${i.mid}', onTap: () => Get.toNamed('/member?mid=${i.mid}',
arguments: {'heroTag': heroTag, 'face': i.upic}), arguments: {'heroTag': heroTag, 'face': i.upic}),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace, vertical: 7),
child: Row( child: Row(
children: [ children: [
Hero( Hero(
@ -41,7 +43,7 @@ Widget searchUserPanel(BuildContext context, ctr, list) {
Row( Row(
children: [ children: [
Text( Text(
i!.uname, i.uname!,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
), ),
@ -53,15 +55,16 @@ Widget searchUserPanel(BuildContext context, ctr, list) {
), ),
], ],
), ),
Row( if (i.fans != null && i.videos != null)
children: [ Row(
Text('粉丝:${i.fans} ', style: style), children: [
Text(' 视频${i.videos}', style: style) Text('粉丝${i.fans} ', style: style),
], Text(' 视频:${i.videos}', style: style)
), ],
if (i.officialVerify['desc'] != '') ),
if (i.officialVerify!['desc'] != '')
Text( Text(
i.officialVerify['desc'], i.officialVerify!['desc'],
style: style, style: style,
), ),
], ],

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/search_panel/index.dart'; import 'package:pilipala/pages/search_panel/index.dart';
import 'controller.dart'; import 'controller.dart';
import 'widget/tab_bar.dart';
class SearchResultPage extends StatefulWidget { class SearchResultPage extends StatefulWidget {
const SearchResultPage({super.key}); const SearchResultPage({super.key});
@ -29,6 +30,17 @@ class _SearchResultPageState extends State<SearchResultPage>
); );
} }
// tab点击事件
void _onTap(int index) {
if (index == _searchResultController.tabIndex) {
Get.find<SearchPanelController>(
tag: SearchType.values[index].type +
_searchResultController.keyword!)
.animateToTop();
}
_searchResultController.tabIndex = index;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -55,50 +67,10 @@ class _SearchResultPageState extends State<SearchResultPage>
body: Column( body: Column(
children: [ children: [
const SizedBox(height: 4), const SizedBox(height: 4),
Container( TabBarWidget(
width: double.infinity, onTap: _onTap,
padding: const EdgeInsets.only(left: 8), tabController: _tabController!,
color: Theme.of(context).colorScheme.surface, searchResultCtr: _searchResultController,
child: Theme(
data: ThemeData(
splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明
),
child: Obx(
() => (TabBar(
controller: _tabController,
tabs: [
for (var i in _searchResultController.searchTabs)
Tab(text: "${i['label']} ${i['count'] ?? ''}")
],
isScrollable: true,
indicatorWeight: 0,
indicatorPadding:
const EdgeInsets.symmetric(horizontal: 3, vertical: 8),
indicator: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
indicatorSize: TabBarIndicatorSize.tab,
labelColor:
Theme.of(context).colorScheme.onSecondaryContainer,
labelStyle: const TextStyle(fontSize: 13),
dividerColor: Colors.transparent,
unselectedLabelColor: Theme.of(context).colorScheme.outline,
tabAlignment: TabAlignment.start,
onTap: (index) {
if (index == _searchResultController.tabIndex) {
Get.find<SearchPanelController>(
tag: SearchType.values[index].type +
_searchResultController.keyword!)
.animateToTop();
}
_searchResultController.tabIndex = index;
},
)),
),
),
), ),
Expanded( Expanded(
child: TabBarView( child: TabBarView(

View File

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/pages/search_result/index.dart';
class TabBarWidget extends StatelessWidget {
final Function(int) onTap;
final TabController tabController;
final SearchResultController searchResultCtr;
const TabBarWidget({
required this.onTap,
required this.tabController,
required this.searchResultCtr,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
Color transparent = Colors.transparent;
return Container(
width: double.infinity,
padding: const EdgeInsets.only(left: 8),
color: colorScheme.surface,
child: Theme(
data: ThemeData(splashColor: transparent, highlightColor: transparent),
child: Obx(
() => TabBar(
controller: tabController,
tabs: [
for (var i in searchResultCtr.searchTabs)
Tab(text: "${i['label']} ${i['count'] ?? ''}"),
],
isScrollable: true,
indicatorPadding:
const EdgeInsets.symmetric(horizontal: 3, vertical: 8),
indicator: BoxDecoration(
color: colorScheme.secondaryContainer,
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
indicatorSize: TabBarIndicatorSize.tab,
labelColor: colorScheme.onSecondaryContainer,
labelStyle: const TextStyle(fontSize: 13),
dividerColor: transparent,
unselectedLabelColor: colorScheme.outline,
tabAlignment: TabAlignment.start,
onTap: onTap,
),
),
),
);
}
}

View File

@ -90,52 +90,55 @@ class VideoContent extends StatelessWidget {
return Expanded( return Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0), padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column( child: Stack(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Column(
subFolderItem.title!, crossAxisAlignment: CrossAxisAlignment.start,
textAlign: TextAlign.start, children: [
style: const TextStyle( Text(
fontWeight: FontWeight.w500, subFolderItem.title!,
letterSpacing: 0.3, textAlign: TextAlign.start,
), maxLines: 3,
style: const TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
),
const SizedBox(height: 2),
Text(
'合集 UP主${subFolderItem.upper!.name!}',
textAlign: TextAlign.start,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(height: 2),
Text(
'${subFolderItem.mediaCount}个视频',
textAlign: TextAlign.start,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
],
), ),
const SizedBox(height: 2),
Text(
'合集 UP主${subFolderItem.upper!.name!}',
textAlign: TextAlign.start,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(height: 2),
Text(
'${subFolderItem.mediaCount}个视频',
textAlign: TextAlign.start,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
const Spacer(),
isOwner isOwner
? Row( ? Positioned(
mainAxisAlignment: MainAxisAlignment.end, right: 0,
children: [ bottom: -4,
IconButton( child: IconButton(
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),
onPressed: () => cancelSub?.call(subFolderItem), onPressed: () => cancelSub?.call(subFolderItem),
icon: Icon( icon: Icon(
Icons.clear_outlined, Icons.clear_outlined,
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
size: 18, size: 18,
), ),
) ),
],
) )
: const SizedBox() : const SizedBox()
], ],

View File

@ -338,73 +338,49 @@ class VideoIntroController extends GetxController {
return; return;
} }
final int currentStatus = followStatus['attribute']; final int currentStatus = followStatus['attribute'];
int actionStatus = 0; if (currentStatus == 128) {
switch (currentStatus) { modifyRelation('block', currentStatus);
case 0: } else {
actionStatus = 1; modifyRelation('follow', currentStatus);
break;
case 2:
actionStatus = 2;
break;
default:
actionStatus = 0;
break;
} }
SmartDialog.show( }
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide, // 操作用户关系
Future modifyRelation(String actionType, int currentStatus) async {
final int mid = videoDetail.value.owner!.mid!;
String contentText;
int act;
if (actionType == 'follow') {
contentText = currentStatus != 0 ? '确定取消关注UP主?' : '确定关注UP主?';
act = currentStatus != 0 ? 2 : 1;
} else if (actionType == 'block') {
contentText = '确定从黑名单移除UP主?';
act = 6;
} else {
return;
}
showDialog(
context: Get.context!,
builder: (BuildContext context) { builder: (BuildContext context) {
final Color outline = Theme.of(Get.context!).colorScheme.outline;
return AlertDialog( return AlertDialog(
title: const Text('提示'), title: const Text('提示'),
content: Text(currentStatus == 0 ? '关注UP主?' : '取消关注UP主?'), content: Text(contentText),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => SmartDialog.dismiss(), onPressed: Navigator.of(context).pop,
child: Text( child: Text('点错了', style: TextStyle(color: outline)),
'点错了',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () => modifyRelationFetch(
var result = await VideoHttp.relationMod( context,
mid: videoDetail.value.owner!.mid!, mid,
act: actionStatus, act,
reSrc: 14, currentStatus,
); actionType,
if (result['status']) { ),
switch (currentStatus) { child: const Text('确定'),
case 0:
actionStatus = 2;
break;
case 2:
actionStatus = 0;
break;
default:
actionStatus = 0;
break;
}
followStatus['attribute'] = actionStatus;
followStatus.refresh();
if (actionStatus == 2) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('关注成功'),
duration: const Duration(seconds: 2),
action: SnackBarAction(
label: '设置分组',
onPressed: setFollowGroup,
),
showCloseIcon: true,
),
);
}
}
}
SmartDialog.dismiss();
},
child: const Text('确认'),
) )
], ],
); );
@ -412,6 +388,52 @@ class VideoIntroController extends GetxController {
); );
} }
// 操作用户关系Future
Future modifyRelationFetch(
BuildContext context,
mid,
act,
currentStatus,
actionType,
) async {
var res = await VideoHttp.relationMod(mid: mid, act: act, reSrc: 11);
if (context.mounted) {
Navigator.of(context).pop();
}
if (res['status']) {
if (actionType == 'follow') {
final Map<int, int> statusMap = {
0: 2,
2: 0,
};
late int actionStatus;
actionStatus = statusMap[currentStatus] ?? 0;
followStatus['attribute'] = actionStatus;
if (currentStatus == 0 && Get.context!.mounted) {
ScaffoldMessenger.of(Get.context!).showSnackBar(
SnackBar(
content: const Text('关注成功'),
duration: const Duration(seconds: 2),
action: SnackBarAction(
label: '设置分组',
onPressed: setFollowGroup,
),
showCloseIcon: true,
),
);
} else {
SmartDialog.showToast('取消关注成功');
}
} else if (actionType == 'block') {
followStatus['attribute'] = 0;
SmartDialog.showToast('取消拉黑成功');
}
followStatus.refresh();
} else {
SmartDialog.showToast(res['msg']);
}
}
// 修改分P或番剧分集 // 修改分P或番剧分集
Future changeSeasonOrbangu( Future changeSeasonOrbangu(
String bvid, String bvid,

View File

@ -470,8 +470,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
const Spacer(), const Spacer(),
Obx( Obx(
() { () {
final bool isFollowed = final int attr =
videoIntroController.followStatus['attribute'] != 0; videoIntroController.followStatus['attribute'] ?? 0;
return videoIntroController.followStatus.isEmpty return videoIntroController.followStatus.isEmpty
? const SizedBox() ? const SizedBox()
: SizedBox( : SizedBox(
@ -484,15 +484,19 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
left: 8, left: 8,
right: 8, right: 8,
), ),
foregroundColor: isFollowed foregroundColor: attr != 0
? outline ? outline
: t.colorScheme.onPrimary, : t.colorScheme.onPrimary,
backgroundColor: isFollowed backgroundColor: attr != 0
? t.colorScheme.onInverseSurface ? t.colorScheme.onInverseSurface
: t.colorScheme.primary, // 设置按钮背景色 : t.colorScheme.primary, // 设置按钮背景色
), ),
child: Text( child: Text(
isFollowed ? '已关注' : '关注', attr == 128
? '已拉黑'
: attr != 0
? '已关注'
: '关注',
style: TextStyle( style: TextStyle(
fontSize: fontSize:
t.textTheme.labelMedium!.fontSize, t.textTheme.labelMedium!.fontSize,

View File

@ -1,6 +1,7 @@
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/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -66,16 +67,14 @@ class _FavPanelState extends State<FavPanel> {
onTap: () => onTap: () =>
widget.ctr!.onChoose(item.favState != 1, index), widget.ctr!.onChoose(item.favState != 1, index),
dense: true, dense: true,
leading: Icon([22, 0].contains(item.attr) leading: Icon(
? Icons.lock_outline Constants.publicFavFolder.contains(item.attr)
: Icons.folder_outlined), ? Icons.folder_outlined
: Icons.lock_outline),
minLeadingWidth: 0, minLeadingWidth: 0,
title: Text(item.title!), title: Text(item.title!),
subtitle: Text( subtitle: Text(
'${item.mediaCount}个内容 - ${[ '${item.mediaCount}个内容 - ${Constants.publicFavFolder.contains(item.attr) ? '公开' : '私密'}',
22,
0
].contains(item.attr) ? '公开' : '私密'}',
), ),
trailing: Transform.scale( trailing: Transform.scale(
scale: 0.9, scale: 0.9,

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.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/http/constants.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -15,6 +16,10 @@ class IntroDetail extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
TextStyle textStyle = TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.primary,
);
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: Column( child: Column(
@ -29,12 +34,7 @@ class IntroDetail extends StatelessWidget {
Clipboard.setData(ClipboardData(text: videoDetail!.bvid!)); Clipboard.setData(ClipboardData(text: videoDetail!.bvid!));
SmartDialog.showToast('已复制'); SmartDialog.showToast('已复制');
}, },
child: Text( child: Text(videoDetail!.bvid!, style: textStyle),
videoDetail!.bvid!,
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.primary),
),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
GestureDetector( GestureDetector(
@ -44,12 +44,18 @@ class IntroDetail extends StatelessWidget {
ClipboardData(text: videoDetail!.aid!.toString())); ClipboardData(text: videoDetail!.aid!.toString()));
SmartDialog.showToast('已复制'); SmartDialog.showToast('已复制');
}, },
child: Text( child: Text(videoDetail!.aid!.toString(), style: textStyle),
videoDetail!.aid!.toString(), ),
style: TextStyle( const SizedBox(width: 10),
fontSize: 13, GestureDetector(
color: Theme.of(context).colorScheme.primary), onTap: () {
), feedBack();
String videoUrl =
'${HttpString.baseUrl}/video/${videoDetail!.bvid!}';
Clipboard.setData(ClipboardData(text: videoUrl));
SmartDialog.showToast('已复制视频链接');
},
child: Text('复制链接', style: textStyle),
) )
], ],
), ),

View File

@ -45,7 +45,7 @@ class ReplyItem extends StatelessWidget {
final bool? showReplyRow; final bool? showReplyRow;
final Function? replyReply; final Function? replyReply;
final ReplyType? replyType; final ReplyType? replyType;
final bool? replySave; final bool replySave;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -55,14 +55,14 @@ class ReplyItem extends StatelessWidget {
child: InkWell( child: InkWell(
// 点击整个评论区 评论详情/回复 // 点击整个评论区 评论详情/回复
onTap: () { onTap: () {
if (replySave!) { if (replySave) {
return; return;
} }
feedBack(); feedBack();
replyReply?.call(replyItem, null, replyItem!.rcount! > 0); replyReply?.call(replyItem, null, replyItem!.rcount! > 0);
}, },
onLongPress: () { onLongPress: () {
if (replySave!) { if (replySave) {
return; return;
} }
feedBack(); feedBack();
@ -235,53 +235,32 @@ class ReplyItem extends StatelessWidget {
// title // title
Container( Container(
margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4), margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4),
child: LayoutBuilder( child: !replySave
builder: (BuildContext context, BoxConstraints boxConstraints) { ? LayoutBuilder(builder:
String text = replyItem?.content?.message ?? ''; (BuildContext context, BoxConstraints boxConstraints) {
bool didExceedMaxLines = false; String text = replyItem?.content?.message ?? '';
final double maxWidth = boxConstraints.maxWidth; bool didExceedMaxLines = false;
TextPainter? textPainter; final double maxWidth = boxConstraints.maxWidth;
final int maxLines = TextPainter? textPainter;
replyItem!.content!.isText! && replyLevel == '1' ? 6 : 999; final int maxLines =
try { replyItem!.content!.isText! && replyLevel == '1'
textPainter = TextPainter( ? 6
text: TextSpan(text: text), : 999;
maxLines: maxLines, try {
textDirection: Directionality.of(context), textPainter = TextPainter(
); text: TextSpan(text: text),
textPainter.layout(maxWidth: maxWidth); maxLines: maxLines,
didExceedMaxLines = textPainter.didExceedMaxLines; textDirection: Directionality.of(context),
} catch (e) { );
debugPrint('Error while measuring text: $e'); textPainter.layout(maxWidth: maxWidth);
didExceedMaxLines = false; didExceedMaxLines = textPainter.didExceedMaxLines;
} } catch (e) {
return Text.rich( debugPrint('Error while measuring text: $e');
style: const TextStyle(height: 1.75), didExceedMaxLines = false;
TextSpan( }
children: [ return replyContent(context, didExceedMaxLines, textPainter);
if (replyItem!.isTop!) })
const WidgetSpan( : replyContent(context, false, null),
alignment: PlaceholderAlignment.top,
child: PBadge(
text: 'TOP',
size: 'small',
stack: 'normal',
type: 'line',
fs: 9,
),
),
buildContent(
context,
replyItem!,
replyReply,
null,
didExceedMaxLines,
textPainter,
),
],
),
);
}),
), ),
// 操作区域 // 操作区域
bottonAction(context, replyItem!.replyControl, replySave), bottonAction(context, replyItem!.replyControl, replySave),
@ -302,6 +281,36 @@ class ReplyItem extends StatelessWidget {
); );
} }
Widget replyContent(
BuildContext context, bool? didExceedMaxLines, TextPainter? textPainter) {
return Text.rich(
style: const TextStyle(height: 1.75),
TextSpan(
children: [
if (replyItem!.isTop!)
const WidgetSpan(
alignment: PlaceholderAlignment.top,
child: PBadge(
text: 'TOP',
size: 'small',
stack: 'normal',
type: 'line',
fs: 9,
),
),
buildContent(
context,
replyItem!,
replyReply,
null,
didExceedMaxLines ?? false,
textPainter,
),
],
),
);
}
// 感谢、回复、复制 // 感谢、回复、复制
Widget bottonAction(BuildContext context, replyControl, replySave) { Widget bottonAction(BuildContext context, replyControl, replySave) {
ColorScheme colorScheme = Theme.of(context).colorScheme; ColorScheme colorScheme = Theme.of(context).colorScheme;

View File

@ -153,6 +153,7 @@ class ChatItem extends StatelessWidget {
jsonDecode(content['content']) jsonDecode(content['content'])
.map((m) => m['text'] as String) .map((m) => m['text'] as String)
.join("\n"), .join("\n"),
textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
letterSpacing: 0.6, letterSpacing: 0.6,
height: 5, height: 5,
@ -359,39 +360,40 @@ class ChatItem extends StatelessWidget {
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
i['field1'], i['field1'],
maxLines: 2, maxLines: 2,
style: TextStyle( style: TextStyle(
letterSpacing: 0.6, letterSpacing: 0.6,
height: 1.5, height: 1.5,
color: textColor(context), color: textColor(context),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
),
), ),
), Text(
Text( i['field2'],
i['field2'], style: TextStyle(
style: TextStyle( letterSpacing: 0.6,
letterSpacing: 0.6, height: 1.5,
height: 1.5, color: textColor(context).withOpacity(0.6),
color: textColor(context).withOpacity(0.6), fontSize: 12,
fontSize: 12, ),
), ),
), Text(
Text( i['field3'],
i['field3'], style: TextStyle(
style: TextStyle( letterSpacing: 0.6,
letterSpacing: 0.6, height: 1.5,
height: 1.5, color: textColor(context).withOpacity(0.6),
color: textColor(context).withOpacity(0.6), fontSize: 12,
fontSize: 12, ),
), ),
), ],
], ),
)), ),
], ],
), ),
), ),

View File

@ -30,6 +30,13 @@ typedef IndexedFocusedWidgetBuilder = Widget Function(
typedef IndexedTagStringBuilder = String Function(int index); typedef IndexedTagStringBuilder = String Function(int index);
// 图片操作类型
enum ImgActionType {
share,
copy,
save,
}
class InteractiveviewerGallery<T> extends StatefulWidget { class InteractiveviewerGallery<T> extends StatefulWidget {
const InteractiveviewerGallery({ const InteractiveviewerGallery({
required this.sources, required this.sources,
@ -39,6 +46,11 @@ class InteractiveviewerGallery<T> extends StatefulWidget {
this.minScale = 1.0, this.minScale = 1.0,
this.onPageChanged, this.onPageChanged,
this.onDismissed, this.onDismissed,
this.actionType = const [
ImgActionType.share,
ImgActionType.copy,
ImgActionType.save
],
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -59,6 +71,8 @@ class InteractiveviewerGallery<T> extends StatefulWidget {
final ValueChanged<int>? onDismissed; final ValueChanged<int>? onDismissed;
final List<ImgActionType> actionType;
@override @override
State<InteractiveviewerGallery> createState() => State<InteractiveviewerGallery> createState() =>
_InteractiveviewerGalleryState(); _InteractiveviewerGalleryState();
@ -247,8 +261,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
onDoubleTapDown: (TapDownDetails details) { onDoubleTapDown: (TapDownDetails details) {
_doubleTapLocalPosition = details.localPosition; _doubleTapLocalPosition = details.localPosition;
}, },
onDoubleTap: onDoubleTap, onDoubleTap: _onDoubleTap,
onLongPress: onLongPress, onLongPress: _onLongPress,
child: widget.itemBuilder != null child: widget.itemBuilder != null
? widget.itemBuilder!( ? widget.itemBuilder!(
context, context,
@ -298,28 +312,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
: const SizedBox(), : const SizedBox(),
PopupMenuButton( PopupMenuButton(
itemBuilder: (context) { itemBuilder: (context) {
return [ return _buildPopupMenuList();
PopupMenuItem(
value: 0,
onTap: () => onShareImg(widget.sources[currentIndex!]),
child: const Text("分享图片"),
),
PopupMenuItem(
value: 1,
onTap: () {
onCopyImg(widget.sources[currentIndex!].toString());
},
child: const Text("复制图片"),
),
PopupMenuItem(
value: 2,
onTap: () {
DownloadUtils.downloadImg(
widget.sources[currentIndex!]);
},
child: const Text("保存图片"),
),
];
}, },
child: const Icon(Icons.more_horiz, color: Colors.white), child: const Icon(Icons.more_horiz, color: Colors.white),
), ),
@ -332,7 +325,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
} }
// 图片分享 // 图片分享
void onShareImg(String imgUrl) async { void _onShareImg(String imgUrl) async {
SmartDialog.showLoading(); SmartDialog.showLoading();
var response = await Dio() var response = await Dio()
.get(imgUrl, options: Options(responseType: ResponseType.bytes)); .get(imgUrl, options: Options(responseType: ResponseType.bytes));
@ -346,7 +339,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
} }
// 复制图片 // 复制图片
void onCopyImg(String imgUrl) { void _onCopyImg(String imgUrl) {
Clipboard.setData( Clipboard.setData(
ClipboardData(text: widget.sources[currentIndex!].toString())) ClipboardData(text: widget.sources[currentIndex!].toString()))
.then((value) { .then((value) {
@ -380,7 +373,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
); );
} }
onDoubleTap() { _onDoubleTap() {
Matrix4 matrix = _transformationController!.value.clone(); Matrix4 matrix = _transformationController!.value.clone();
double currentScale = matrix.row0.x; double currentScale = matrix.row0.x;
@ -427,7 +420,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
.whenComplete(() => _onScaleChanged(targetScale)); .whenComplete(() => _onScaleChanged(targetScale));
} }
onLongPress() { _onLongPress() {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
useRootNavigator: true, useRootNavigator: true,
@ -456,31 +449,81 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
), ),
), ),
), ),
ListTile( ..._buildListTitles(),
onTap: () {
onShareImg(widget.sources[currentIndex!]);
Navigator.of(context).pop();
},
title: const Text('分享图片'),
),
ListTile(
onTap: () {
onCopyImg(widget.sources[currentIndex!].toString());
Navigator.of(context).pop();
},
title: const Text('复制图片'),
),
ListTile(
onTap: () {
DownloadUtils.downloadImg(widget.sources[currentIndex!]);
Navigator.of(context).pop();
},
title: const Text('保存图片'),
),
], ],
), ),
); );
}, },
); );
} }
List<PopupMenuEntry> _buildPopupMenuList() {
List<PopupMenuItem> items = [];
for (var i in widget.actionType) {
switch (i) {
case ImgActionType.share:
items.add(PopupMenuItem(
value: 0,
onTap: () => _onShareImg(widget.sources[currentIndex!]),
child: const Text("分享图片"),
));
break;
case ImgActionType.copy:
items.add(PopupMenuItem(
value: 1,
onTap: () {
_onCopyImg(widget.sources[currentIndex!].toString());
},
child: const Text("复制图片"),
));
break;
case ImgActionType.save:
items.add(PopupMenuItem(
value: 2,
onTap: () {
DownloadUtils.downloadImg(widget.sources[currentIndex!]);
},
child: const Text("保存图片"),
));
break;
}
}
return items;
}
List<Widget> _buildListTitles() {
List<Widget> items = [];
for (var i in widget.actionType) {
switch (i) {
case ImgActionType.share:
items.add(ListTile(
onTap: () {
_onShareImg(widget.sources[currentIndex!]);
Navigator.of(context).pop();
},
title: const Text('分享图片'),
));
break;
case ImgActionType.copy:
items.add(ListTile(
onTap: () {
_onCopyImg(widget.sources[currentIndex!].toString());
Navigator.of(context).pop();
},
title: const Text('复制图片'),
));
break;
case ImgActionType.save:
items.add(ListTile(
onTap: () {
DownloadUtils.downloadImg(widget.sources[currentIndex!]);
Navigator.of(context).pop();
},
title: const Text('保存图片'),
));
break;
}
}
return items;
}
} }

View File

@ -5,6 +5,7 @@ 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/fav_edit/index.dart';
import 'package:pilipala/pages/follow_search/view.dart'; import 'package:pilipala/pages/follow_search/view.dart';
import 'package:pilipala/pages/live_follow/index.dart';
import 'package:pilipala/pages/member_article/index.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';
@ -199,6 +200,8 @@ class Routes {
name: '/memberArticle', page: () => const MemberArticlePage()), name: '/memberArticle', page: () => const MemberArticlePage()),
// 用户信息编辑 // 用户信息编辑
CustomGetPage(name: '/mineEdit', page: () => const MineEditPage()), CustomGetPage(name: '/mineEdit', page: () => const MineEditPage()),
// 关注的直播up
CustomGetPage(name: '/liveFollowing', page: () => const LiveFollowPage()),
]; ];
} }

View File

@ -212,9 +212,9 @@ class PiliSchame {
} }
} }
static Future<void> biliScheme(SchemeEntity value) async { static Future<void> biliScheme(Uri value) async {
final String host = value.host!; final String host = value.host;
final String path = value.path!; final String path = value.path;
switch (host) { switch (host) {
case 'root': case 'root':
Navigator.popUntil( Navigator.popUntil(
@ -301,7 +301,7 @@ class PiliSchame {
break; break;
default: default:
SmartDialog.showToast('未匹配地址,请联系开发者'); SmartDialog.showToast('未匹配地址,请联系开发者');
Clipboard.setData(ClipboardData(text: value.toJson().toString())); Clipboard.setData(ClipboardData(text: value.toString()));
break; break;
} }
} }

View File

@ -8,7 +8,7 @@ import 'package:pilipala/utils/utils.dart';
class RoutePush { class RoutePush {
// 番剧跳转 // 番剧跳转
static Future<void> bangumiPush(int? seasonId, int? epId, static Future<void> bangumiPush(int? seasonId, int? epId,
{String? heroTag}) async { {String? heroTag, int? progressIndex}) async {
SmartDialog.showLoading<dynamic>(msg: '获取中...'); SmartDialog.showLoading<dynamic>(msg: '获取中...');
try { try {
var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId); var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
@ -19,7 +19,10 @@ class RoutePush {
return; return;
} }
final BangumiInfoModel bangumiDetail = result['data']; final BangumiInfoModel bangumiDetail = result['data'];
final EpisodeItem episode = bangumiDetail.episodes!.first; EpisodeItem episode = bangumiDetail.episodes!.first;
if (progressIndex != null && progressIndex >= 1) {
episode = bangumiDetail.episodes![progressIndex - 1];
}
final int epId = episode.id!; final int epId = episode.id!;
final int cid = episode.cid!; final int cid = episode.cid!;
final String bvid = episode.bvid!; final String bvid = episode.bvid!;
@ -31,7 +34,7 @@ class RoutePush {
}; };
arguments['heroTag'] = heroTag ?? Utils.makeHeroTag(cid); arguments['heroTag'] = heroTag ?? Utils.makeHeroTag(cid);
Get.toNamed( Get.toNamed(
'/video?bvid=$bvid&cid=$cid&epId=$epId', '/video?bvid=$bvid&cid=$cid&epId=$epId&seasonId=$seasonId',
arguments: arguments, arguments: arguments,
); );
} else { } else {

View File

@ -218,6 +218,7 @@ flutter:
- assets/images/logo/ - assets/images/logo/
- assets/images/live/ - assets/images/live/
- assets/images/video/ - assets/images/video/
- assets/images/pay/
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware # https://flutter.dev/assets-and-images/#resolution-aware