diff --git a/assets/images/pay/alipay.jpg b/assets/images/pay/alipay.jpg new file mode 100644 index 00000000..1c1fc4c6 Binary files /dev/null and b/assets/images/pay/alipay.jpg differ diff --git a/assets/images/pay/wechat.png b/assets/images/pay/wechat.png new file mode 100644 index 00000000..3aa3a6a2 Binary files /dev/null and b/assets/images/pay/wechat.png differ diff --git a/lib/common/constants.dart b/lib/common/constants.dart index 0607206c..dda54361 100644 --- a/lib/common/constants.dart +++ b/lib/common/constants.dart @@ -15,4 +15,5 @@ class Constants { // 59b43e04ad6965f34319062b478f83dd TV端 static const String appSec = '59b43e04ad6965f34319062b478f83dd'; static const String thirdSign = '04224646d1fea004e79606d3b038c84a'; + static const List publicFavFolder = [0, 2, 22]; } diff --git a/lib/common/pages_bottom_sheet.dart b/lib/common/pages_bottom_sheet.dart index d7eea2ca..afc74bd2 100644 --- a/lib/common/pages_bottom_sheet.dart +++ b/lib/common/pages_bottom_sheet.dart @@ -430,7 +430,7 @@ class EpisodeGridItem extends StatelessWidget { decoration: BoxDecoration( color: isCurrentIndex ? colorScheme.primaryContainer.withOpacity(0.6) - : colorScheme.secondaryContainer.withOpacity(0.4), + : colorScheme.onInverseSurface.withOpacity(0.6), borderRadius: BorderRadius.circular(8), border: Border.all( color: isCurrentIndex diff --git a/lib/common/skeleton/media_bangumi.dart b/lib/common/skeleton/media_bangumi.dart index cf589254..98282cf0 100644 --- a/lib/common/skeleton/media_bangumi.dart +++ b/lib/common/skeleton/media_bangumi.dart @@ -3,14 +3,9 @@ import 'package:pilipala/common/constants.dart'; import 'skeleton.dart'; -class MediaBangumiSkeleton extends StatefulWidget { +class MediaBangumiSkeleton extends StatelessWidget { const MediaBangumiSkeleton({super.key}); - @override - State createState() => _MediaBangumiSkeletonState(); -} - -class _MediaBangumiSkeletonState extends State { @override Widget build(BuildContext context) { Color bgColor = Theme.of(context).colorScheme.onInverseSurface; @@ -35,25 +30,25 @@ class _MediaBangumiSkeletonState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - color: Theme.of(context).colorScheme.onInverseSurface, + color: bgColor, width: 200, height: 20, margin: const EdgeInsets.only(bottom: 15), ), Container( - color: Theme.of(context).colorScheme.onInverseSurface, + color: bgColor, width: 150, height: 13, margin: const EdgeInsets.only(bottom: 5), ), Container( - color: Theme.of(context).colorScheme.onInverseSurface, + color: bgColor, width: 150, height: 13, margin: const EdgeInsets.only(bottom: 5), ), Container( - color: Theme.of(context).colorScheme.onInverseSurface, + color: bgColor, width: 150, height: 13, ), @@ -64,7 +59,7 @@ class _MediaBangumiSkeletonState extends State { decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(20)), - color: Theme.of(context).colorScheme.onInverseSurface, + color: bgColor, ), ), ], diff --git a/lib/common/skeleton/user_list.dart b/lib/common/skeleton/user_list.dart new file mode 100644 index 00000000..cd9d4eb3 --- /dev/null +++ b/lib/common/skeleton/user_list.dart @@ -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, + ), + ], + ), + ), + ], + )); + } +} diff --git a/lib/common/widgets/http_error.dart b/lib/common/widgets/http_error.dart index 305d43c8..51396c0b 100644 --- a/lib/common/widgets/http_error.dart +++ b/lib/common/widgets/http_error.dart @@ -4,7 +4,7 @@ import 'package:flutter_svg/flutter_svg.dart'; class HttpError extends StatelessWidget { const HttpError({ required this.errMsg, - required this.fn, + this.fn, this.btnText, this.isShowBtn = true, this.isInSliver = true, @@ -23,7 +23,6 @@ class HttpError extends StatelessWidget { final errorContent = SizedBox( height: 400, child: Column( - crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ SvgPicture.asset("assets/images/error.svg", height: 200), @@ -50,7 +49,7 @@ class HttpError extends StatelessWidget { if (isInSliver) { return SliverToBoxAdapter(child: errorContent); } else { - return Center(child: errorContent); + return Align(alignment: Alignment.topCenter, child: errorContent); } } } diff --git a/lib/http/api.dart b/lib/http/api.dart index 787b036b..2bd83d69 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -301,10 +301,6 @@ class Api { static const String bangumiList = '/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1©right=-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'; @@ -604,4 +600,10 @@ class Api { /// 图片上传 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'; } diff --git a/lib/http/bangumi.dart b/lib/http/bangumi.dart index 91508682..d0c052d6 100644 --- a/lib/http/bangumi.dart +++ b/lib/http/bangumi.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; import '../models/bangumi/list.dart'; import 'index.dart'; +import 'package:html/parser.dart' as html_parser; +import 'package:html/dom.dart' as html_dom; class BangumiHttp { static Future bangumiList({int? page}) async { @@ -18,8 +21,19 @@ class BangumiHttp { } } - static Future bangumiFollow({int? mid}) async { - var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid}); + static Future getRecentBangumi({ + 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) { return { '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']}; + } + } } diff --git a/lib/models/bangumi/list.dart b/lib/models/bangumi/list.dart index fe71bb61..df20fa3b 100644 --- a/lib/models/bangumi/list.dart +++ b/lib/models/bangumi/list.dart @@ -47,6 +47,7 @@ class BangumiListItemModel { this.title, this.titleIcon, this.progress, + this.progressIndex, }); String? badge; @@ -66,8 +67,8 @@ class BangumiListItemModel { String? subTitle; String? title; String? titleIcon; - String? progress; + int? progressIndex; BangumiListItemModel.fromJson(Map json) { badge = json['badge'] == '' ? null : json['badge']; @@ -87,7 +88,9 @@ class BangumiListItemModel { subTitle = json['sub_title']; title = json['title']; titleIcon = json['title_icon']; - progress = json['progress']; + progressIndex = int.parse( + RegExp(r'第(\d+)话').firstMatch(json['progress'] ?? '第1话')?.group(1) ?? + '0'); } } diff --git a/lib/models/live/follow.dart b/lib/models/live/follow.dart index 4a941b8b..411087a5 100644 --- a/lib/models/live/follow.dart +++ b/lib/models/live/follow.dart @@ -63,7 +63,7 @@ class LiveFollowingItemModel { String? roomNews; String? watchIcon; String? textSmall; - String? roomCover; + String? cover; String? pic; int? parentAreaId; int? areaId; @@ -90,7 +90,7 @@ class LiveFollowingItemModel { this.roomNews, this.watchIcon, this.textSmall, - this.roomCover, + this.cover, this.pic, this.parentAreaId, this.areaId, @@ -108,7 +108,8 @@ class LiveFollowingItemModel { isAttention = json['is_attention']; clipNum = json['clipnum']; fansNum = json['fans_num']; - areaName = json['area_name']; + areaName = + json['area_name'] == '' ? json['area_name_v2'] : json['area_name']; areaValue = json['area_value']; tags = json['tags']; recentRecordIdV2 = json['recent_record_id_v2']; @@ -118,7 +119,7 @@ class LiveFollowingItemModel { roomNews = json['room_news']; watchIcon = json['watch_icon']; textSmall = json['text_small']; - roomCover = json['room_cover']; + cover = json['room_cover']; pic = json['room_cover']; parentAreaId = json['parent_area_id']; areaId = json['area_id']; diff --git a/lib/models/member/info.dart b/lib/models/member/info.dart index 83f94c54..602cdcdb 100644 --- a/lib/models/member/info.dart +++ b/lib/models/member/info.dart @@ -8,6 +8,7 @@ class MemberInfoModel { this.level, this.isFollowed, this.topPhoto, + this.silence, this.official, this.vip, this.liveRoom, @@ -21,6 +22,7 @@ class MemberInfoModel { int? level; bool? isFollowed; String? topPhoto; + int? silence; Map? official; Vip? vip; LiveRoom? liveRoom; @@ -34,6 +36,7 @@ class MemberInfoModel { level = json['level']; isFollowed = json['is_followed']; topPhoto = json['top_photo']; + silence = json['silence'] ?? 0; official = json['official']; vip = Vip.fromJson(json['vip']); liveRoom = diff --git a/lib/pages/about/index.dart b/lib/pages/about/index.dart index 9164d4e9..3a8e5a0a 100644 --- a/lib/pages/about/index.dart +++ b/lib/pages/about/index.dart @@ -5,6 +5,7 @@ import 'package:get/get.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:pilipala/http/index.dart'; import 'package:pilipala/models/github/latest.dart'; +import 'package:pilipala/plugin/pl_gallery/index.dart'; import 'package:pilipala/utils/utils.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../utils/cache_manage.dart'; @@ -124,7 +125,7 @@ class _AboutPageState extends State { onTap: () => _aboutController.webSiteUrl(), title: const Text('访问官网'), trailing: Text( - 'https://pilipalanet.mysxl.cn', + 'https://pilipala.life', style: subTitleStyle, ), ), @@ -168,7 +169,7 @@ class _AboutPageState extends State { onTap: () => _aboutController.tgChanel(), title: const Text('TG频道'), trailing: Text( - 'https://t.me/+lm_oOVmF0RJiODk1', + 'https://t.me/+1DFtqS6usUM5MDNl', style: subTitleStyle, ), ), @@ -321,29 +322,35 @@ class AboutController extends GetxController { // tg频道 tgChanel() { Clipboard.setData( - const ClipboardData(text: 'https://t.me/+lm_oOVmF0RJiODk1'), + const ClipboardData(text: 'https://t.me/+1DFtqS6usUM5MDNl'), ); SmartDialog.showToast( '已复制,即将在浏览器打开', displayTime: const Duration(milliseconds: 500), ).then( (value) => launchUrl( - Uri.parse('https://t.me/+lm_oOVmF0RJiODk1'), + Uri.parse('https://t.me/+1DFtqS6usUM5MDNl'), mode: LaunchMode.externalApplication, ), ); } aPay() { - try { - launchUrl( - Uri.parse( - 'alipayqr://platformapi/startapp?saId=10000007&qrcode=https://qr.alipay.com/fkx14623ddwl1ping3ddd73'), - mode: LaunchMode.externalApplication, - ); - } catch (e) { - print(e); - } + const List sources = [ + 'assets/images/pay/wechat.png', + 'assets/images/pay/alipay.jpg' + ]; + Navigator.of(Get.context!).push( + HeroDialogRoute( + builder: (BuildContext context) => InteractiveviewerGallery( + sources: sources, + initIndex: 0, + itemBuilder: (context, index, isFocus, enablePageView) => + Image.asset(sources[index]), + actionType: const [ImgActionType.save], + ), + ), + ); } // 官网 diff --git a/lib/pages/bangumi/controller.dart b/lib/pages/bangumi/controller.dart index e5748d6c..29dd15d1 100644 --- a/lib/pages/bangumi/controller.dart +++ b/lib/pages/bangumi/controller.dart @@ -9,6 +9,7 @@ class BangumiController extends GetxController { final ScrollController scrollController = ScrollController(); RxList bangumiList = [].obs; RxList bangumiFollowList = [].obs; + RxInt total = 0.obs; int _currentPage = 1; bool isLoadingMore = true; Box userInfoCache = GStrorage.userInfo; @@ -54,9 +55,10 @@ class BangumiController extends GetxController { if (userInfo == null) { return; } - var result = await BangumiHttp.bangumiFollow(mid: userInfo.mid); + var result = await BangumiHttp.getRecentBangumi(mid: userInfo.mid); if (result['status']) { bangumiFollowList.value = result['data'].list; + total.value = result['data'].total; } else {} return result; } diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index cf428c28..dc173a57 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/http/bangumi.dart'; import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/video.dart'; @@ -52,28 +53,34 @@ class BangumiIntroController extends GetxController { Rx favFolderData = FavFolderData().obs; List addMediaIdsNew = []; List delMediaIdsNew = []; - // 关注状态 默认未关注 - RxMap followStatus = {}.obs; + // 追番状态 1想看 2在看 3已看 + RxBool isFollowed = false.obs; + RxInt followStatus = 1.obs; int _tempThemeValue = -1; var userInfo; PersistentBottomSheetController? bottomSheetController; + List> followStatusList = [ + {'title': '标记为 「想看」', 'status': 1}, + {'title': '标记为 「在看」', 'status': 2}, + {'title': '标记为 「已看」', 'status': 3}, + {'title': '取消追番', 'status': -1}, + ]; @override void onInit() { super.onInit(); userInfo = userInfoCache.get('userInfoCache'); userLogin = userInfo != null; + if (userLogin && seasonId != null) { + bangumiStatus(); + } } // 获取番剧简介&选集 Future queryBangumiIntro() async { if (userLogin) { - // 获取点赞状态 - queryHasLikeVideo(); - // 获取投币状态 - queryHasCoinVideo(); - // 获取收藏状态 - queryHasFavVideo(); + // 获取点赞投币收藏状态 + bangumiActionStatus(); } var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId); if (result['status']) { @@ -83,26 +90,15 @@ class BangumiIntroController extends GetxController { return result; } - // 获取点赞状态 - Future queryHasLikeVideo() async { - var result = await VideoHttp.hasLikeVideo(bvid: bvid); - // 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)); + // 获取番剧点赞投币收藏状态 + Future bangumiActionStatus() async { + var result = await BangumiHttp.bangumiActionStatus(epId: epId!); 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 { - hasFav.value = false; + SmartDialog.showToast(result['msg']); } } @@ -110,7 +106,7 @@ class BangumiIntroController extends GetxController { Future actionLikeVideo() async { var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value); if (result['status']) { - SmartDialog.showToast(!hasLike.value ? '点赞成功 👍' : '取消赞'); + SmartDialog.showToast(!hasLike.value ? '点赞成功' : '取消赞'); hasLike.value = !hasLike.value; bangumiDetail.value.stat!['likes'] = bangumiDetail.value.stat!['likes'] + (!hasLike.value ? 1 : -1); @@ -147,7 +143,7 @@ class BangumiIntroController extends GetxController { var res = await VideoHttp.coinVideo( bvid: bvid, multiply: _tempThemeValue); if (res['status']) { - SmartDialog.showToast('投币成功 👏'); + SmartDialog.showToast('投币成功'); hasCoin.value = true; bangumiDetail.value.stat!['coins'] = bangumiDetail.value.stat!['coins'] + @@ -185,9 +181,11 @@ class BangumiIntroController extends GetxController { addMediaIdsNew = []; delMediaIdsNew = []; // 重新获取收藏状态 - queryHasFavVideo(); - SmartDialog.showToast('✅ 操作成功'); + bangumiActionStatus(); + SmartDialog.showToast('操作成功'); Get.back(); + } else { + SmartDialog.showToast(result['msg']); } } @@ -239,15 +237,22 @@ class BangumiIntroController extends GetxController { // 追番 Future bangumiAdd() async { - var result = - await VideoHttp.bangumiAdd(seasonId: bangumiDetail.value.seasonId); + var result = await VideoHttp.bangumiAdd( + seasonId: seasonId ?? bangumiDetail.value.seasonId); + if (result['status']) { + followStatus.value = 2; + isFollowed.value = true; + } SmartDialog.showToast(result['msg']); } // 取消追番 Future bangumiDel() async { - var result = - await VideoHttp.bangumiDel(seasonId: bangumiDetail.value.seasonId); + var result = await VideoHttp.bangumiDel( + seasonId: seasonId ?? bangumiDetail.value.seasonId); + if (result['status']) { + isFollowed.value = false; + } SmartDialog.showToast(result['msg']); } @@ -315,4 +320,35 @@ class BangumiIntroController extends GetxController { hiddenEpisodeBottomSheet() { 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; + } + } } diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart index b79b37b8..429b109a 100644 --- a/lib/pages/bangumi/introduction/view.dart +++ b/lib/pages/bangumi/introduction/view.dart @@ -239,104 +239,65 @@ class _BangumiInfoState extends State { Expanded( child: InkWell( onTap: () => showIntroDetail(), + borderRadius: BorderRadius.circular(8), child: SizedBox( height: 115 / 0.75, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded( - child: Text( - widget.bangumiDetail!.title!, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 20), - SizedBox( - width: 34, - height: 34, - child: IconButton( - style: ButtonStyle( - padding: MaterialStateProperty.all( - EdgeInsets.zero), - backgroundColor: - MaterialStateProperty.resolveWith( - (Set states) { - return t.colorScheme.primaryContainer - .withOpacity(0.7); - }), - ), - onPressed: () => - bangumiIntroController.bangumiAdd(), - icon: Icon( - Icons.favorite_border_rounded, - color: t.colorScheme.primary, - size: 22, + child: Padding( + padding: const EdgeInsets.fromLTRB(6, 4, 6, 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + widget.bangumiDetail!.title!, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), - ), - ], - ), - Row( - children: [ - StatView( - 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: 20), + Obx( + () => BangumiStatusWidget( + ctr: bangumiIntroController, + isFollowed: + bangumiIntroController.isFollowed.value, + ), ), - ), - 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 Spacer(), - Text( - '简介:${widget.bangumiDetail!.evaluate!}', - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13, - color: t.colorScheme.outline, + const SizedBox(height: 4), + Row( + children: [ + StatView( + view: widget.bangumiDetail!.stat!['views'], + size: 'medium', + ), + const SizedBox(width: 6), + 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 { }); } } + +// 追番状态 +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 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), + ], + ), + ); + } +} diff --git a/lib/pages/bangumi/view.dart b/lib/pages/bangumi/view.dart index 4092943b..f1f8d279 100644 --- a/lib/pages/bangumi/view.dart +++ b/lib/pages/bangumi/view.dart @@ -76,9 +76,14 @@ class _BangumiPageState extends State child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - '最近追番', - style: Theme.of(context).textTheme.titleMedium, + Obx( + () => 0 != _bangumidController.total.value + ? Text( + '我的追番(${_bangumidController.total.value})', + style: + Theme.of(context).textTheme.titleMedium, + ) + : const SizedBox(), ), IconButton( onPressed: () { diff --git a/lib/pages/bangumi/widgets/bangumi_panel.dart b/lib/pages/bangumi/widgets/bangumi_panel.dart index 3cb9abc0..db29d8e2 100644 --- a/lib/pages/bangumi/widgets/bangumi_panel.dart +++ b/lib/pages/bangumi/widgets/bangumi_panel.dart @@ -175,59 +175,60 @@ class _BangumiPanelState extends State { return Container( width: 150, margin: const EdgeInsets.only(right: 10), - child: Material( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( color: Theme.of(context).colorScheme.onInverseSurface, - borderRadius: BorderRadius.circular(6), - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: () => changeFucCall(page, i), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 10, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (isSelected) ...[ - Image.asset('assets/images/live.png', - color: primary, height: 12), - const SizedBox(width: 6) - ], + borderRadius: BorderRadius.circular(8), + ), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => changeFucCall(page, i), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 10, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (isSelected) ...[ + Image.asset('assets/images/live.png', + 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( - '第${i + 1}话', + page.badge!, style: TextStyle( - fontSize: 13, - color: isSelected ? primary : onSurface, + fontSize: 12, + color: primary, ), ), - const SizedBox(width: 2), - if (page.badge != null) ...[ - const Spacer(), - Text( - page.badge!, - style: TextStyle( - fontSize: 12, - color: primary, - ), - ), - ] - ], + ] + ], + ), + const SizedBox(height: 3), + Text( + page.longTitle!, + maxLines: 1, + style: TextStyle( + fontSize: 13, + color: isSelected ? primary : onSurface, ), - const SizedBox(height: 3), - Text( - page.longTitle!, - maxLines: 1, - style: TextStyle( - fontSize: 13, - color: isSelected ? primary : onSurface, - ), - overflow: TextOverflow.ellipsis, - ) - ], - ), + overflow: TextOverflow.ellipsis, + ) + ], ), ), ), diff --git a/lib/pages/bangumi/widgets/bangumu_card_v.dart b/lib/pages/bangumi/widgets/bangumu_card_v.dart index 10d95a1c..19aa4d88 100644 --- a/lib/pages/bangumi/widgets/bangumu_card_v.dart +++ b/lib/pages/bangumi/widgets/bangumu_card_v.dart @@ -25,6 +25,7 @@ class BangumiCardV extends StatelessWidget { RoutePush.bangumiPush( bangumiItem.seasonId, null, + progressIndex: bangumiItem.progressIndex, heroTag: heroTag, ); }, diff --git a/lib/pages/blacklist/index.dart b/lib/pages/blacklist/index.dart index bdd2346f..0616c1dc 100644 --- a/lib/pages/blacklist/index.dart +++ b/lib/pages/blacklist/index.dart @@ -87,7 +87,9 @@ class _BlackListPageState extends State { itemCount: list.length, itemBuilder: (BuildContext context, int index) { return ListTile( - onTap: () {}, + onTap: () => Get.toNamed( + '/member?mid=${list[index].mid}', + arguments: {'face': list[index].face}), leading: NetworkImgLayer( width: 45, height: 45, diff --git a/lib/pages/fav/widgets/item.dart b/lib/pages/fav/widgets/item.dart index 069051d5..412b498f 100644 --- a/lib/pages/fav/widgets/item.dart +++ b/lib/pages/fav/widgets/item.dart @@ -96,7 +96,9 @@ class VideoContent extends StatelessWidget { ), const Spacer(), Text( - [22, 0].contains(favFolderItem.attr) ? '公开' : '私密', + Constants.publicFavFolder.contains(favFolderItem.attr) + ? '公开' + : '私密', textAlign: TextAlign.start, style: TextStyle( fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, diff --git a/lib/pages/live/controller.dart b/lib/pages/live/controller.dart index 0ca9c815..492024e5 100644 --- a/lib/pages/live/controller.dart +++ b/lib/pages/live/controller.dart @@ -14,6 +14,7 @@ class LiveController extends GetxController { RxList liveList = [].obs; RxList liveFollowingList = [].obs; + RxInt liveFollowingCount = 0.obs; bool flag = false; OverlayEntry? popupDialog; Box setting = GStrorage.setting; @@ -27,9 +28,6 @@ class LiveController extends GetxController { // 获取推荐 Future queryLiveList(type) async { - // if (type == 'init') { - // _currentPage = 1; - // } var res = await LiveHttp.liveList( pn: _currentPage, ); @@ -68,13 +66,14 @@ class LiveController extends GetxController { // Future fetchLiveFollowing() async { - var res = await LiveHttp.liveFollowing(pn: 1, ps: 20); + var res = await LiveHttp.liveFollowing(pn: 1, ps: 10); if (res['status']) { liveFollowingList.value = (res['data'].list as List) .where((LiveFollowingItemModel item) => item.liveStatus == 1 && item.recordLiveTime == 0) // 根据条件过滤 .toList(); + liveFollowingCount.value = res['data'].liveCount; } return res; } diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index 51316fe3..eac705c7 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -162,34 +162,61 @@ class _LivePageState extends State mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Obx( - () => Text.rich( - TextSpan( - children: [ - const TextSpan( - text: ' 我的关注 ', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - ), - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx( + () => Text.rich( TextSpan( - text: ' ${_liveController.liveFollowingList.length}', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.primary, - ), + children: [ + const TextSpan( + text: ' 我的关注 ', + 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( - fontSize: 12, + ), + ), + InkWell( + 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, ), - ), - ], + ], + ), ), - ), + ], ), FutureBuilder( future: _futureBuilderFuture2, @@ -201,8 +228,7 @@ class _LivePageState extends State Map? data = snapshot.data; if (data?['status']) { RxList list = _liveController.liveFollowingList; - // ignore: invalid_use_of_protected_member - return Obx(() => LiveFollowingListView(list: list.value)); + return LiveFollowingListView(list: list); } else { return SizedBox( height: 80, @@ -230,69 +256,71 @@ class _LivePageState extends State } class LiveFollowingListView extends StatelessWidget { - final List list; + final RxList list; const LiveFollowingListView({super.key, required this.list}); @override Widget build(BuildContext context) { - return SizedBox( - height: 100, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemBuilder: (context, index) { - final LiveFollowingItemModel item = list[index]; - return Padding( - padding: const EdgeInsets.fromLTRB(3, 12, 3, 0), - child: Column( - children: [ - InkWell( - onTap: () { - Get.toNamed( - '/liveRoom?roomid=${item.roomId}', - arguments: { - 'liveItem': item, - 'heroTag': item.roomId.toString() - }, - ); - }, - child: Container( - width: 54, - height: 54, - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(27), - border: Border.all( - color: Theme.of(context).colorScheme.primary, - width: 1.5, + return Obx( + () => SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + final LiveFollowingItemModel item = list[index]; + return Padding( + padding: const EdgeInsets.fromLTRB(3, 12, 3, 0), + child: Column( + children: [ + InkWell( + onTap: () { + Get.toNamed( + '/liveRoom?roomid=${item.roomId}', + arguments: { + 'liveItem': item, + 'heroTag': item.roomId.toString() + }, + ); + }, + child: Container( + width: 54, + height: 54, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(27), + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: 1.5, + ), + ), + child: NetworkImgLayer( + width: 50, + height: 50, + type: 'avatar', + src: list[index].face, ), ), - child: NetworkImgLayer( - width: 50, - height: 50, - type: 'avatar', - src: list[index].face, + ), + const SizedBox(height: 6), + SizedBox( + width: 62, + child: Text( + list[index].uname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 12, + ), ), ), - ), - const SizedBox(height: 6), - SizedBox( - width: 62, - child: Text( - list[index].uname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 12, - ), - ), - ), - ], - ), - ); - }, - itemCount: list.length, + ], + ), + ); + }, + itemCount: list.length, + ), ), ); } diff --git a/lib/pages/live/widgets/live_item.dart b/lib/pages/live/widgets/live_item.dart index f70ba82b..c19567af 100644 --- a/lib/pages/live/widgets/live_item.dart +++ b/lib/pages/live/widgets/live_item.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/badge.dart'; +import 'package:pilipala/models/live/follow.dart'; import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/utils/image_save.dart'; import 'package:pilipala/utils/utils.dart'; @@ -9,7 +11,7 @@ import 'package:pilipala/common/widgets/network_img_layer.dart'; // 视频卡片 - 垂直布局 class LiveCardV extends StatelessWidget { - final LiveItemModel liveItem; + final dynamic liveItem; final int crossAxisCount; 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 { - final LiveItemModel? liveItem; + final dynamic liveItem; const VideoStat({ Key? key, @@ -178,25 +183,20 @@ class VideoStat extends StatelessWidget { liveItem!.areaName!, style: const TextStyle(fontSize: 11, color: Colors.white), ), - Text( - liveItem!.watchedShow!['text_small'], - style: const TextStyle(fontSize: 11, color: Colors.white), - ), + if (liveItem is LiveItemModel) ...[ + Text( + 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']), - // ], - // ), - // ), ); } } diff --git a/lib/pages/live_follow/controller.dart b/lib/pages/live_follow/controller.dart new file mode 100644 index 00000000..65c99384 --- /dev/null +++ b/lib/pages/live_follow/controller.dart @@ -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 liveFollowingList = + [].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'); + } +} diff --git a/lib/pages/live_follow/index.dart b/lib/pages/live_follow/index.dart new file mode 100644 index 00000000..6bba50bc --- /dev/null +++ b/lib/pages/live_follow/index.dart @@ -0,0 +1,4 @@ +library live_follow; + +export 'view.dart'; +export 'controller.dart'; diff --git a/lib/pages/live_follow/view.dart b/lib/pages/live_follow/view.dart new file mode 100644 index 00000000..2b116991 --- /dev/null +++ b/lib/pages/live_follow/view.dart @@ -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 createState() => _LiveFollowPageState(); +} + +class _LiveFollowPageState extends State { + 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, + ), + ); + } +} diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index 6987337b..b805b33d 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -122,18 +122,13 @@ class MemberController extends GetxController { // 合并关注/取关和拉黑逻辑 Future modifyRelation(String actionType) async { - if (userInfo == null) { - SmartDialog.showToast('账号未登录'); - return; - } - String contentText; int act; if (actionType == 'follow') { contentText = memberInfo.value.isFollowed! ? '确定取消关注UP主?' : '确定关注UP主?'; act = memberInfo.value.isFollowed! ? 2 : 1; } else if (actionType == 'block') { - contentText = attribute.value != 128 ? '确定拉黑UP主?' : '确定从黑名单移除UP主?'; + contentText = attribute.value != 128 ? '确定拉黑UP主?' : '确定从黑名单移除UP主?'; act = attribute.value != 128 ? 5 : 6; } else { return; diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 4ebc6153..9d446b07 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.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/models/member/info.dart'; import 'package:pilipala/pages/member/index.dart'; +import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/utils.dart'; import 'widgets/commen_widget.dart'; import 'widgets/conis.dart'; @@ -154,6 +156,25 @@ class _MemberPageState extends State bottom: MediaQuery.of(context).padding.bottom + 20, ), children: [ + Obx(() { + Rx 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(), /// 动态链接 @@ -318,6 +339,7 @@ class _MemberPageState extends State Rx memberInfo = _memberController.memberInfo; return Obx( () => Column( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ ProfilePanel(ctr: _memberController), @@ -376,7 +398,7 @@ class _MemberPageState extends State .value.vip!.label!['img_label_uri_hans_static'], height: 20, ), - ] + ], ], ), if (memberInfo.value.official!['title'] != '') ...[ @@ -393,6 +415,39 @@ class _MemberPageState extends State ), ], 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 ?? ''), ], ), diff --git a/lib/pages/message/system/view.dart b/lib/pages/message/system/view.dart index b207c63c..8e62517d 100644 --- a/lib/pages/message/system/view.dart +++ b/lib/pages/message/system/view.dart @@ -100,13 +100,6 @@ class SystemItem extends StatelessWidget { @override 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( padding: const EdgeInsets.fromLTRB(14, 14, 14, 12), child: Column( @@ -156,7 +149,7 @@ class SystemItem extends StatelessWidget { contentMap['message'].splitMapJoin( regExp, onMatch: (Match match) { - if (!match.group(0)!.startsWith('BV')) { + if (match.group(0) != '' && !match.group(0)!.startsWith('BV')) { spanChilds.add( WidgetSpan( child: Icon(Icons.link, color: colorScheme.primary, size: 16), diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart index 964f8625..8c478fd1 100644 --- a/lib/pages/search/controller.dart +++ b/lib/pages/search/controller.dart @@ -26,7 +26,6 @@ class SSearchController extends GetxController { Box setting = GStrorage.setting; bool enableHotKey = true; bool enableSearchSuggest = true; - late StreamController clearStream = StreamController.broadcast(); @override void onInit() { @@ -42,7 +41,6 @@ class SSearchController extends GetxController { final hint = parameters['hintText']; if (hint != null) { hintText = hint; - searchKeyWord.value = hintText; } } historyCacheList = GlobalDataCache().historyCacheList; @@ -55,10 +53,8 @@ class SSearchController extends GetxController { searchKeyWord.value = value; if (value == '') { searchSuggestList.value = []; - clearStream.add(false); return; } - clearStream.add(true); if (enableSearchSuggest) { _debouncer.call(() => querySearchSuggest(value)); } @@ -68,24 +64,20 @@ class SSearchController extends GetxController { controller.value.clear(); searchKeyWord.value = ''; searchSuggestList.value = []; - clearStream.add(false); } // 搜索 void submit() { - if (searchKeyWord.value == '') { + if (searchKeyWord.value == '' && hintText.isNotEmpty && hintText == '搜索') { return; + } else { + if (searchKeyWord.value == '' && hintText != '搜索') { + searchKeyWord.value = hintText; + controller.value.text = hintText; + } } - 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(); + hintText = '搜索'; + cacheHistory(); Get.toNamed('/searchResult', parameters: {'keyword': searchKeyWord.value}); } @@ -139,4 +131,15 @@ class SSearchController extends GetxController { GlobalDataCache().historyCacheList = []; 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(); + } } diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index 7baeb13f..373edf15 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -63,24 +63,35 @@ class _SearchPageState extends State with RouteAware { focusNode: _searchController.searchFocusNode, controller: _searchController.controller.value, textInputAction: TextInputAction.search, - onChanged: (value) => _searchController.onChange(value), + onChanged: _searchController.onChange, decoration: InputDecoration( hintText: _searchController.hintText, border: InputBorder.none, - suffixIcon: StreamBuilder( - initialData: false, - stream: _searchController.clearStream.stream, - builder: (_, snapshot) { - if (snapshot.data == true) { - return IconButton( + suffix: Obx(() { + RxString searchKeyWord = _searchController.searchKeyWord; + if (searchKeyWord.value.isEmpty) { + return const SizedBox(); + } + 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), onPressed: () => _searchController.onClear(), - ); - } else { - return const SizedBox(); - } - }, - ), + ), + ], + ); + }), ), onSubmitted: (String value) => _searchController.submit(), ), diff --git a/lib/pages/search_panel/controller.dart b/lib/pages/search_panel/controller.dart index 2d1aa228..32eadfc0 100644 --- a/lib/pages/search_panel/controller.dart +++ b/lib/pages/search_panel/controller.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:pilipala/http/member.dart'; import 'package:pilipala/http/search.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/utils.dart'; @@ -31,7 +33,7 @@ class SearchPanelController extends GetxController { tids: searchType!.type != 'video' ? null : tids.value, ); if (result['status']) { - if (type == 'onRefresh') { + if (type == 'init') { resultList.value = result['data'].list ?? []; } else { resultList.addAll(result['data'].list ?? []); @@ -39,12 +41,36 @@ class SearchPanelController extends GetxController { page.value++; 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; } Future onRefresh() async { page.value = 1; - await onSearch(type: 'onRefresh'); + await onSearch(); } // 返回顶部并刷新 diff --git a/lib/pages/search_panel/view.dart b/lib/pages/search_panel/view.dart index f032b12b..6d9a9535 100644 --- a/lib/pages/search_panel/view.dart +++ b/lib/pages/search_panel/view.dart @@ -4,6 +4,7 @@ import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.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/widgets/http_error.dart'; import 'package:pilipala/models/common/search_type.dart'; @@ -81,11 +82,11 @@ class _SearchPanelState extends State future: _futureBuilderFuture, builder: (context, snapshot) { 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; RxList list = ctr.resultList; - if (data['status']) { + if (list.isNotEmpty) { return Obx(() { switch (widget.searchType) { case SearchType.video: @@ -110,21 +111,18 @@ class _SearchPanelState extends State }); } else { return HttpError( - errMsg: data['msg'], - fn: () { - setState(() { - _searchPanelController.onSearch(); - }); - }, + errMsg: '没有数据', + isShowBtn: false, + fn: () => {}, isInSliver: false, ); } } else { return HttpError( - errMsg: '没有相关数据', + errMsg: data?['msg'] ?? '请求异常', fn: () { setState(() { - _searchPanelController.onSearch(); + _futureBuilderFuture = _searchPanelController.onRefresh(); }); }, isInSliver: false, @@ -143,7 +141,7 @@ class _SearchPanelState extends State case SearchType.media_bangumi: return const MediaBangumiSkeleton(); case SearchType.bili_user: - return const VideoCardHSkeleton(); + return const UserListSkeleton(); case SearchType.live_room: return const VideoCardHSkeleton(); default: diff --git a/lib/pages/search_panel/widgets/media_bangumi_panel.dart b/lib/pages/search_panel/widgets/media_bangumi_panel.dart index 5bba0ab8..0a3704bb 100644 --- a/lib/pages/search_panel/widgets/media_bangumi_panel.dart +++ b/lib/pages/search_panel/widgets/media_bangumi_panel.dart @@ -1,12 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; -import 'package:pilipala/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/utils.dart'; @@ -30,8 +25,8 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) { // }); }, child: Padding( - padding: const EdgeInsets.fromLTRB( - StyleString.safeSpace, 7, StyleString.safeSpace, 7), + padding: const EdgeInsets.symmetric( + horizontal: StyleString.safeSpace, vertical: 7), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/pages/search_panel/widgets/user_panel.dart b/lib/pages/search_panel/widgets/user_panel.dart index 918082bc..561cb1f8 100644 --- a/lib/pages/search_panel/widgets/user_panel.dart +++ b/lib/pages/search_panel/widgets/user_panel.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/utils/utils.dart'; @@ -12,15 +13,16 @@ Widget searchUserPanel(BuildContext context, ctr, list) { controller: ctr!.scrollController, addAutomaticKeepAlives: false, addRepaintBoundaries: false, - itemCount: list!.length, + itemCount: list.length, itemBuilder: (context, index) { - var i = list![index]; - String heroTag = Utils.makeHeroTag(i!.mid); + var i = list[index]; + String heroTag = Utils.makeHeroTag(i.mid); return InkWell( onTap: () => Get.toNamed('/member?mid=${i.mid}', arguments: {'heroTag': heroTag, 'face': i.upic}), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), + padding: const EdgeInsets.symmetric( + horizontal: StyleString.safeSpace, vertical: 7), child: Row( children: [ Hero( @@ -41,7 +43,7 @@ Widget searchUserPanel(BuildContext context, ctr, list) { Row( children: [ Text( - i!.uname, + i.uname!, style: const TextStyle( fontSize: 14, ), @@ -53,15 +55,16 @@ Widget searchUserPanel(BuildContext context, ctr, list) { ), ], ), - Row( - children: [ - Text('粉丝:${i.fans} ', style: style), - Text(' 视频:${i.videos}', style: style) - ], - ), - if (i.officialVerify['desc'] != '') + if (i.fans != null && i.videos != null) + Row( + children: [ + Text('粉丝:${i.fans} ', style: style), + Text(' 视频:${i.videos}', style: style) + ], + ), + if (i.officialVerify!['desc'] != '') Text( - i.officialVerify['desc'], + i.officialVerify!['desc'], style: style, ), ], diff --git a/lib/pages/search_result/view.dart b/lib/pages/search_result/view.dart index 96fdd91d..9056905f 100644 --- a/lib/pages/search_result/view.dart +++ b/lib/pages/search_result/view.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/pages/search_panel/index.dart'; import 'controller.dart'; +import 'widget/tab_bar.dart'; class SearchResultPage extends StatefulWidget { const SearchResultPage({super.key}); @@ -29,6 +30,17 @@ class _SearchResultPageState extends State ); } + // tab点击事件 + void _onTap(int index) { + if (index == _searchResultController.tabIndex) { + Get.find( + tag: SearchType.values[index].type + + _searchResultController.keyword!) + .animateToTop(); + } + _searchResultController.tabIndex = index; + } + @override Widget build(BuildContext context) { return Scaffold( @@ -55,50 +67,10 @@ class _SearchResultPageState extends State body: Column( children: [ const SizedBox(height: 4), - Container( - width: double.infinity, - padding: const EdgeInsets.only(left: 8), - color: Theme.of(context).colorScheme.surface, - 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( - tag: SearchType.values[index].type + - _searchResultController.keyword!) - .animateToTop(); - } - - _searchResultController.tabIndex = index; - }, - )), - ), - ), + TabBarWidget( + onTap: _onTap, + tabController: _tabController!, + searchResultCtr: _searchResultController, ), Expanded( child: TabBarView( diff --git a/lib/pages/search_result/widget/tab_bar.dart b/lib/pages/search_result/widget/tab_bar.dart new file mode 100644 index 00000000..acb6a3de --- /dev/null +++ b/lib/pages/search_result/widget/tab_bar.dart @@ -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, + ), + ), + ), + ); + } +} diff --git a/lib/pages/subscription/widgets/item.dart b/lib/pages/subscription/widgets/item.dart index 0389b4a6..bfa2e46d 100644 --- a/lib/pages/subscription/widgets/item.dart +++ b/lib/pages/subscription/widgets/item.dart @@ -90,52 +90,55 @@ class VideoContent extends StatelessWidget { return Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(10, 2, 6, 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Stack( children: [ - Text( - subFolderItem.title!, - textAlign: TextAlign.start, - style: const TextStyle( - fontWeight: FontWeight.w500, - letterSpacing: 0.3, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + subFolderItem.title!, + 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 - ? Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - IconButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - onPressed: () => cancelSub?.call(subFolderItem), - icon: Icon( - Icons.clear_outlined, - color: Theme.of(context).colorScheme.outline, - size: 18, - ), - ) - ], + ? Positioned( + right: 0, + bottom: -4, + child: IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => cancelSub?.call(subFolderItem), + icon: Icon( + Icons.clear_outlined, + color: Theme.of(context).colorScheme.outline, + size: 18, + ), + ), ) : const SizedBox() ], diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 2c1e47eb..e78d8121 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -338,73 +338,49 @@ class VideoIntroController extends GetxController { return; } final int currentStatus = followStatus['attribute']; - int actionStatus = 0; - switch (currentStatus) { - case 0: - actionStatus = 1; - break; - case 2: - actionStatus = 2; - break; - default: - actionStatus = 0; - break; + if (currentStatus == 128) { + modifyRelation('block', currentStatus); + } else { + modifyRelation('follow', currentStatus); } - 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) { + final Color outline = Theme.of(Get.context!).colorScheme.outline; return AlertDialog( title: const Text('提示'), - content: Text(currentStatus == 0 ? '关注UP主?' : '取消关注UP主?'), + content: Text(contentText), actions: [ TextButton( - onPressed: () => SmartDialog.dismiss(), - child: Text( - '点错了', - style: TextStyle(color: Theme.of(context).colorScheme.outline), - ), + onPressed: Navigator.of(context).pop, + child: Text('点错了', style: TextStyle(color: outline)), ), TextButton( - onPressed: () async { - var result = await VideoHttp.relationMod( - mid: videoDetail.value.owner!.mid!, - act: actionStatus, - reSrc: 14, - ); - if (result['status']) { - switch (currentStatus) { - 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('确认'), + onPressed: () => modifyRelationFetch( + context, + mid, + act, + currentStatus, + actionType, + ), + 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 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或番剧分集 Future changeSeasonOrbangu( String bvid, diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 81d0c3f0..80176a7b 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -470,8 +470,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { const Spacer(), Obx( () { - final bool isFollowed = - videoIntroController.followStatus['attribute'] != 0; + final int attr = + videoIntroController.followStatus['attribute'] ?? 0; return videoIntroController.followStatus.isEmpty ? const SizedBox() : SizedBox( @@ -484,15 +484,19 @@ class _VideoInfoState extends State with TickerProviderStateMixin { left: 8, right: 8, ), - foregroundColor: isFollowed + foregroundColor: attr != 0 ? outline : t.colorScheme.onPrimary, - backgroundColor: isFollowed + backgroundColor: attr != 0 ? t.colorScheme.onInverseSurface : t.colorScheme.primary, // 设置按钮背景色 ), child: Text( - isFollowed ? '已关注' : '关注', + attr == 128 + ? '已拉黑' + : attr != 0 + ? '已关注' + : '关注', style: TextStyle( fontSize: t.textTheme.labelMedium!.fontSize, diff --git a/lib/pages/video/detail/introduction/widgets/fav_panel.dart b/lib/pages/video/detail/introduction/widgets/fav_panel.dart index b13a033b..fa043cb6 100644 --- a/lib/pages/video/detail/introduction/widgets/fav_panel.dart +++ b/lib/pages/video/detail/introduction/widgets/fav_panel.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; @@ -66,16 +67,14 @@ class _FavPanelState extends State { onTap: () => widget.ctr!.onChoose(item.favState != 1, index), dense: true, - leading: Icon([22, 0].contains(item.attr) - ? Icons.lock_outline - : Icons.folder_outlined), + leading: Icon( + Constants.publicFavFolder.contains(item.attr) + ? Icons.folder_outlined + : Icons.lock_outline), minLeadingWidth: 0, title: Text(item.title!), subtitle: Text( - '${item.mediaCount}个内容 - ${[ - 22, - 0 - ].contains(item.attr) ? '公开' : '私密'}', + '${item.mediaCount}个内容 - ${Constants.publicFavFolder.contains(item.attr) ? '公开' : '私密'}', ), trailing: Transform.scale( scale: 0.9, diff --git a/lib/pages/video/detail/introduction/widgets/intro_detail.dart b/lib/pages/video/detail/introduction/widgets/intro_detail.dart index 78c621c0..c74558a0 100644 --- a/lib/pages/video/detail/introduction/widgets/intro_detail.dart +++ b/lib/pages/video/detail/introduction/widgets/intro_detail.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:pilipala/http/constants.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/utils.dart'; @@ -15,6 +16,10 @@ class IntroDetail extends StatelessWidget { @override Widget build(BuildContext context) { + TextStyle textStyle = TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary, + ); return SizedBox( width: double.infinity, child: Column( @@ -29,12 +34,7 @@ class IntroDetail extends StatelessWidget { Clipboard.setData(ClipboardData(text: videoDetail!.bvid!)); SmartDialog.showToast('已复制'); }, - child: Text( - videoDetail!.bvid!, - style: TextStyle( - fontSize: 13, - color: Theme.of(context).colorScheme.primary), - ), + child: Text(videoDetail!.bvid!, style: textStyle), ), const SizedBox(width: 10), GestureDetector( @@ -44,12 +44,18 @@ class IntroDetail extends StatelessWidget { ClipboardData(text: videoDetail!.aid!.toString())); SmartDialog.showToast('已复制'); }, - child: Text( - videoDetail!.aid!.toString(), - style: TextStyle( - fontSize: 13, - color: Theme.of(context).colorScheme.primary), - ), + child: Text(videoDetail!.aid!.toString(), style: textStyle), + ), + const SizedBox(width: 10), + GestureDetector( + onTap: () { + feedBack(); + String videoUrl = + '${HttpString.baseUrl}/video/${videoDetail!.bvid!}'; + Clipboard.setData(ClipboardData(text: videoUrl)); + SmartDialog.showToast('已复制视频链接'); + }, + child: Text('复制链接', style: textStyle), ) ], ), diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index f6863466..be69d99c 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -45,7 +45,7 @@ class ReplyItem extends StatelessWidget { final bool? showReplyRow; final Function? replyReply; final ReplyType? replyType; - final bool? replySave; + final bool replySave; @override Widget build(BuildContext context) { @@ -55,14 +55,14 @@ class ReplyItem extends StatelessWidget { child: InkWell( // 点击整个评论区 评论详情/回复 onTap: () { - if (replySave!) { + if (replySave) { return; } feedBack(); replyReply?.call(replyItem, null, replyItem!.rcount! > 0); }, onLongPress: () { - if (replySave!) { + if (replySave) { return; } feedBack(); @@ -235,53 +235,32 @@ class ReplyItem extends StatelessWidget { // title Container( margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4), - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints boxConstraints) { - String text = replyItem?.content?.message ?? ''; - bool didExceedMaxLines = false; - final double maxWidth = boxConstraints.maxWidth; - TextPainter? textPainter; - final int maxLines = - replyItem!.content!.isText! && replyLevel == '1' ? 6 : 999; - try { - textPainter = TextPainter( - text: TextSpan(text: text), - maxLines: maxLines, - textDirection: Directionality.of(context), - ); - textPainter.layout(maxWidth: maxWidth); - didExceedMaxLines = textPainter.didExceedMaxLines; - } catch (e) { - debugPrint('Error while measuring text: $e'); - didExceedMaxLines = false; - } - 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, - textPainter, - ), - ], - ), - ); - }), + child: !replySave + ? LayoutBuilder(builder: + (BuildContext context, BoxConstraints boxConstraints) { + String text = replyItem?.content?.message ?? ''; + bool didExceedMaxLines = false; + final double maxWidth = boxConstraints.maxWidth; + TextPainter? textPainter; + final int maxLines = + replyItem!.content!.isText! && replyLevel == '1' + ? 6 + : 999; + try { + textPainter = TextPainter( + text: TextSpan(text: text), + maxLines: maxLines, + textDirection: Directionality.of(context), + ); + textPainter.layout(maxWidth: maxWidth); + didExceedMaxLines = textPainter.didExceedMaxLines; + } catch (e) { + debugPrint('Error while measuring text: $e'); + didExceedMaxLines = false; + } + return replyContent(context, didExceedMaxLines, textPainter); + }) + : replyContent(context, false, null), ), // 操作区域 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) { ColorScheme colorScheme = Theme.of(context).colorScheme; diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index 01ede374..60a04f74 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -153,6 +153,7 @@ class ChatItem extends StatelessWidget { jsonDecode(content['content']) .map((m) => m['text'] as String) .join("\n"), + textAlign: TextAlign.center, style: TextStyle( letterSpacing: 0.6, height: 5, @@ -359,39 +360,40 @@ class ChatItem extends StatelessWidget { ), const SizedBox(width: 6), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - i['field1'], - maxLines: 2, - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor(context), - fontWeight: FontWeight.bold, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + i['field1'], + maxLines: 2, + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor(context), + fontWeight: FontWeight.bold, + ), ), - ), - Text( - i['field2'], - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor(context).withOpacity(0.6), - fontSize: 12, + Text( + i['field2'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor(context).withOpacity(0.6), + fontSize: 12, + ), ), - ), - Text( - i['field3'], - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor(context).withOpacity(0.6), - fontSize: 12, + Text( + i['field3'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor(context).withOpacity(0.6), + fontSize: 12, + ), ), - ), - ], - )), + ], + ), + ), ], ), ), diff --git a/lib/plugin/pl_gallery/interactiveviewer_gallery.dart b/lib/plugin/pl_gallery/interactiveviewer_gallery.dart index cd13194e..f0b11619 100644 --- a/lib/plugin/pl_gallery/interactiveviewer_gallery.dart +++ b/lib/plugin/pl_gallery/interactiveviewer_gallery.dart @@ -30,6 +30,13 @@ typedef IndexedFocusedWidgetBuilder = Widget Function( typedef IndexedTagStringBuilder = String Function(int index); +// 图片操作类型 +enum ImgActionType { + share, + copy, + save, +} + class InteractiveviewerGallery extends StatefulWidget { const InteractiveviewerGallery({ required this.sources, @@ -39,6 +46,11 @@ class InteractiveviewerGallery extends StatefulWidget { this.minScale = 1.0, this.onPageChanged, this.onDismissed, + this.actionType = const [ + ImgActionType.share, + ImgActionType.copy, + ImgActionType.save + ], Key? key, }) : super(key: key); @@ -59,6 +71,8 @@ class InteractiveviewerGallery extends StatefulWidget { final ValueChanged? onDismissed; + final List actionType; + @override State createState() => _InteractiveviewerGalleryState(); @@ -247,8 +261,8 @@ class _InteractiveviewerGalleryState extends State onDoubleTapDown: (TapDownDetails details) { _doubleTapLocalPosition = details.localPosition; }, - onDoubleTap: onDoubleTap, - onLongPress: onLongPress, + onDoubleTap: _onDoubleTap, + onLongPress: _onLongPress, child: widget.itemBuilder != null ? widget.itemBuilder!( context, @@ -298,28 +312,7 @@ class _InteractiveviewerGalleryState extends State : const SizedBox(), PopupMenuButton( itemBuilder: (context) { - return [ - 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("保存图片"), - ), - ]; + return _buildPopupMenuList(); }, child: const Icon(Icons.more_horiz, color: Colors.white), ), @@ -332,7 +325,7 @@ class _InteractiveviewerGalleryState extends State } // 图片分享 - void onShareImg(String imgUrl) async { + void _onShareImg(String imgUrl) async { SmartDialog.showLoading(); var response = await Dio() .get(imgUrl, options: Options(responseType: ResponseType.bytes)); @@ -346,7 +339,7 @@ class _InteractiveviewerGalleryState extends State } // 复制图片 - void onCopyImg(String imgUrl) { + void _onCopyImg(String imgUrl) { Clipboard.setData( ClipboardData(text: widget.sources[currentIndex!].toString())) .then((value) { @@ -380,7 +373,7 @@ class _InteractiveviewerGalleryState extends State ); } - onDoubleTap() { + _onDoubleTap() { Matrix4 matrix = _transformationController!.value.clone(); double currentScale = matrix.row0.x; @@ -427,7 +420,7 @@ class _InteractiveviewerGalleryState extends State .whenComplete(() => _onScaleChanged(targetScale)); } - onLongPress() { + _onLongPress() { showModalBottomSheet( context: context, useRootNavigator: true, @@ -456,31 +449,81 @@ class _InteractiveviewerGalleryState extends State ), ), ), - ListTile( - 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('保存图片'), - ), + ..._buildListTitles(), ], ), ); }, ); } + + List _buildPopupMenuList() { + List 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 _buildListTitles() { + List 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; + } } diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 7a14b499..2d581293 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -5,6 +5,7 @@ import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/pages/fav_edit/index.dart'; import 'package:pilipala/pages/follow_search/view.dart'; +import 'package:pilipala/pages/live_follow/index.dart'; import 'package:pilipala/pages/member_article/index.dart'; import 'package:pilipala/pages/message/at/index.dart'; import 'package:pilipala/pages/message/like/index.dart'; @@ -199,6 +200,8 @@ class Routes { name: '/memberArticle', page: () => const MemberArticlePage()), // 用户信息编辑 CustomGetPage(name: '/mineEdit', page: () => const MineEditPage()), + // 关注的直播up + CustomGetPage(name: '/liveFollowing', page: () => const LiveFollowPage()), ]; } diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index 3a69843c..5d4196bc 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -212,9 +212,9 @@ class PiliSchame { } } - static Future biliScheme(SchemeEntity value) async { - final String host = value.host!; - final String path = value.path!; + static Future biliScheme(Uri value) async { + final String host = value.host; + final String path = value.path; switch (host) { case 'root': Navigator.popUntil( @@ -301,7 +301,7 @@ class PiliSchame { break; default: SmartDialog.showToast('未匹配地址,请联系开发者'); - Clipboard.setData(ClipboardData(text: value.toJson().toString())); + Clipboard.setData(ClipboardData(text: value.toString())); break; } } diff --git a/lib/utils/route_push.dart b/lib/utils/route_push.dart index 9ee28846..9254da2a 100644 --- a/lib/utils/route_push.dart +++ b/lib/utils/route_push.dart @@ -8,7 +8,7 @@ import 'package:pilipala/utils/utils.dart'; class RoutePush { // 番剧跳转 static Future bangumiPush(int? seasonId, int? epId, - {String? heroTag}) async { + {String? heroTag, int? progressIndex}) async { SmartDialog.showLoading(msg: '获取中...'); try { var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId); @@ -19,7 +19,10 @@ class RoutePush { return; } 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 cid = episode.cid!; final String bvid = episode.bvid!; @@ -31,7 +34,7 @@ class RoutePush { }; arguments['heroTag'] = heroTag ?? Utils.makeHeroTag(cid); Get.toNamed( - '/video?bvid=$bvid&cid=$cid&epId=$epId', + '/video?bvid=$bvid&cid=$cid&epId=$epId&seasonId=$seasonId', arguments: arguments, ); } else { diff --git a/pubspec.yaml b/pubspec.yaml index b9d29abf..0b9711ad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -218,6 +218,7 @@ flutter: - assets/images/logo/ - assets/images/live/ - assets/images/video/ + - assets/images/pay/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware