From 297020a16f51f704156e8e5810581532000b1296 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 20 Apr 2023 23:24:14 +0800 Subject: [PATCH 01/30] =?UTF-8?q?mod:=20=E8=A7=86=E9=A2=91=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/video/detail/controller.dart | 29 ++++++ lib/pages/video/detail/view.dart | 132 ++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 4 deletions(-) diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index e69de29b..c0eae8b4 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -0,0 +1,29 @@ +import 'package:get/get.dart'; + +class VideoDetailController extends GetxController { + // 视频aid + String aid = Get.parameters['aid']!; + + // 是否预渲染 骨架屏 + bool preRender = false; + + // 视频详情 上个页面传入 + Map videoItem = {}; + + // 请求状态 + RxBool isLoading = false.obs; + + @override + void onInit() { + super.onInit(); + if (Get.arguments.isNotEmpty) { + if (Get.arguments.containsKey('videoItem')) { + preRender = true; + var args = Get.arguments['videoItem']; + if (args.pic != null && args.pic != '') { + videoItem['pic'] = args.pic; + } + } + } + } +} diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 0c1d47df..fa0c99d2 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -1,18 +1,142 @@ +import 'package:get/get.dart'; import 'package:flutter/material.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/pages/video/detail/controller.dart'; class VideoDetailPage extends StatefulWidget { - const VideoDetailPage({super.key}); + const VideoDetailPage({Key? key}) : super(key: key); @override State createState() => _VideoDetailPageState(); } class _VideoDetailPageState extends State { + final VideoDetailController videoDetailController = + Get.put(VideoDetailController()); + + final _tabs = ['简介', '评论']; + @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('videoDetail'), + return DefaultTabController( + length: _tabs.length, // tab的数量. + child: SafeArea( + top: false, + bottom: false, + child: Scaffold( + body: NestedScrollView( + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: + NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + title: const Text("视频详情"), + // floating: true, + // snap: true, + pinned: true, + elevation: 0, + scrolledUnderElevation: 0, + forceElevated: innerBoxIsScrolled, + expandedHeight: MediaQuery.of(context).size.width * 9 / 16, + collapsedHeight: MediaQuery.of(context).size.width * 9 / 16, + toolbarHeight: kToolbarHeight, + flexibleSpace: FlexibleSpaceBar( + background: Padding( + padding: EdgeInsets.only( + bottom: 50, + top: MediaQuery.of(context).padding.top), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = MediaQuery.of(context).devicePixelRatio; + return NetworkImgLayer( + src: videoDetailController.videoItem['pic'], + width: maxWidth, + height: maxHeight, + ); + }, + ), + ), + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(50.0), + child: Container( + height: 50, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context) + .dividerColor + .withOpacity(0.1)))), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Container( + width: 180, + margin: const EdgeInsets.only(left: 20), + child: TabBar( + splashBorderRadius: BorderRadius.circular(6), + dividerColor: Colors.transparent, + tabs: _tabs + .map((String name) => Tab(text: name)) + .toList(), + ), + ), + // 弹幕开关 + // const Spacer(), + // Flexible( + // flex: 2, + // child: Container( + // height: 50, + // ), + // ), + ], + ), + ), + ), + ), + ), + ]; + }, + body: TabBarView( + children: [ + Builder(builder: (context) { + return CustomScrollView( + key: const PageStorageKey('简介'), + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context), + ), + // const VideoIntroPanel(), + const SliverToBoxAdapter( + child: Text('简介'), + ) + ], + ); + }), + Builder(builder: (context) { + return CustomScrollView( + key: const PageStorageKey('评论'), + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context), + ), + const SliverToBoxAdapter( + child: Text('评论'), + ) + ], + ); + }) + ], + ), + ), + ), ), ); } From f3b7ad0302b46e14db608dd083a282f01b0749b5 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 21 Apr 2023 11:12:51 +0800 Subject: [PATCH 02/30] =?UTF-8?q?mod:=20=E8=A7=86=E9=A2=91=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E3=80=81=E8=B7=B3=E8=BD=ACHero=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/widgets/video_card_v.dart | 18 +- lib/http/video.dart | 32 ++ lib/models/video_detail_res.dart | 524 ++++++++++++++++++ lib/pages/home/controller.dart | 1 - lib/pages/video/detail/controller.dart | 2 + .../video/detail/introduction/controller.dart | 47 ++ .../video/detail/introduction/index.dart | 4 + lib/pages/video/detail/introduction/view.dart | 471 ++++++++++++++++ lib/pages/video/detail/view.dart | 17 +- .../detail/widgets/expandable_section.dart | 83 +++ lib/utils/utils.dart | 5 + 11 files changed, 1188 insertions(+), 16 deletions(-) create mode 100644 lib/http/video.dart create mode 100644 lib/models/video_detail_res.dart create mode 100644 lib/pages/video/detail/introduction/controller.dart create mode 100644 lib/pages/video/detail/introduction/index.dart create mode 100644 lib/pages/video/detail/introduction/view.dart create mode 100644 lib/pages/video/detail/widgets/expandable_section.dart diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 8481e540..6e52b2ed 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -15,6 +15,7 @@ class VideoCardV extends StatelessWidget { @override Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(videoItem.id); return Card( elevation: 0.8, clipBehavior: Clip.hardEdge, @@ -26,7 +27,7 @@ class VideoCardV extends StatelessWidget { onTap: () async { await Future.delayed(const Duration(milliseconds: 200)); Get.toNamed('/video?aid=${videoItem.id}', - arguments: {'videoItem': videoItem}); + arguments: {'videoItem': videoItem, 'heroTag': heroTag}); }, onLongPress: () { print('长按'); @@ -46,12 +47,15 @@ class VideoCardV extends StatelessWidget { double PR = MediaQuery.of(context).devicePixelRatio; return Stack( children: [ - NetworkImgLayer( - // 指定图片尺寸 - // src: videoItem.pic + '@${(maxWidth * 2).toInt()}w', - src: videoItem.pic + '@.webp', - width: maxWidth, - height: maxHeight, + Hero( + tag: heroTag, + child: NetworkImgLayer( + // 指定图片尺寸 + // src: videoItem.pic + '@${(maxWidth * 2).toInt()}w', + src: videoItem.pic + '@.webp', + width: maxWidth, + height: maxHeight, + ), ), Positioned( left: 0, diff --git a/lib/http/video.dart b/lib/http/video.dart new file mode 100644 index 00000000..98d448bd --- /dev/null +++ b/lib/http/video.dart @@ -0,0 +1,32 @@ +import 'package:pilipala/http/api.dart'; +import 'package:pilipala/http/init.dart'; +import 'package:pilipala/models/video_detail_res.dart'; + +class VideoHttp { + // 视频信息 标题、简介 + static Future videoDetail(data) async { + var res = await Request().get(Api.videoDetail, data: data); + VideoDetailResponse result = VideoDetailResponse.fromJson(res.data); + if (result.code == 0) { + return {'status': true, 'data': result.data!}; + } else { + Map errMap = { + -400: '请求错误', + -403: '权限不足', + -404: '无视频', + 62002: '稿件不可见', + 62004: '稿件审核中', + }; + return { + 'status': false, + 'data': null, + 'msg': errMap[result.code] ?? '请求异常' + }; + } + } + + // static Future videoRecommend(data) async { + // var res = await Request().get(Api.videoRecommend, data: data); + // return res; + // } +} diff --git a/lib/models/video_detail_res.dart b/lib/models/video_detail_res.dart new file mode 100644 index 00000000..8b5180f3 --- /dev/null +++ b/lib/models/video_detail_res.dart @@ -0,0 +1,524 @@ +import 'dart:convert'; + +class VideoDetailResponse { + int? code; + String? message; + int? ttl; + VideoDetailData? data; + + VideoDetailResponse({ + this.code, + this.message, + this.ttl, + this.data, + }); + + VideoDetailResponse.fromJson(Map json) { + code = json["code"]; + message = json["message"]; + ttl = json["ttl"]; + data = json["data"] == null ? null : VideoDetailData.fromJson(json["data"]); + } + + Map toJson() { + final Map data = {}; + data["code"] = code; + data["message"] = message; + data["ttl"] = ttl; + data["data"] = data; + + return data; + } +} + +class VideoDetailData { + String? bvid; + int? aid; + int? videos; + int? tid; + String? tname; + int? copyright; + String? pic; + String? title; + int? pubdate; + int? ctime; + String? desc; + List? descV2; + int? state; + int? duration; + Map? rights; + Owner? owner; + Stat? stat; + String? videoDynamic; + int? cid; + Dimension? dimension; + dynamic premiere; + int? teenageMode; + bool? isChargeableSeason; + bool? isStory; + bool? noCache; + List? pages; + Subtitle? subtitle; + // Label? label; + bool? isSeasonDisplay; + UserGarb? userGarb; + HonorReply? honorReply; + String? likeIcon; + bool? needJumpBv; + + VideoDetailData({ + this.bvid, + this.aid, + this.videos, + this.tid, + this.tname, + this.copyright, + this.pic, + this.title, + this.pubdate, + this.ctime, + this.desc, + this.descV2, + this.state, + this.duration, + this.rights, + this.owner, + this.stat, + this.videoDynamic, + this.cid, + this.dimension, + this.premiere, + this.teenageMode, + this.isChargeableSeason, + this.isStory, + this.noCache, + this.pages, + this.subtitle, + this.isSeasonDisplay, + this.userGarb, + this.honorReply, + this.likeIcon, + this.needJumpBv, + }); + + VideoDetailData.fromJson(Map json) { + bvid = json["bvid"]; + aid = json["aid"]; + videos = json["videos"]; + tid = json["tid"]; + tname = json["tname"]; + copyright = json["copyright"]; + pic = json["pic"]; + title = json["title"]; + pubdate = json["pubdate"]; + ctime = json["ctime"]; + desc = json["desc"]; + descV2 = json["desc_v2"] == null + ? [] + : List.from(json["desc_v2"]!.map((e) => DescV2.fromJson(e))); + state = json["state"]; + duration = json["duration"]; + rights = + Map.from(json["rights"]!).map((k, v) => MapEntry(k, v)); + owner = json["owner"] == null ? null : Owner.fromJson(json["owner"]); + stat = json["stat"] == null ? null : Stat.fromJson(json["stat"]); + videoDynamic = json["dynamic"]; + cid = json["cid"]; + dimension = json["dimension"] == null + ? null + : Dimension.fromJson(json["dimension"]); + premiere = json["premiere"]; + teenageMode = json["teenage_mode"]; + isChargeableSeason = json["is_chargeable_season"]; + isStory = json["is_story"]; + noCache = json["no_cache"]; + pages = json["pages"] == null + ? [] + : List.from(json["pages"]!.map((e) => Page.fromJson(e))); + subtitle = + json["subtitle"] == null ? null : Subtitle.fromJson(json["subtitle"]); + isSeasonDisplay = json["is_season_display"]; + userGarb = + json["user_garb"] == null ? null : UserGarb.fromJson(json["user_garb"]); + honorReply = json["honor_reply"] == null + ? null + : HonorReply.fromJson(json["honor_reply"]); + likeIcon = json["like_icon"]; + needJumpBv = json["need_jump_bv"]; + } + + Map toJson() => { + "bvid": bvid, + "aid": aid, + "videos": videos, + "tid": tid, + "tname": tname, + "copyright": copyright, + "pic": pic, + "title": title, + "pubdate": pubdate, + "ctime": ctime, + "desc": desc, + "desc_v2": descV2 == null + ? [] + : List.from(descV2!.map((e) => e.toJson())), + "state": state, + "duration": duration, + "rights": + Map.from(rights!).map((k, v) => MapEntry(k, v)), + "owner": owner?.toJson(), + "stat": stat?.toJson(), + "dynamic": videoDynamic, + "cid": cid, + "dimension": dimension?.toJson(), + "premiere": premiere, + "teenage_mode": teenageMode, + "is_chargeable_season": isChargeableSeason, + "is_story": isStory, + "no_cache": noCache, + "pages": pages == null + ? [] + : List.from(pages!.map((e) => e.toJson())), + "subtitle": subtitle?.toJson(), + "is_season_display": isSeasonDisplay, + "user_garb": userGarb?.toJson(), + "honor_reply": honorReply?.toJson(), + "like_icon": likeIcon, + "need_jump_bv": needJumpBv, + }; +} + +class DescV2 { + String? rawText; + int? type; + int? bizId; + + DescV2({ + this.rawText, + this.type, + this.bizId, + }); + + fromRawJson(String str) { + return DescV2.fromJson(json.decode(str)); + } + + String toRawJson() => json.encode(toJson()); + + DescV2.fromJson(Map json) { + rawText = json["raw_text"]; + type = json["type"]; + bizId = json["biz_id"]; + } + + Map toJson() { + final Map data = {}; + + data["raw_text"] = rawText; + data["type"] = type; + data["biz_id"] = bizId; + + return data; + } +} + +class Dimension { + int? width; + int? height; + int? rotate; + + Dimension({ + this.width, + this.height, + this.rotate, + }); + + fromRawJson(String str) => Dimension.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Dimension.fromJson(Map json) { + width = json["width"]; + height = json["height"]; + rotate = json["rotate"]; + } + + Map toJson() { + final Map data = {}; + + data["width"] = width; + data["height"] = height; + data["rotate"] = rotate; + data["data"] = data; + + return data; + } +} + +class HonorReply { + List? honor; + + HonorReply({ + this.honor, + }); + + fromRawJson(String str) => HonorReply.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + HonorReply.fromJson(Map json) { + honor = json["honor"] == null + ? [] + : List.from(json["honor"]!.map((x) => Honor.fromJson(x))); + } + + Map toJson() { + final Map data = {}; + + data["honor"] = + honor == null ? [] : List.from(honor!.map((x) => x.toJson())); + return data; + } +} + +class Honor { + int? aid; + int? type; + String? desc; + int? weeklyRecommendNum; + + Honor({ + this.aid, + this.type, + this.desc, + this.weeklyRecommendNum, + }); + + fromRawJson(String str) => Honor.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Honor.fromJson(Map json) { + aid = json["aid"]; + type = json["type"]; + desc = json["desc"]; + weeklyRecommendNum = json["weekly_recommend_num"]; + } + + Map toJson() { + final Map data = {}; + + data["aid"] = aid; + data["type"] = type; + data["desc"] = desc; + data["weekly_recommend_num"] = weeklyRecommendNum; + + return data; + } +} + +class Owner { + int? mid; + String? name; + String? face; + + Owner({ + this.mid, + this.name, + this.face, + }); + + fromRawJson(String str) => Owner.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Owner.fromJson(Map json) { + mid = json["mid"]; + name = json["name"]; + face = json["face"]; + } + + Map toJson() { + final Map data = {}; + data["mid"] = mid; + data["name"] = name; + data["face"] = face; + return data; + } +} + +class Page { + int? cid; + int? page; + String? from; + String? pagePart; + int? duration; + String? vid; + String? weblink; + Dimension? dimension; + String? firstFrame; + + Page({ + this.cid, + this.page, + this.from, + this.pagePart, + this.duration, + this.vid, + this.weblink, + this.dimension, + this.firstFrame, + }); + + fromRawJson(String str) => Page.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Page.fromJson(Map json) { + cid = json["cid"]; + page = json["page"]; + from = json["from"]; + pagePart = json["part"]; + duration = json["duration"]; + vid = json["vid"]; + weblink = json["weblink"]; + dimension = json["dimension"] == null + ? null + : Dimension.fromJson(json["dimension"]); + firstFrame = json["first_frame"]; + } + + Map toJson() { + final Map data = {}; + data["cid"] = cid; + data["page"] = page; + data["from"] = from; + data["part"] = pagePart; + data["duration"] = duration; + data["vid"] = vid; + data["weblink"] = weblink; + data["dimension"] = dimension?.toJson(); + data["first_frame"] = firstFrame; + return data; + } +} + +class Stat { + int? aid; + int? view; + int? danmaku; + int? reply; + int? favorite; + int? coin; + int? share; + int? nowRank; + int? hisRank; + int? like; + int? dislike; + String? evaluation; + String? argueMsg; + + Stat({ + this.aid, + this.view, + this.danmaku, + this.reply, + this.favorite, + this.coin, + this.share, + this.nowRank, + this.hisRank, + this.like, + this.dislike, + this.evaluation, + this.argueMsg, + }); + + fromRawJson(String str) => Stat.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Stat.fromJson(Map json) { + aid = json["aid"]; + view = json["view"]; + danmaku = json["danmaku"]; + reply = json["reply"]; + favorite = json["favorite"]; + coin = json["coin"]; + share = json["share"]; + nowRank = json["now_rank"]; + hisRank = json["his_rank"]; + like = json["like"]; + dislike = json["dislike"]; + evaluation = json["evaluation"]; + argueMsg = json["argue_msg"]; + } + + Map toJson() { + final Map data = {}; + + data["aid"] = aid; + data["view"] = view; + data["danmaku"] = danmaku; + data["reply"] = reply; + data["favorite"] = favorite; + data["coin"] = coin; + data["share"] = share; + data["now_rank"] = nowRank; + data["his_rank"] = hisRank; + data["like"] = like; + data["dislike"] = dislike; + data["evaluation"] = evaluation; + data["argue_msg"] = argueMsg; + return data; + } +} + +class Subtitle { + bool? allowSubmit; + List? list; + + Subtitle({ + this.allowSubmit, + this.list, + }); + + fromRawJson(String str) => Subtitle.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Subtitle.fromJson(Map json) { + allowSubmit = json["allow_submit"]; + list = json["list"] == null + ? [] + : List.from(json["list"]!.map((x) => x)); + } + + Map toJson() { + final Map data = {}; + + data["allow_submit"] = allowSubmit; + data["list"] = list == null ? [] : List.from(list!.map((x) => x)); + return data; + } +} + +class UserGarb { + String? urlImageAniCut; + + UserGarb({ + this.urlImageAniCut, + }); + + fromRawJson(String str) => UserGarb.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + UserGarb.fromJson(Map json) { + urlImageAniCut = json["url_image_ani_cut"]; + } + + Map toJson() => {"url_image_ani_cut": urlImageAniCut}; +} + +class Label {} diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index a81b9591..20f166e6 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -47,7 +47,6 @@ class HomeController extends GetxController { // 上拉加载 Future onLoad() async { - await Future.delayed(const Duration(milliseconds: 500)); queryRcmdFeed('onLoad'); } diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index c0eae8b4..5feddde6 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -13,6 +13,7 @@ class VideoDetailController extends GetxController { // 请求状态 RxBool isLoading = false.obs; + String heroTag = ''; @override void onInit() { super.onInit(); @@ -24,6 +25,7 @@ class VideoDetailController extends GetxController { videoItem['pic'] = args.pic; } } + heroTag = Get.arguments['heroTag']; } } } diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart new file mode 100644 index 00000000..1480c004 --- /dev/null +++ b/lib/pages/video/detail/introduction/controller.dart @@ -0,0 +1,47 @@ +import 'package:get/get.dart'; +import 'package:pilipala/http/api.dart'; +import 'package:pilipala/http/init.dart'; +import 'package:pilipala/models/video_detail_res.dart'; + +class VideoIntroController extends GetxController { + // 视频aid + String aid = Get.parameters['aid']!; + + // 是否预渲染 骨架屏 + bool preRender = false; + + // 视频详情 上个页面传入 + Map? videoItem = {}; + + // 请求状态 + RxBool isLoading = false.obs; + + // 视频详情 请求返回 + Rx videoDetail = VideoDetailData().obs; + + @override + void onInit() { + super.onInit(); + if (Get.arguments.isNotEmpty) { + if (Get.arguments.containsKey('videoItem')) { + preRender = true; + var args = Get.arguments['videoItem']; + videoItem!['pic'] = args.pic; + videoItem!['title'] = args.title; + videoItem!['stat'] = args.stat; + videoItem!['pubdate'] = args.pubdate; + videoItem!['owner'] = args.owner; + } + } + } + + Future queryVideoDetail() async { + var res = await Request().get(Api.videoDetail, data: { + 'aid': aid, + }); + VideoDetailResponse result = VideoDetailResponse.fromJson(res.data); + videoDetail.value = result.data!; + // await Future.delayed(const Duration(seconds: 3)); + return true; + } +} diff --git a/lib/pages/video/detail/introduction/index.dart b/lib/pages/video/detail/introduction/index.dart new file mode 100644 index 00000000..60b98c3c --- /dev/null +++ b/lib/pages/video/detail/introduction/index.dart @@ -0,0 +1,4 @@ +library video_detail_introduction; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart new file mode 100644 index 00000000..4ddaa53c --- /dev/null +++ b/lib/pages/video/detail/introduction/view.dart @@ -0,0 +1,471 @@ +import 'package:flutter/rendering.dart'; +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/pages/video/detail/widgets/expandable_section.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/common/widgets/stat/danmu.dart'; +import 'package:pilipala/common/widgets/stat/view.dart'; +import 'package:pilipala/models/video_detail_res.dart'; +import 'package:pilipala/pages/video/detail/introduction/controller.dart'; +import 'package:pilipala/utils/utils.dart'; + +class VideoIntroPanel extends StatefulWidget { + const VideoIntroPanel({Key? key}) : super(key: key); + + @override + State createState() => _VideoIntroPanelState(); +} + +class _VideoIntroPanelState extends State { + final VideoIntroController videoIntroController = + Get.put(VideoIntroController()); + VideoDetailData? videoDetail; + + @override + void initState() { + super.initState(); + videoIntroController.videoDetail.listen((value) { + videoDetail = value; + }); + } + + @override + void dispose() { + videoIntroController.onClose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: videoIntroController.queryVideoDetail(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + print(snapshot.data); + if (snapshot.data) { + // 请求成功 + return _buildView(context, false, videoDetail); + } else { + // 请求错误 + return Center( + child: IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + setState(() {}); + }, + ), + ); + } + } else { + return _buildView(context, true, videoDetail); + } + }, + ); + } + + Widget _buildView(context, loadingStatus, videoDetail) { + // return CustomScrollView( + // key: const PageStorageKey('简介'), + // slivers: [ + // SliverOverlapInjector( + // handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)), + // VideoInfo(loadingStatus: loadingStatus, videoDetail: videoDetail), + // SliverToBoxAdapter( + // child: + // Divider(color: Theme.of(context).dividerColor.withOpacity(0.1)), + // ), + // const RecommendList() + // ], + // ); + return VideoInfo(loadingStatus: loadingStatus, videoDetail: videoDetail); + } +} + +class VideoInfo extends StatefulWidget { + bool loadingStatus = false; + VideoDetailData? videoDetail; + + VideoInfo({Key? key, required this.loadingStatus, this.videoDetail}) + : super(key: key); + + @override + State createState() => _VideoInfoState(); +} + +class _VideoInfoState extends State with TickerProviderStateMixin { + Map videoItem = Get.put(VideoIntroController()).videoItem!; + bool isExpand = false; + + /// 手动控制动画的控制器 + late AnimationController? _manualController; + + /// 手动控制 + late Animation? _manualAnimation; + + @override + void initState() { + super.initState(); + + /// 不设置重复,使用代码控制进度,动画时间1秒 + _manualController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 400), + ); + _manualAnimation = + Tween(begin: 0.5, end: 1.5).animate(_manualController!); + } + + @override + Widget build(BuildContext context) { + return SliverPadding( + padding: const EdgeInsets.only(left: 12, right: 12, top: 25), + sliver: SliverToBoxAdapter( + child: !widget.loadingStatus || videoItem.isNotEmpty + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + NetworkImgLayer( + type: 'avatar', + src: !widget.loadingStatus + ? widget.videoDetail!.owner!.face + : videoItem['owner'].face, + width: 38, + height: 38, + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, + ), + const SizedBox(width: 14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(!widget.loadingStatus + ? widget.videoDetail!.owner!.name + : videoItem['owner'].name), + const SizedBox(height: 2), + // Text.rich( + // TextSpan( + // style: TextStyle( + // color: Theme.of(context) + // .colorScheme + // .outline, + // fontSize: 11), + // children: const [ + // TextSpan(text: '2.6万粉丝'), + // TextSpan(text: ' '), + // TextSpan(text: '2.6万粉丝'), + // ]), + // ), + ]), + const Spacer(), + AnimatedOpacity( + opacity: widget.loadingStatus ? 0 : 1, + duration: const Duration(milliseconds: 150), + child: SizedBox( + height: 35, + child: ElevatedButton( + onPressed: () {}, child: const Text('+ 关注')), + ), + ), + const SizedBox(width: 4), + ], + ), + const SizedBox(height: 18), + // 标题 超过两行收起 + // Container( + // color: Colors.blue[50], + // child: SizedOverflowBox( + // size: const Size(50.0, 50.0), + // alignment: AlignmentDirectional.bottomStart, + // child: Container(height: 150.0, width: 150.0, color: Colors.blue,), + // ), + // ), + // Row( + // children: [ + // Expanded( + // child: ExpandedSection( + // expand: false, + // begin: 1, + // end: 1, + // child: Text( + // !widget.loadingStatus + // ? widget.videoDetail!.title + // : videoItem['title'], + // overflow: TextOverflow.ellipsis, + // maxLines: 1, + // ), + // ), + // ), + // const SizedBox(width: 10), + // RotationTransition( + // turns: _manualAnimation!, + // child: IconButton( + // onPressed: () { + // /// 获取动画当前的值 + // var value = _manualController!.value; + + // /// 0.5代表 180弧度 + // if (value == 0) { + // _manualController!.animateTo(0.5); + // } else { + // _manualController!.animateTo(0); + // } + // setState(() { + // isExpand = !isExpand; + // }); + // }, + // icon: const Icon(Icons.expand_less)), + // ), + // ], + // ), + SizedBox( + width: double.infinity, + child: Text( + !widget.loadingStatus + ? widget.videoDetail!.title + : videoItem['title'], + // style: Theme.of(context).textTheme.titleMedium, + // maxLines: 2, + ), + ), + // const SizedBox(height: 5), + // 播放量、评论、日期 + Row( + children: [ + const SizedBox(width: 2), + StatView( + theme: 'gray', + view: !widget.loadingStatus + ? widget.videoDetail!.stat!.view + : videoItem['stat'].view, + size: 'medium', + ), + const SizedBox(width: 10), + StatDanMu( + theme: 'gray', + danmu: !widget.loadingStatus + ? widget.videoDetail!.stat!.danmaku + : videoItem['stat'].danmaku, + size: 'medium', + ), + const SizedBox(width: 10), + Text( + Utils.dateFormat( + !widget.loadingStatus + ? widget.videoDetail!.pubdate + : videoItem['pubdate'], + formatType: 'detail'), + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.outline), + ), + const Spacer(), + RotationTransition( + turns: _manualAnimation!, + child: IconButton( + onPressed: () { + /// 获取动画当前的值 + var value = _manualController!.value; + + /// 0.5代表 180弧度 + if (value == 0) { + _manualController!.animateTo(0.5); + } else { + _manualController!.animateTo(0); + } + setState(() { + isExpand = !isExpand; + }); + }, + icon: Icon( + Icons.expand_less, + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + ], + ), + // const SizedBox(height: 5), + // 简介 默认收起 + if (!widget.loadingStatus) + ExpandedSection( + expand: isExpand, + begin: 0.0, + end: 1.0, + child: DefaultTextStyle( + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + height: 1.5, + fontSize: + Theme.of(context).textTheme.labelMedium?.fontSize, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: SelectableRegion( + magnifierConfiguration: + const TextMagnifierConfiguration(), + focusNode: FocusNode(), + selectionControls: MaterialTextSelectionControls(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.videoDetail!.bvid!), + Text(widget.videoDetail!.desc!), + ], + ), + ), + ), + ), + ), + _actionGrid(context), + const SizedBox(height: 5), + ], + ) + : const Center(child: CircularProgressIndicator()), + ), + ); + } + + // 喜欢 投币 分享 + Widget _actionGrid(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + return Container( + color: Colors.black12, + height: constraints.maxWidth / 5, + child: GridView.count( + primary: false, + padding: const EdgeInsets.all(0), + crossAxisCount: 5, + children: [ + ActionItem( + icon: const Icon(Icons.thumb_up), + onTap: () => {}, + selectStatus: false, + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.like!.toString() + : '-'), + ActionItem( + icon: const Icon(Icons.thumb_down), + onTap: () => {}, + selectStatus: false, + loadingStatus: widget.loadingStatus, + text: '不喜欢'), + ActionItem( + icon: const Icon(Icons.generating_tokens), + onTap: () => {}, + selectStatus: false, + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.coin!.toString() + : '-'), + ActionItem( + icon: const Icon(Icons.star), + onTap: () => {}, + selectStatus: false, + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.favorite!.toString() + : '-'), + ActionItem( + icon: const Icon(Icons.share), + onTap: () => {}, + selectStatus: false, + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.share!.toString() + : '-'), + ], + ), + ); + }); + } +} + +class ActionItem extends StatelessWidget { + Icon? icon; + Function? onTap; + bool? loadingStatus; + String? text; + bool selectStatus = false; + + ActionItem({ + Key? key, + this.icon, + this.onTap, + this.loadingStatus, + this.text, + required this.selectStatus, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + child: Ink( + child: InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon!.icon!, + color: selectStatus + ? Theme.of(context).primaryColor + : Theme.of(context).colorScheme.outline), + const SizedBox(height: 2), + AnimatedOpacity( + opacity: loadingStatus! ? 0 : 1, + duration: const Duration(milliseconds: 200), + child: Text( + text!, + style: TextStyle( + color: selectStatus + ? Theme.of(context).primaryColor + : Theme.of(context).colorScheme.outline, + fontSize: Theme.of(context).textTheme.labelSmall?.fontSize), + ), + ), + ], + ), + ), + )); + } +} + +class RecommendList extends StatelessWidget { + const RecommendList({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return Material( + child: InkWell( + onTap: () {}, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 10), + child: Text( + '$index」 求推荐一些高质量的系统地介绍 ChatGPT 及相关技术的视频、文章或者书', + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith(height: 1.6), + ), + ), + ), + ); + }, childCount: 50), + ); + } +} + +class ActionGrid extends StatelessWidget { + const ActionGrid({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index fa0c99d2..d0ad3a85 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -2,6 +2,7 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/pages/video/detail/controller.dart'; +import 'package:pilipala/pages/video/detail/introduction/index.dart'; class VideoDetailPage extends StatefulWidget { const VideoDetailPage({Key? key}) : super(key: key); @@ -52,10 +53,13 @@ class _VideoDetailPageState extends State { double maxWidth = boxConstraints.maxWidth; double maxHeight = boxConstraints.maxHeight; double PR = MediaQuery.of(context).devicePixelRatio; - return NetworkImgLayer( - src: videoDetailController.videoItem['pic'], - width: maxWidth, - height: maxHeight, + return Hero( + tag: videoDetailController.heroTag, + child: NetworkImgLayer( + src: videoDetailController.videoItem['pic'], + width: maxWidth, + height: maxHeight, + ), ); }, ), @@ -112,10 +116,7 @@ class _VideoDetailPageState extends State { handle: NestedScrollView.sliverOverlapAbsorberHandleFor( context), ), - // const VideoIntroPanel(), - const SliverToBoxAdapter( - child: Text('简介'), - ) + const VideoIntroPanel(), ], ); }), diff --git a/lib/pages/video/detail/widgets/expandable_section.dart b/lib/pages/video/detail/widgets/expandable_section.dart new file mode 100644 index 00000000..c3d49a80 --- /dev/null +++ b/lib/pages/video/detail/widgets/expandable_section.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +class ExpandedSection extends StatefulWidget { + final Widget child; + final bool expand; + double begin = 0.0; + double end = 1.0; + + ExpandedSection( + {this.expand = false, + required this.child, + required this.begin, + required this.end}); + + @override + _ExpandedSectionState createState() => _ExpandedSectionState(); +} + +class _ExpandedSectionState extends State + with SingleTickerProviderStateMixin { + late AnimationController expandController; + late Animation animation; + + @override + void initState() { + super.initState(); + prepareAnimations(); + _runExpandCheck(); + } + + ///Setting up the animation + // void prepareAnimations() { + // expandController = AnimationController( + // vsync: this, duration: const Duration(milliseconds: 500)); + // animation = CurvedAnimation( + // parent: expandController, + // curve: Curves.fastOutSlowIn, + // ); + // } + + void prepareAnimations() { + expandController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 400)); + Animation curve = CurvedAnimation( + parent: expandController, + curve: Curves.fastOutSlowIn, + ); + animation = Tween(begin: widget.begin, end: widget.end).animate(curve); + // animation = CurvedAnimation( + // parent: expandController, + // curve: Curves.fastOutSlowIn, + // ); + } + + void _runExpandCheck() { + if (widget.expand) { + expandController.forward(); + } else { + expandController.reverse(); + } + } + + @override + void didUpdateWidget(ExpandedSection oldWidget) { + super.didUpdateWidget(oldWidget); + _runExpandCheck(); + } + + @override + void dispose() { + expandController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizeTransition( + axisAlignment: -1.0, + sizeFactor: animation, + child: widget.child, + ); + } +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 2bd88366..084a6e6f 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,5 +1,6 @@ // 工具函数 import 'dart:io'; +import 'dart:math'; import 'package:get/get_utils/get_utils.dart'; import 'package:path_provider/path_provider.dart'; @@ -130,4 +131,8 @@ class Utils { } return date; } + + static String makeHeroTag(v) { + return v.toString() + Random().nextInt(9999).toString(); + } } From 171c16a4f923e1e5791504a88b52417070591a1f Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 21 Apr 2023 14:06:01 +0800 Subject: [PATCH 03/30] =?UTF-8?q?mod:=20=E7=9B=B8=E5=85=B3=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E8=A7=86=E9=A2=91=E5=BC=80=E5=8F=91=E3=80=81=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E8=B7=B3=E8=BD=ACHero=E3=80=81=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/widgets/video_card_h.dart | 23 +++++---- lib/http/api.dart | 3 ++ lib/http/video.dart | 8 +-- lib/models/model_hot_video_item.dart | 4 +- lib/pages/video/detail/controller.dart | 4 ++ .../video/detail/introduction/controller.dart | 5 ++ .../video/detail/introduction/index.dart | 2 +- lib/pages/video/detail/introduction/view.dart | 17 ++++--- lib/pages/video/detail/player/controller.dart | 0 lib/pages/video/detail/player/index.dart | 4 ++ lib/pages/video/detail/player/view.dart | 0 .../video/detail/related/controller.dart | 31 ++++++++++++ lib/pages/video/detail/related/index.dart | 4 ++ lib/pages/video/detail/related/view.dart | 49 +++++++++++++++++++ lib/pages/video/detail/view.dart | 29 +++++++---- 15 files changed, 152 insertions(+), 31 deletions(-) create mode 100644 lib/pages/video/detail/player/controller.dart create mode 100644 lib/pages/video/detail/player/index.dart create mode 100644 lib/pages/video/detail/player/view.dart create mode 100644 lib/pages/video/detail/related/controller.dart create mode 100644 lib/pages/video/detail/related/index.dart create mode 100644 lib/pages/video/detail/related/view.dart diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index 5333ad72..5156a191 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -13,13 +13,15 @@ class VideoCardH extends StatelessWidget { @override Widget build(BuildContext context) { + int aid = videoItem.aid; + String heroTag = Utils.makeHeroTag(aid); return Material( child: Ink( child: InkWell( onTap: () async { await Future.delayed(const Duration(milliseconds: 200)); - int aid = videoItem['id'] ?? videoItem['aid']; - Get.toNamed('/video?aid=$aid', arguments: {'videoItem': videoItem}); + Get.toNamed('/video?aid=$aid', + arguments: {'videoItem': videoItem, 'heroTag': heroTag}); }, child: Container( padding: const EdgeInsets.fromLTRB( @@ -44,12 +46,15 @@ class VideoCardH extends StatelessWidget { double PR = MediaQuery.of(context).devicePixelRatio; return Stack( children: [ - NetworkImgLayer( - // src: videoItem['pic'] + - // '@${(maxWidth * 2).toInt()}w', - src: videoItem.pic + '@.webp', - width: maxWidth, - height: maxHeight, + Hero( + tag: heroTag, + child: NetworkImgLayer( + // src: videoItem['pic'] + + // '@${(maxWidth * 2).toInt()}w', + src: videoItem.pic + '@.webp', + width: maxWidth, + height: maxHeight, + ), ), // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), Positioned( @@ -109,7 +114,7 @@ class VideoContent extends StatelessWidget { overflow: TextOverflow.ellipsis, ), const Spacer(), - if (videoItem.rcmdReason != '' && + if (videoItem.rcmdReason != null && videoItem.rcmdReason.content != '') Container( padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5), diff --git a/lib/http/api.dart b/lib/http/api.dart index ccea17f7..4f088015 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -6,4 +6,7 @@ class Api { // 视频详情 // 竖屏 https://api.bilibili.com/x/web-interface/view?aid=527403921 static const String videoDetail = '/x/web-interface/view'; + + // 视频详情页 相关视频 + static const String relatedList = '/x/web-interface/archive/related'; } diff --git a/lib/http/video.dart b/lib/http/video.dart index 98d448bd..9e023051 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -25,8 +25,8 @@ class VideoHttp { } } - // static Future videoRecommend(data) async { - // var res = await Request().get(Api.videoRecommend, data: data); - // return res; - // } + static Future videoRecommend(data) async { + var res = await Request().get(Api.relatedList, data: data); + return res; + } } diff --git a/lib/models/model_hot_video_item.dart b/lib/models/model_hot_video_item.dart index 7c30dcd4..1a9f582e 100644 --- a/lib/models/model_hot_video_item.dart +++ b/lib/models/model_hot_video_item.dart @@ -80,7 +80,9 @@ class HotVideoItemModel { pubLocation = json["pub_location"]; seasontype = json["seasontype"]; isOgv = json["isOgv"]; - rcmdReason = RcmdReason.fromJson(json['rcmd_reason']); + rcmdReason = json['rcmd_reason'] != '' + ? RcmdReason.fromJson(json['rcmd_reason']) + : null; } } diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 5feddde6..89142e01 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -1,6 +1,9 @@ import 'package:get/get.dart'; class VideoDetailController extends GetxController { + // tabs + RxList tabs = ['简介', '评论'].obs; + // 视频aid String aid = Get.parameters['aid']!; @@ -14,6 +17,7 @@ class VideoDetailController extends GetxController { RxBool isLoading = false.obs; String heroTag = ''; + @override void onInit() { super.onInit(); diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 1480c004..e098880d 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -2,6 +2,7 @@ import 'package:get/get.dart'; import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/models/video_detail_res.dart'; +import 'package:pilipala/pages/video/detail/controller.dart'; class VideoIntroController extends GetxController { // 视频aid @@ -41,6 +42,10 @@ class VideoIntroController extends GetxController { }); VideoDetailResponse result = VideoDetailResponse.fromJson(res.data); videoDetail.value = result.data!; + Get.find().tabs.value = [ + '简介', + '评论 ${result.data!.stat!.reply}' + ]; // await Future.delayed(const Duration(seconds: 3)); return true; } diff --git a/lib/pages/video/detail/introduction/index.dart b/lib/pages/video/detail/introduction/index.dart index 60b98c3c..5eaae572 100644 --- a/lib/pages/video/detail/introduction/index.dart +++ b/lib/pages/video/detail/introduction/index.dart @@ -1,4 +1,4 @@ -library video_detail_introduction; +library video_intro_panel; export './controller.dart'; export './view.dart'; diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 4ddaa53c..41e624d4 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -1,4 +1,3 @@ -import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/constants.dart'; @@ -17,11 +16,16 @@ class VideoIntroPanel extends StatefulWidget { State createState() => _VideoIntroPanelState(); } -class _VideoIntroPanelState extends State { +class _VideoIntroPanelState extends State + with AutomaticKeepAliveClientMixin { final VideoIntroController videoIntroController = Get.put(VideoIntroController()); VideoDetailData? videoDetail; + // 添加页面缓存 + @override + bool get wantKeepAlive => true; + @override void initState() { super.initState(); @@ -42,10 +46,10 @@ class _VideoIntroPanelState extends State { future: videoIntroController.queryVideoDetail(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { - print(snapshot.data); if (snapshot.data) { // 请求成功 - return _buildView(context, false, videoDetail); + // return _buildView(context, false, videoDetail); + return VideoInfo(loadingStatus: false, videoDetail: videoDetail); } else { // 请求错误 return Center( @@ -58,7 +62,8 @@ class _VideoIntroPanelState extends State { ); } } else { - return _buildView(context, true, videoDetail); + // return _buildView(context, true, videoDetail); + return VideoInfo(loadingStatus: true, videoDetail: videoDetail); } }, ); @@ -320,7 +325,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ), ), _actionGrid(context), - const SizedBox(height: 5), + // const SizedBox(height: 5), ], ) : const Center(child: CircularProgressIndicator()), diff --git a/lib/pages/video/detail/player/controller.dart b/lib/pages/video/detail/player/controller.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/pages/video/detail/player/index.dart b/lib/pages/video/detail/player/index.dart new file mode 100644 index 00000000..c5d4ff78 --- /dev/null +++ b/lib/pages/video/detail/player/index.dart @@ -0,0 +1,4 @@ +library video_player; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/video/detail/player/view.dart b/lib/pages/video/detail/player/view.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/pages/video/detail/related/controller.dart b/lib/pages/video/detail/related/controller.dart new file mode 100644 index 00000000..872f8871 --- /dev/null +++ b/lib/pages/video/detail/related/controller.dart @@ -0,0 +1,31 @@ +import 'dart:convert'; + +import 'package:get/get.dart'; +import 'package:pilipala/http/video.dart'; +import 'package:pilipala/models/model_hot_video_item.dart'; + +class ReleatedController extends GetxController { + // 视频aid + String aid = Get.parameters['aid']!; + // 推荐视频列表 + List relatedVideoList = []; + + Future queryVideoRecommend() async { + try { + var res = await VideoHttp.videoRecommend({'aid': aid}); + List list = []; + try { + for (var i in res.data['data']) { + list.add(HotVideoItemModel.fromJson(i)); + } + relatedVideoList = list; + } catch (err) { + return err.toString(); + } + + return res.data['data']; + } catch (err) { + return err.toString(); + } + } +} diff --git a/lib/pages/video/detail/related/index.dart b/lib/pages/video/detail/related/index.dart new file mode 100644 index 00000000..ce29b10a --- /dev/null +++ b/lib/pages/video/detail/related/index.dart @@ -0,0 +1,4 @@ +library releated_video_panel; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/video/detail/related/view.dart b/lib/pages/video/detail/related/view.dart new file mode 100644 index 00000000..319e75d4 --- /dev/null +++ b/lib/pages/video/detail/related/view.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/video_card_h.dart'; +import './controller.dart'; + +class RelatedVideoPanel extends StatefulWidget { + const RelatedVideoPanel({super.key}); + + @override + State createState() => _RelatedVideoPanelState(); +} + +class _RelatedVideoPanelState extends State { + final ReleatedController _releatedController = Get.put(ReleatedController()); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _releatedController.queryVideoRecommend(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data!.isNotEmpty) { + // 请求成功 + List videoList = _releatedController.relatedVideoList; + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + if (index == videoList.length) { + return SizedBox(height: MediaQuery.of(context).padding.bottom); + } else { + return VideoCardH( + videoItem: videoList[index], + ); + } + }, childCount: videoList.length + 1)); + } else { + // 请求错误 + return const Center( + child: Text('出错了'), + ); + } + } else { + return const SliverToBoxAdapter( + child: Text('请求中'), + ); + } + }, + ); + } +} diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index d0ad3a85..eb088f19 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/introduction/index.dart'; +import 'package:pilipala/pages/video/detail/related/index.dart'; class VideoDetailPage extends StatefulWidget { const VideoDetailPage({Key? key}) : super(key: key); @@ -15,12 +16,10 @@ class _VideoDetailPageState extends State { final VideoDetailController videoDetailController = Get.put(VideoDetailController()); - final _tabs = ['简介', '评论']; - @override Widget build(BuildContext context) { return DefaultTabController( - length: _tabs.length, // tab的数量. + length: videoDetailController.tabs.length, // tab的数量. child: SafeArea( top: false, bottom: false, @@ -80,14 +79,16 @@ class _VideoDetailPageState extends State { mainAxisSize: MainAxisSize.max, children: [ Container( - width: 180, + width: 280, margin: const EdgeInsets.only(left: 20), - child: TabBar( - splashBorderRadius: BorderRadius.circular(6), - dividerColor: Colors.transparent, - tabs: _tabs - .map((String name) => Tab(text: name)) - .toList(), + child: Obx( + () => TabBar( + splashBorderRadius: BorderRadius.circular(6), + dividerColor: Colors.transparent, + tabs: videoDetailController.tabs + .map((String name) => Tab(text: name)) + .toList(), + ), ), ), // 弹幕开关 @@ -117,6 +118,14 @@ class _VideoDetailPageState extends State { context), ), const VideoIntroPanel(), + SliverToBoxAdapter( + child: Divider( + color: + Theme.of(context).dividerColor.withOpacity(0.1), + ), + ), + const SliverPadding(padding: EdgeInsets.only(bottom: 5)), + const RelatedVideoPanel(), ], ); }), From 3aee691d00b9db885b122f8c126cf4296527362e Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 21 Apr 2023 16:07:34 +0800 Subject: [PATCH 04/30] =?UTF-8?q?mod:=20=E6=8E=A5=E5=8F=A3=E6=95=B4?= =?UTF-8?q?=E7=90=86=E3=80=81=E5=A2=9E=E5=8A=A0up=E7=B2=89=E4=B8=9D?= =?UTF-8?q?=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/widgets/http_error.dart | 33 ++++++++++ lib/http/api.dart | 6 +- lib/http/user.dart | 13 ++++ lib/http/video.dart | 66 +++++++++++++++++-- lib/pages/home/controller.dart | 31 ++++----- lib/pages/hot/controller.dart | 34 +++++----- .../video/detail/introduction/controller.dart | 45 +++++++++---- lib/pages/video/detail/introduction/view.dart | 56 +++++++++------- .../video/detail/related/controller.dart | 22 +------ lib/pages/video/detail/related/view.dart | 12 ++-- 10 files changed, 210 insertions(+), 108 deletions(-) create mode 100644 lib/common/widgets/http_error.dart create mode 100644 lib/http/user.dart diff --git a/lib/common/widgets/http_error.dart b/lib/common/widgets/http_error.dart new file mode 100644 index 00000000..b3aa348d --- /dev/null +++ b/lib/common/widgets/http_error.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class HttpError extends StatelessWidget { + HttpError({required this.errMsg, required this.fn, super.key}); + + String errMsg = ''; + final Function()? fn; + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: SizedBox( + height: 150, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + errMsg, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () { + fn!(); + }, + child: const Text('点击重试')) + ], + ), + ), + ); + } +} diff --git a/lib/http/api.dart b/lib/http/api.dart index 4f088015..4c378780 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -5,8 +5,12 @@ class Api { static const String hotList = '/x/web-interface/popular'; // 视频详情 // 竖屏 https://api.bilibili.com/x/web-interface/view?aid=527403921 - static const String videoDetail = '/x/web-interface/view'; + static const String videoIntro = '/x/web-interface/view'; // 视频详情页 相关视频 static const String relatedList = '/x/web-interface/archive/related'; + + // 用户(被)关注数、投稿数 + // https://api.bilibili.com/x/relation/stat?vmid=697166795 + static const String userStat = '/x/relation/stat'; } diff --git a/lib/http/user.dart b/lib/http/user.dart new file mode 100644 index 00000000..327f0ba5 --- /dev/null +++ b/lib/http/user.dart @@ -0,0 +1,13 @@ +import 'package:pilipala/http/api.dart'; +import 'package:pilipala/http/init.dart'; + +class UserHttp { + static Future userStat(mid) async { + var res = await Request().get(Api.userStat, data: {'vmid': mid}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false}; + } + } +} diff --git a/lib/http/video.dart b/lib/http/video.dart index 9e023051..b263f3ba 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -1,11 +1,58 @@ import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; +import 'package:pilipala/models/model_hot_video_item.dart'; +import 'package:pilipala/models/model_rec_video_item.dart'; import 'package:pilipala/models/video_detail_res.dart'; +/// res.data['code'] == 0 请求正常返回结果 +/// res.data['data'] 为结果 +/// 返回{'status': bool, 'data': List} +/// view层根据 status 判断渲染逻辑 class VideoHttp { + // 首页推荐视频 + static Future rcmdVideoList(data) async { + var res = await Request().get( + Api.recommendList, + data: { + 'feed_version': 'V3', + 'ps': data['ps'], + 'fresh_idx': data['fresh_idx'] + }, + ); + if (res.data['code'] == 0) { + List list = []; + for (var i in res.data['data']['item']) { + list.add(RecVideoItemModel.fromJson(i)); + } + return {'status': true, 'data': list}; + } else { + return {'status': false, 'data': []}; + } + } + + // 最热视频 + static Future hotVideoList(data) async { + var res = await Request().get( + Api.hotList, + data: { + 'pn': data['pn'], + 'ps': data['ps'], + }, + ); + if (res.data['code'] == 0) { + List list = []; + for (var i in res.data['data']['list']) { + list.add(HotVideoItemModel.fromJson(i)); + } + return {'status': true, 'data': list}; + } else { + return {'status': false, 'data': []}; + } + } + // 视频信息 标题、简介 - static Future videoDetail(data) async { - var res = await Request().get(Api.videoDetail, data: data); + static Future videoIntro(aid) async { + var res = await Request().get(Api.videoIntro, data: {'aid': aid}); VideoDetailResponse result = VideoDetailResponse.fromJson(res.data); if (result.code == 0) { return {'status': true, 'data': result.data!}; @@ -25,8 +72,17 @@ class VideoHttp { } } - static Future videoRecommend(data) async { - var res = await Request().get(Api.relatedList, data: data); - return res; + // 相关视频 + static Future relatedVideoList(aid) async { + var res = await Request().get(Api.relatedList, data: {'aid': aid}); + if (res.data['code'] == 0) { + List list = []; + for (var i in res.data['data']) { + list.add(HotVideoItemModel.fromJson(i)); + } + return {'status': true, 'data': list}; + } else { + return {'status': true, 'data': []}; + } } } diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index 20f166e6..809eda17 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -1,7 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; -import 'package:pilipala/http/api.dart'; -import 'package:pilipala/http/init.dart'; +import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/model_rec_video_item.dart'; class HomeController extends GetxController { @@ -21,22 +20,20 @@ class HomeController extends GetxController { // 获取推荐 Future queryRcmdFeed(type) async { - var res = await Request().get( - Api.recommendList, - data: {'feed_version': "V3", 'ps': count, 'fresh_idx': _currentPage}, - ); - List list = []; - for (var i in res.data['data']['item']) { - list.add(RecVideoItemModel.fromJson(i)); + var res = await VideoHttp.rcmdVideoList({ + 'ps': count, + 'fresh_idx': _currentPage, + }); + if (res['status']) { + if (type == 'init') { + videoList.value = res['data']; + } else if (type == 'onRefresh') { + videoList.insertAll(0, res['data']); + } else if (type == 'onLoad') { + videoList.addAll(res['data']); + } + _currentPage += 1; } - if (type == 'init') { - videoList.value = list; - } else if (type == 'onRefresh') { - videoList.insertAll(0, list); - } else if (type == 'onLoad') { - videoList.addAll(list); - } - _currentPage += 1; isLoadingMore = false; } diff --git a/lib/pages/hot/controller.dart b/lib/pages/hot/controller.dart index ced43a7b..3690b9e9 100644 --- a/lib/pages/hot/controller.dart +++ b/lib/pages/hot/controller.dart @@ -1,8 +1,6 @@ -import 'package:flutter/animation.dart'; -import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; -import 'package:pilipala/http/api.dart'; -import 'package:pilipala/http/init.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/model_hot_video_item.dart'; class HotController extends GetxController { @@ -21,22 +19,20 @@ class HotController extends GetxController { // 获取推荐 Future queryHotFeed(type) async { - var res = await Request().get( - Api.hotList, - data: {'pn': _currentPage, 'ps': _count}, - ); - List list = []; - for (var i in res.data['data']['list']) { - list.add(HotVideoItemModel.fromJson(i)); + var res = await VideoHttp.hotVideoList({ + 'pn': _currentPage, + 'ps': _count, + }); + if (res['status']) { + if (type == 'init') { + videoList.value = res['data']; + } else if (type == 'onRefresh') { + videoList.insertAll(0, res['data']); + } else if (type == 'onLoad') { + videoList.addAll(res['data']); + } + _currentPage += 1; } - if (type == 'init') { - videoList.value = list; - } else if (type == 'onRefresh') { - videoList.insertAll(0, list); - } else if (type == 'onLoad') { - videoList.addAll(list); - } - _currentPage += 1; isLoadingMore = false; } diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index e098880d..d3740d05 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -1,6 +1,6 @@ import 'package:get/get.dart'; -import 'package:pilipala/http/api.dart'; -import 'package:pilipala/http/init.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/pages/video/detail/controller.dart'; @@ -20,6 +20,12 @@ class VideoIntroController extends GetxController { // 视频详情 请求返回 Rx videoDetail = VideoDetailData().obs; + // 请求返回的信息 + String responseMsg = '请求异常'; + + // up主粉丝数 + Map userStat = {'follower': '-'}; + @override void onInit() { super.onInit(); @@ -36,17 +42,28 @@ class VideoIntroController extends GetxController { } } - Future queryVideoDetail() async { - var res = await Request().get(Api.videoDetail, data: { - 'aid': aid, - }); - VideoDetailResponse result = VideoDetailResponse.fromJson(res.data); - videoDetail.value = result.data!; - Get.find().tabs.value = [ - '简介', - '评论 ${result.data!.stat!.reply}' - ]; - // await Future.delayed(const Duration(seconds: 3)); - return true; + // 获取视频简介 + Future queryVideoIntro() async { + var result = await VideoHttp.videoIntro(aid); + if (result['status']) { + videoDetail.value = result['data']!; + Get.find().tabs.value = [ + '简介', + '评论 ${result['data']!.stat!.reply}' + ]; + } else { + responseMsg = result['msg']; + } + // 获取到粉丝数再返回 + await queryUserStat(); + return result; + } + + // 获取up主粉丝数 + Future queryUserStat() async { + var result = await UserHttp.userStat(videoDetail.value.owner!.mid); + if (result['status']) { + userStat = result['data']; + } } } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 41e624d4..de89e948 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/pages/video/detail/widgets/expandable_section.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/stat/danmu.dart'; @@ -43,27 +44,29 @@ class _VideoIntroPanelState extends State @override Widget build(BuildContext context) { return FutureBuilder( - future: videoIntroController.queryVideoDetail(), + future: videoIntroController.queryVideoIntro(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.data) { + if (snapshot.data['status']) { // 请求成功 // return _buildView(context, false, videoDetail); - return VideoInfo(loadingStatus: false, videoDetail: videoDetail); + return VideoInfo( + loadingStatus: false, + videoDetail: videoDetail, + videoIntroController: videoIntroController); } else { // 请求错误 - return Center( - child: IconButton( - icon: const Icon(Icons.refresh), - onPressed: () { - setState(() {}); - }, - ), + return HttpError( + errMsg: snapshot.data['msg'], + fn: () => setState(() {}), ); } } else { // return _buildView(context, true, videoDetail); - return VideoInfo(loadingStatus: true, videoDetail: videoDetail); + return VideoInfo( + loadingStatus: true, + videoDetail: videoDetail, + videoIntroController: videoIntroController); } }, ); @@ -90,8 +93,13 @@ class _VideoIntroPanelState extends State class VideoInfo extends StatefulWidget { bool loadingStatus = false; VideoDetailData? videoDetail; + VideoIntroController? videoIntroController; - VideoInfo({Key? key, required this.loadingStatus, this.videoDetail}) + VideoInfo( + {Key? key, + required this.loadingStatus, + this.videoDetail, + this.videoIntroController}) : super(key: key); @override @@ -150,19 +158,17 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ? widget.videoDetail!.owner!.name : videoItem['owner'].name), const SizedBox(height: 2), - // Text.rich( - // TextSpan( - // style: TextStyle( - // color: Theme.of(context) - // .colorScheme - // .outline, - // fontSize: 11), - // children: const [ - // TextSpan(text: '2.6万粉丝'), - // TextSpan(text: ' '), - // TextSpan(text: '2.6万粉丝'), - // ]), - // ), + Text( + widget.loadingStatus + ? '- 粉丝' + : '${Utils.numFormat(widget.videoIntroController!.userStat['follower'])}粉丝', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize, + color: Theme.of(context).colorScheme.outline), + ) ]), const Spacer(), AnimatedOpacity( diff --git a/lib/pages/video/detail/related/controller.dart b/lib/pages/video/detail/related/controller.dart index 872f8871..ed9cad3a 100644 --- a/lib/pages/video/detail/related/controller.dart +++ b/lib/pages/video/detail/related/controller.dart @@ -1,8 +1,5 @@ -import 'dart:convert'; - import 'package:get/get.dart'; import 'package:pilipala/http/video.dart'; -import 'package:pilipala/models/model_hot_video_item.dart'; class ReleatedController extends GetxController { // 视频aid @@ -10,22 +7,5 @@ class ReleatedController extends GetxController { // 推荐视频列表 List relatedVideoList = []; - Future queryVideoRecommend() async { - try { - var res = await VideoHttp.videoRecommend({'aid': aid}); - List list = []; - try { - for (var i in res.data['data']) { - list.add(HotVideoItemModel.fromJson(i)); - } - relatedVideoList = list; - } catch (err) { - return err.toString(); - } - - return res.data['data']; - } catch (err) { - return err.toString(); - } - } + Future queryRelatedVideo() => VideoHttp.relatedVideoList(aid); } diff --git a/lib/pages/video/detail/related/view.dart b/lib/pages/video/detail/related/view.dart index 319e75d4..7eca3157 100644 --- a/lib/pages/video/detail/related/view.dart +++ b/lib/pages/video/detail/related/view.dart @@ -16,22 +16,22 @@ class _RelatedVideoPanelState extends State { @override Widget build(BuildContext context) { return FutureBuilder( - future: _releatedController.queryVideoRecommend(), + future: _releatedController.queryRelatedVideo(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.data!.isNotEmpty) { + if (snapshot.data!['status']) { // 请求成功 - List videoList = _releatedController.relatedVideoList; + // List videoList = _releatedController.relatedVideoList; return SliverList( delegate: SliverChildBuilderDelegate((context, index) { - if (index == videoList.length) { + if (index == snapshot.data['data'].length) { return SizedBox(height: MediaQuery.of(context).padding.bottom); } else { return VideoCardH( - videoItem: videoList[index], + videoItem: snapshot.data['data'][index], ); } - }, childCount: videoList.length + 1)); + }, childCount: snapshot.data['data'].length + 1)); } else { // 请求错误 return const Center( From ebbb0b86a26e99ccb5d65a0347d0405dce82242e Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 21 Apr 2023 21:44:51 +0800 Subject: [PATCH 05/30] =?UTF-8?q?mod:=20=E8=AF=B7=E6=B1=82=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E7=BB=93=E6=9E=84=E3=80=81=E8=AF=84=E8=AE=BA=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 3 ++ lib/http/reply.dart | 33 +++++++++++++++++++ lib/http/user.dart | 2 +- lib/http/video.dart | 19 +++++------ lib/pages/home/controller.dart | 8 ++--- lib/pages/hot/controller.dart | 8 ++--- lib/pages/video/detail/controller.dart | 1 + .../video/detail/introduction/controller.dart | 4 +-- .../video/detail/related/controller.dart | 2 +- lib/pages/video/detail/reply/controller.dart | 17 ++++++++++ lib/pages/video/detail/reply/index.dart | 4 +++ lib/pages/video/detail/reply/view.dart | 17 ++++++++++ lib/pages/video/detail/view.dart | 6 ++-- 13 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 lib/http/reply.dart create mode 100644 lib/pages/video/detail/reply/controller.dart create mode 100644 lib/pages/video/detail/reply/index.dart create mode 100644 lib/pages/video/detail/reply/view.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 4c378780..de89bee9 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -13,4 +13,7 @@ class Api { // 用户(被)关注数、投稿数 // https://api.bilibili.com/x/relation/stat?vmid=697166795 static const String userStat = '/x/relation/stat'; + + // 评论列表 + static const String replyList = '/x/v2/reply'; } diff --git a/lib/http/reply.dart b/lib/http/reply.dart new file mode 100644 index 00000000..1762ee4e --- /dev/null +++ b/lib/http/reply.dart @@ -0,0 +1,33 @@ +import 'package:pilipala/http/api.dart'; +import 'package:pilipala/http/init.dart'; + +class ReplyHttp { + static Future replyList({ + required String oid, + required int pageNum, + required int type, + int sort = 1, + }) async { + var res = await Request().get(Api.replyList, data: { + 'oid': oid, + 'pn': pageNum, + 'type': type, + 'sort': 1, + }); + print(res); + if (res.data['code'] == 0) { + } else { + Map errMap = { + -400: '请求错误', + -404: '无此项', + 12002: '评论区已关闭', + 12009: '评论主体的type不合法', + }; + return { + 'status': false, + 'date': [], + 'msg': errMap[res.data['code']] ?? '请求异常', + }; + } + } +} diff --git a/lib/http/user.dart b/lib/http/user.dart index 327f0ba5..ce3d4c43 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -2,7 +2,7 @@ import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; class UserHttp { - static Future userStat(mid) async { + static Future userStat({required int mid}) async { var res = await Request().get(Api.userStat, data: {'vmid': mid}); if (res.data['code'] == 0) { return {'status': true, 'data': res.data['data']}; diff --git a/lib/http/video.dart b/lib/http/video.dart index b263f3ba..61f2770e 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -10,13 +10,13 @@ import 'package:pilipala/models/video_detail_res.dart'; /// view层根据 status 判断渲染逻辑 class VideoHttp { // 首页推荐视频 - static Future rcmdVideoList(data) async { + static Future rcmdVideoList({required int ps, required int freshIdx}) async { var res = await Request().get( Api.recommendList, data: { 'feed_version': 'V3', - 'ps': data['ps'], - 'fresh_idx': data['fresh_idx'] + 'ps': ps, + 'fresh_idx': freshIdx, }, ); if (res.data['code'] == 0) { @@ -31,13 +31,10 @@ class VideoHttp { } // 最热视频 - static Future hotVideoList(data) async { + static Future hotVideoList({required int pn, required int ps}) async { var res = await Request().get( Api.hotList, - data: { - 'pn': data['pn'], - 'ps': data['ps'], - }, + data: {'pn': pn, 'ps': ps}, ); if (res.data['code'] == 0) { List list = []; @@ -51,7 +48,7 @@ class VideoHttp { } // 视频信息 标题、简介 - static Future videoIntro(aid) async { + static Future videoIntro({required String aid}) async { var res = await Request().get(Api.videoIntro, data: {'aid': aid}); VideoDetailResponse result = VideoDetailResponse.fromJson(res.data); if (result.code == 0) { @@ -67,13 +64,13 @@ class VideoHttp { return { 'status': false, 'data': null, - 'msg': errMap[result.code] ?? '请求异常' + 'msg': errMap[result.code] ?? '请求异常', }; } } // 相关视频 - static Future relatedVideoList(aid) async { + static Future relatedVideoList({required String aid}) async { var res = await Request().get(Api.relatedList, data: {'aid': aid}); if (res.data['code'] == 0) { List list = []; diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index 809eda17..e96d8b60 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -20,10 +20,10 @@ class HomeController extends GetxController { // 获取推荐 Future queryRcmdFeed(type) async { - var res = await VideoHttp.rcmdVideoList({ - 'ps': count, - 'fresh_idx': _currentPage, - }); + var res = await VideoHttp.rcmdVideoList( + ps: count, + freshIdx: _currentPage, + ); if (res['status']) { if (type == 'init') { videoList.value = res['data']; diff --git a/lib/pages/hot/controller.dart b/lib/pages/hot/controller.dart index 3690b9e9..0620ecf4 100644 --- a/lib/pages/hot/controller.dart +++ b/lib/pages/hot/controller.dart @@ -19,10 +19,10 @@ class HotController extends GetxController { // 获取推荐 Future queryHotFeed(type) async { - var res = await VideoHttp.hotVideoList({ - 'pn': _currentPage, - 'ps': _count, - }); + var res = await VideoHttp.hotVideoList( + pn: _currentPage, + ps: _count, + ); if (res['status']) { if (type == 'init') { videoList.value = res['data']; diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 89142e01..744c6c74 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; class VideoDetailController extends GetxController { + int tabInitialIndex = 0; // tabs RxList tabs = ['简介', '评论'].obs; diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index d3740d05..4c1b7522 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -44,7 +44,7 @@ class VideoIntroController extends GetxController { // 获取视频简介 Future queryVideoIntro() async { - var result = await VideoHttp.videoIntro(aid); + var result = await VideoHttp.videoIntro(aid: aid); if (result['status']) { videoDetail.value = result['data']!; Get.find().tabs.value = [ @@ -61,7 +61,7 @@ class VideoIntroController extends GetxController { // 获取up主粉丝数 Future queryUserStat() async { - var result = await UserHttp.userStat(videoDetail.value.owner!.mid); + var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!); if (result['status']) { userStat = result['data']; } diff --git a/lib/pages/video/detail/related/controller.dart b/lib/pages/video/detail/related/controller.dart index ed9cad3a..cb9081dc 100644 --- a/lib/pages/video/detail/related/controller.dart +++ b/lib/pages/video/detail/related/controller.dart @@ -7,5 +7,5 @@ class ReleatedController extends GetxController { // 推荐视频列表 List relatedVideoList = []; - Future queryRelatedVideo() => VideoHttp.relatedVideoList(aid); + Future queryRelatedVideo() => VideoHttp.relatedVideoList(aid: aid); } diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart new file mode 100644 index 00000000..d18a74b2 --- /dev/null +++ b/lib/pages/video/detail/reply/controller.dart @@ -0,0 +1,17 @@ +import 'package:get/get.dart'; +import 'package:pilipala/http/reply.dart'; + +class VideoReplyController extends GetxController { + // 视频aid + String aid = Get.parameters['aid']!; + + @override + void onInit() { + super.onInit(); + queryReplyList(); + } + + Future queryReplyList() async { + var res = await ReplyHttp.replyList(oid: aid, pageNum: 1, type: 1); + } +} diff --git a/lib/pages/video/detail/reply/index.dart b/lib/pages/video/detail/reply/index.dart new file mode 100644 index 00000000..f3d25ba4 --- /dev/null +++ b/lib/pages/video/detail/reply/index.dart @@ -0,0 +1,4 @@ +library video_reply_panel; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart new file mode 100644 index 00000000..13661065 --- /dev/null +++ b/lib/pages/video/detail/reply/view.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class VideoReplyPanel extends StatefulWidget { + const VideoReplyPanel({super.key}); + + @override + State createState() => _VideoReplyPanelState(); +} + +class _VideoReplyPanelState extends State { + @override + Widget build(BuildContext context) { + return const SliverToBoxAdapter( + child: Text('评论'), + ); + } +} diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index eb088f19..78beb58b 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/introduction/index.dart'; import 'package:pilipala/pages/video/detail/related/index.dart'; @@ -19,6 +20,7 @@ class _VideoDetailPageState extends State { @override Widget build(BuildContext context) { return DefaultTabController( + initialIndex: videoDetailController.tabInitialIndex, length: videoDetailController.tabs.length, // tab的数量. child: SafeArea( top: false, @@ -137,9 +139,7 @@ class _VideoDetailPageState extends State { handle: NestedScrollView.sliverOverlapAbsorberHandleFor( context), ), - const SliverToBoxAdapter( - child: Text('评论'), - ) + const VideoReplyPanel() ], ); }) From d668da67b9d5679628e1fc3cfe392d209e2965f8 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 21 Apr 2023 23:32:24 +0800 Subject: [PATCH 06/30] =?UTF-8?q?mod:=20=E6=A0=B7=E5=BC=8F=E3=80=81?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/widgets/video_card_h.dart | 202 ++++++++++++------ lib/pages/home/view.dart | 2 +- lib/pages/video/detail/introduction/view.dart | 187 ++++++---------- lib/pages/video/detail/view.dart | 13 +- 4 files changed, 214 insertions(+), 190 deletions(-) diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index 5156a191..b14b6911 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -15,79 +15,147 @@ class VideoCardH extends StatelessWidget { Widget build(BuildContext context) { int aid = videoItem.aid; String heroTag = Utils.makeHeroTag(aid); - return Material( - child: Ink( - child: InkWell( - onTap: () async { - await Future.delayed(const Duration(milliseconds: 200)); - Get.toNamed('/video?aid=$aid', - arguments: {'videoItem': videoItem, 'heroTag': heroTag}); - }, - child: Container( + return InkWell( + onTap: () async { + await Future.delayed(const Duration(milliseconds: 200)); + Get.toNamed('/video?aid=$aid', + arguments: {'videoItem': videoItem, 'heroTag': heroTag}); + }, + child: Column( + children: [ + Padding( padding: const EdgeInsets.fromLTRB( - StyleString.cardSpace, 5, StyleString.cardSpace, 5), - child: LayoutBuilder(builder: (context, boxConstraints) { - double width = - (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; - return SizedBox( - height: width / StyleString.aspectRatio, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AspectRatio( - aspectRatio: StyleString.aspectRatio, - // child: ClipRRect( - // borderRadius: StyleString.mdRadius, - child: LayoutBuilder( - builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - double PR = MediaQuery.of(context).devicePixelRatio; - return Stack( - children: [ - Hero( - tag: heroTag, - child: NetworkImgLayer( - // src: videoItem['pic'] + - // '@${(maxWidth * 2).toInt()}w', - src: videoItem.pic + '@.webp', - width: maxWidth, - height: maxHeight, - ), - ), - // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), - Positioned( - right: 4, - bottom: 4, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 1, horizontal: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Colors.black54.withOpacity(0.4)), - child: Text( - Utils.timeFormat(videoItem.duration!), - style: const TextStyle( - fontSize: 11, color: Colors.white), + StyleString.cardSpace, 7, StyleString.cardSpace, 7), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double width = + (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; + return SizedBox( + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = MediaQuery.of(context).devicePixelRatio; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + // src: videoItem['pic'] + + // '@${(maxWidth * 2).toInt()}w', + src: videoItem.pic + '@.webp', + width: maxWidth, + height: maxHeight, ), ), - ) - ], - ); - }, + // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), + Positioned( + right: 4, + bottom: 4, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 1, horizontal: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.black54.withOpacity(0.4)), + child: Text( + Utils.timeFormat(videoItem.duration!), + style: const TextStyle( + fontSize: 11, color: Colors.white), + ), + ), + ) + ], + ); + }, + ), ), - // ), - ), - VideoContent(videoItem: videoItem) - ], - ), - ); - }), - // height: 124, + VideoContent(videoItem: videoItem) + ], + ), + ); + }, + ), ), - ), + Divider( + height: 1, + indent: 8, + endIndent: 12, + color: Theme.of(context).dividerColor.withOpacity(0.08), + ) + ], ), + // Container( + // padding: const EdgeInsets.fromLTRB( + // StyleString.cardSpace, 5, StyleString.cardSpace, 5), + // child: LayoutBuilder( + // builder: (context, boxConstraints) { + // double width = + // (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; + // return SizedBox( + // height: width / StyleString.aspectRatio, + // child: Row( + // mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // AspectRatio( + // aspectRatio: StyleString.aspectRatio, + // // child: ClipRRect( + // // borderRadius: StyleString.mdRadius, + // child: LayoutBuilder( + // builder: (context, boxConstraints) { + // double maxWidth = boxConstraints.maxWidth; + // double maxHeight = boxConstraints.maxHeight; + // double PR = MediaQuery.of(context).devicePixelRatio; + // return Stack( + // children: [ + // Hero( + // tag: heroTag, + // child: NetworkImgLayer( + // // src: videoItem['pic'] + + // // '@${(maxWidth * 2).toInt()}w', + // src: videoItem.pic + '@.webp', + // width: maxWidth, + // height: maxHeight, + // ), + // ), + // // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), + // Positioned( + // right: 4, + // bottom: 4, + // child: Container( + // padding: const EdgeInsets.symmetric( + // vertical: 1, horizontal: 6), + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(4), + // color: Colors.black54.withOpacity(0.4)), + // child: Text( + // Utils.timeFormat(videoItem.duration!), + // style: const TextStyle( + // fontSize: 11, color: Colors.white), + // ), + // ), + // ) + // ], + // ); + // }, + // ), + // // ), + // ), + // VideoContent(videoItem: videoItem) + // ], + // ), + // ); + // }, + // ), + // ), ); } } diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index fba2f82c..1adb482b 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -80,7 +80,7 @@ class _HomePageState extends State mainAxisExtent: MediaQuery.of(context).size.width / _homeController.crossAxisCount / StyleString.aspectRatio + - 72), + 70), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return videoList.isNotEmpty diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index de89e948..66b577fc 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -62,7 +62,6 @@ class _VideoIntroPanelState extends State ); } } else { - // return _buildView(context, true, videoDetail); return VideoInfo( loadingStatus: true, videoDetail: videoDetail, @@ -71,23 +70,6 @@ class _VideoIntroPanelState extends State }, ); } - - Widget _buildView(context, loadingStatus, videoDetail) { - // return CustomScrollView( - // key: const PageStorageKey('简介'), - // slivers: [ - // SliverOverlapInjector( - // handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)), - // VideoInfo(loadingStatus: loadingStatus, videoDetail: videoDetail), - // SliverToBoxAdapter( - // child: - // Divider(color: Theme.of(context).dividerColor.withOpacity(0.1)), - // ), - // const RecommendList() - // ], - // ); - return VideoInfo(loadingStatus: loadingStatus, videoDetail: videoDetail); - } } class VideoInfo extends StatefulWidget { @@ -183,7 +165,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { const SizedBox(width: 4), ], ), - const SizedBox(height: 18), + const SizedBox(height: 13), // 标题 超过两行收起 // Container( // color: Colors.blue[50], @@ -231,73 +213,81 @@ class _VideoInfoState extends State with TickerProviderStateMixin { // ), // ], // ), - SizedBox( - width: double.infinity, - child: Text( - !widget.loadingStatus - ? widget.videoDetail!.title - : videoItem['title'], - // style: Theme.of(context).textTheme.titleMedium, - // maxLines: 2, - ), + + Text( + !widget.loadingStatus + ? widget.videoDetail!.title + : videoItem['title'], ), // const SizedBox(height: 5), // 播放量、评论、日期 - Row( - children: [ - const SizedBox(width: 2), - StatView( - theme: 'gray', - view: !widget.loadingStatus - ? widget.videoDetail!.stat!.view - : videoItem['stat'].view, - size: 'medium', - ), - const SizedBox(width: 10), - StatDanMu( - theme: 'gray', - danmu: !widget.loadingStatus - ? widget.videoDetail!.stat!.danmaku - : videoItem['stat'].danmaku, - size: 'medium', - ), - const SizedBox(width: 10), - Text( - Utils.dateFormat( - !widget.loadingStatus - ? widget.videoDetail!.pubdate - : videoItem['pubdate'], - formatType: 'detail'), - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.outline), - ), - const Spacer(), - RotationTransition( - turns: _manualAnimation!, - child: IconButton( - onPressed: () { - /// 获取动画当前的值 - var value = _manualController!.value; - /// 0.5代表 180弧度 - if (value == 0) { - _manualController!.animateTo(0.5); - } else { - _manualController!.animateTo(0); - } - setState(() { - isExpand = !isExpand; - }); - }, - icon: Icon( - Icons.expand_less, - color: Theme.of(context).colorScheme.outline, + InkWell( + splashColor: Colors.transparent, + onTap: () { + _manualController!.animateTo(isExpand ? 0 : 0.5); + setState(() { + isExpand = !isExpand; + }); + }, + child: Row( + children: [ + const SizedBox(width: 2), + StatView( + theme: 'gray', + view: !widget.loadingStatus + ? widget.videoDetail!.stat!.view + : videoItem['stat'].view, + size: 'medium', + ), + const SizedBox(width: 10), + StatDanMu( + theme: 'gray', + danmu: !widget.loadingStatus + ? widget.videoDetail!.stat!.danmaku + : videoItem['stat'].danmaku, + size: 'medium', + ), + const SizedBox(width: 10), + Text( + Utils.dateFormat( + !widget.loadingStatus + ? widget.videoDetail!.pubdate + : videoItem['pubdate'], + formatType: 'detail'), + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.outline), + ), + const Spacer(), + RotationTransition( + turns: _manualAnimation!, + child: SizedBox( + width: 35, + height: 35, + child: IconButton( + padding: const EdgeInsets.all(2.0), + onPressed: () { + /// 0.5代表 180弧度 + _manualController! + .animateTo(isExpand ? 0 : 0.5); + setState(() { + isExpand = !isExpand; + }); + }, + icon: Icon( + Icons.expand_less, + size: 22, + color: Theme.of(context).colorScheme.outline, + ), + ), ), ), - ), - ], + const SizedBox(width: 10), + ], + ), ), + // const SizedBox(height: 5), // 简介 默认收起 if (!widget.loadingStatus) @@ -342,8 +332,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { // 喜欢 投币 分享 Widget _actionGrid(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { - return Container( - color: Colors.black12, + return SizedBox( height: constraints.maxWidth / 5, child: GridView.count( primary: false, @@ -444,39 +433,3 @@ class ActionItem extends StatelessWidget { )); } } - -class RecommendList extends StatelessWidget { - const RecommendList({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return Material( - child: InkWell( - onTap: () {}, - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 10, 20, 10), - child: Text( - '$index」 求推荐一些高质量的系统地介绍 ChatGPT 及相关技术的视频、文章或者书', - style: Theme.of(context) - .textTheme - .titleSmall! - .copyWith(height: 1.6), - ), - ), - ), - ); - }, childCount: 50), - ); - } -} - -class ActionGrid extends StatelessWidget { - const ActionGrid({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 78beb58b..d8cf35df 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -120,13 +120,16 @@ class _VideoDetailPageState extends State { context), ), const VideoIntroPanel(), - SliverToBoxAdapter( - child: Divider( - color: - Theme.of(context).dividerColor.withOpacity(0.1), + SliverPadding( + padding: const EdgeInsets.only(top: 8, bottom: 5), + sliver: SliverToBoxAdapter( + child: Divider( + height: 1, + color: + Theme.of(context).dividerColor.withOpacity(0.1), + ), ), ), - const SliverPadding(padding: EdgeInsets.only(bottom: 5)), const RelatedVideoPanel(), ], ); From 572d2a4139219d152f81aaaae34395c26b8cafbe Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 21 Apr 2023 23:56:28 +0800 Subject: [PATCH 07/30] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8heroTag=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E5=90=8C=E4=B8=80=E9=A1=B5=E9=9D=A2controller?= =?UTF-8?q?=E4=B8=8D=E5=88=B7=E6=96=B0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/video/detail/introduction/controller.dart | 7 +++---- lib/pages/video/detail/introduction/view.dart | 2 +- lib/pages/video/detail/related/view.dart | 3 ++- lib/pages/video/detail/view.dart | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 4c1b7522..191a6d43 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -47,10 +47,9 @@ class VideoIntroController extends GetxController { var result = await VideoHttp.videoIntro(aid: aid); if (result['status']) { videoDetail.value = result['data']!; - Get.find().tabs.value = [ - '简介', - '评论 ${result['data']!.stat!.reply}' - ]; + Get.find(tag: Get.arguments['heroTag']) + .tabs + .value = ['简介', '评论 ${result['data']!.stat!.reply}']; } else { responseMsg = result['msg']; } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 66b577fc..52a6efb0 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -20,7 +20,7 @@ class VideoIntroPanel extends StatefulWidget { class _VideoIntroPanelState extends State with AutomaticKeepAliveClientMixin { final VideoIntroController videoIntroController = - Get.put(VideoIntroController()); + Get.put(VideoIntroController(), tag: Get.arguments['heroTag']); VideoDetailData? videoDetail; // 添加页面缓存 diff --git a/lib/pages/video/detail/related/view.dart b/lib/pages/video/detail/related/view.dart index 7eca3157..32492d13 100644 --- a/lib/pages/video/detail/related/view.dart +++ b/lib/pages/video/detail/related/view.dart @@ -11,7 +11,8 @@ class RelatedVideoPanel extends StatefulWidget { } class _RelatedVideoPanelState extends State { - final ReleatedController _releatedController = Get.put(ReleatedController()); + final ReleatedController _releatedController = + Get.put(ReleatedController(), tag: Get.arguments['heroTag']); @override Widget build(BuildContext context) { diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index d8cf35df..ed2e1047 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -15,7 +15,7 @@ class VideoDetailPage extends StatefulWidget { class _VideoDetailPageState extends State { final VideoDetailController videoDetailController = - Get.put(VideoDetailController()); + Get.put(VideoDetailController(), tag: Get.arguments['heroTag']); @override Widget build(BuildContext context) { From 2fd1cc422b5d68506f3ef254eb38f1f2ee263163 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 22 Apr 2023 00:22:36 +0800 Subject: [PATCH 08/30] =?UTF-8?q?mod:=20=E5=A2=9E=E5=8A=A0=E9=AA=A8?= =?UTF-8?q?=E6=9E=B6=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/skeleton/video_card_h.dart | 119 ++++++++++++++++++++++ lib/common/skeleton/video_card_v.dart | 4 +- lib/common/widgets/network_img_layer.dart | 4 +- lib/common/widgets/video_card_h.dart | 64 ------------ lib/pages/video/detail/related/view.dart | 9 +- 5 files changed, 130 insertions(+), 70 deletions(-) create mode 100644 lib/common/skeleton/video_card_h.dart diff --git a/lib/common/skeleton/video_card_h.dart b/lib/common/skeleton/video_card_h.dart new file mode 100644 index 00000000..dc5e9c32 --- /dev/null +++ b/lib/common/skeleton/video_card_h.dart @@ -0,0 +1,119 @@ +import 'package:pilipala/common/constants.dart'; +import 'package:flutter/material.dart'; +import 'skeleton.dart'; + +class VideoCardHSkeleton extends StatefulWidget { + const VideoCardHSkeleton({super.key}); + + @override + State createState() => _VideoCardHSkeletonState(); +} + +class _VideoCardHSkeletonState extends State { + @override + Widget build(BuildContext context) { + return Skeleton( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + StyleString.cardSpace, 7, StyleString.cardSpace, 7), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double width = + (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; + return SizedBox( + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = MediaQuery.of(context).devicePixelRatio; + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + borderRadius: BorderRadius.circular( + StyleString.imgRadius.x), + ), + ); + }, + ), + ), + // VideoContent(videoItem: videoItem) + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 4, 6, 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + width: 200, + height: 13, + margin: const EdgeInsets.only(bottom: 5), + ), + Container( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + width: 150, + height: 13, + ), + const Spacer(), + Container( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + width: 100, + height: 13, + margin: const EdgeInsets.only(bottom: 5), + ), + Row( + children: [ + Container( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + width: 40, + height: 13, + margin: const EdgeInsets.only(right: 8), + ), + Container( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + width: 40, + height: 13, + ), + ], + ) + ], + ), + )), + ], + ), + ); + }, + ), + ), + Divider( + height: 1, + indent: 8, + endIndent: 12, + color: Theme.of(context).dividerColor.withOpacity(0.08), + ) + ], + ), + ); + } +} diff --git a/lib/common/skeleton/video_card_v.dart b/lib/common/skeleton/video_card_v.dart index 1c9ef23d..4f83501e 100644 --- a/lib/common/skeleton/video_card_v.dart +++ b/lib/common/skeleton/video_card_v.dart @@ -49,15 +49,15 @@ class VideoCardVSkeleton extends StatelessWidget { Container( width: 200, height: 13, + margin: const EdgeInsets.only(bottom: 5), color: Theme.of(context).colorScheme.background, ), - const SizedBox(height: 5), Container( width: 150, height: 13, + margin: const EdgeInsets.only(bottom: 12), color: Theme.of(context).colorScheme.background, ), - const SizedBox(height: 12), Container( width: 80, height: 13, diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index abaa369d..e8249b80 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:pilipala/common/constants.dart'; class NetworkImgLayer extends StatelessWidget { final String? src; @@ -29,7 +30,8 @@ class NetworkImgLayer extends StatelessWidget { // double pr = 2; return src != '' ? ClipRRect( - borderRadius: BorderRadius.circular(type == 'avatar' ? 50 : 4), + borderRadius: BorderRadius.circular( + type == 'avatar' ? 50 : StyleString.imgRadius.x), child: CachedNetworkImage( imageUrl: src!, width: width ?? double.infinity, diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index b14b6911..6e881831 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -92,70 +92,6 @@ class VideoCardH extends StatelessWidget { ) ], ), - // Container( - // padding: const EdgeInsets.fromLTRB( - // StyleString.cardSpace, 5, StyleString.cardSpace, 5), - // child: LayoutBuilder( - // builder: (context, boxConstraints) { - // double width = - // (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; - // return SizedBox( - // height: width / StyleString.aspectRatio, - // child: Row( - // mainAxisAlignment: MainAxisAlignment.start, - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // AspectRatio( - // aspectRatio: StyleString.aspectRatio, - // // child: ClipRRect( - // // borderRadius: StyleString.mdRadius, - // child: LayoutBuilder( - // builder: (context, boxConstraints) { - // double maxWidth = boxConstraints.maxWidth; - // double maxHeight = boxConstraints.maxHeight; - // double PR = MediaQuery.of(context).devicePixelRatio; - // return Stack( - // children: [ - // Hero( - // tag: heroTag, - // child: NetworkImgLayer( - // // src: videoItem['pic'] + - // // '@${(maxWidth * 2).toInt()}w', - // src: videoItem.pic + '@.webp', - // width: maxWidth, - // height: maxHeight, - // ), - // ), - // // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), - // Positioned( - // right: 4, - // bottom: 4, - // child: Container( - // padding: const EdgeInsets.symmetric( - // vertical: 1, horizontal: 6), - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(4), - // color: Colors.black54.withOpacity(0.4)), - // child: Text( - // Utils.timeFormat(videoItem.duration!), - // style: const TextStyle( - // fontSize: 11, color: Colors.white), - // ), - // ), - // ) - // ], - // ); - // }, - // ), - // // ), - // ), - // VideoContent(videoItem: videoItem) - // ], - // ), - // ); - // }, - // ), - // ), ); } } diff --git a/lib/pages/video/detail/related/view.dart b/lib/pages/video/detail/related/view.dart index 32492d13..4034cdfb 100644 --- a/lib/pages/video/detail/related/view.dart +++ b/lib/pages/video/detail/related/view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; import './controller.dart'; @@ -22,7 +23,6 @@ class _RelatedVideoPanelState extends State { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.data!['status']) { // 请求成功 - // List videoList = _releatedController.relatedVideoList; return SliverList( delegate: SliverChildBuilderDelegate((context, index) { if (index == snapshot.data['data'].length) { @@ -40,8 +40,11 @@ class _RelatedVideoPanelState extends State { ); } } else { - return const SliverToBoxAdapter( - child: Text('请求中'), + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoCardHSkeleton(); + }, childCount: 5), ); } }, From a0441aa5899b4b7ba59d82a1125e5a7e7f595a8b Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 23 Apr 2023 15:50:51 +0800 Subject: [PATCH 09/30] =?UTF-8?q?=E8=AF=84=E8=AE=BA=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/images/lv/lv0.png | Bin 0 -> 514 bytes assets/images/lv/lv1.png | Bin 0 -> 524 bytes assets/images/lv/lv2.png | Bin 0 -> 518 bytes assets/images/lv/lv3.png | Bin 0 -> 541 bytes assets/images/lv/lv4.png | Bin 0 -> 498 bytes assets/images/lv/lv5.png | Bin 0 -> 539 bytes assets/images/lv/lv6.png | Bin 0 -> 517 bytes lib/common/widgets/reply_item.dart | 178 +++++++++++++++++++ lib/http/reply.dart | 5 +- lib/models/video/reply/config.dart | 17 ++ lib/models/video/reply/content.dart | 23 +++ lib/models/video/reply/data.dart | 34 ++++ lib/models/video/reply/item.dart | 125 +++++++++++++ lib/models/video/reply/member.dart | 55 ++++++ lib/models/video/reply/page.dart | 20 +++ lib/models/video/reply/top_replies.dart | 1 + lib/models/video/reply/upper.dart | 18 ++ lib/pages/video/detail/reply/controller.dart | 6 + lib/pages/video/detail/reply/view.dart | 45 ++++- pubspec.yaml | 1 + 20 files changed, 525 insertions(+), 3 deletions(-) create mode 100644 assets/images/lv/lv0.png create mode 100644 assets/images/lv/lv1.png create mode 100644 assets/images/lv/lv2.png create mode 100644 assets/images/lv/lv3.png create mode 100644 assets/images/lv/lv4.png create mode 100644 assets/images/lv/lv5.png create mode 100644 assets/images/lv/lv6.png create mode 100644 lib/common/widgets/reply_item.dart create mode 100644 lib/models/video/reply/config.dart create mode 100644 lib/models/video/reply/content.dart create mode 100644 lib/models/video/reply/data.dart create mode 100644 lib/models/video/reply/item.dart create mode 100644 lib/models/video/reply/member.dart create mode 100644 lib/models/video/reply/page.dart create mode 100644 lib/models/video/reply/top_replies.dart create mode 100644 lib/models/video/reply/upper.dart diff --git a/assets/images/lv/lv0.png b/assets/images/lv/lv0.png new file mode 100644 index 0000000000000000000000000000000000000000..3b9999cf730585d4184b92ca9ffd87a1cd4bc024 GIT binary patch literal 514 zcmV+d0{#7oP);Nu2kPew*oUc(brv1yaRN2+n{V<90K8g!HY`%D|L0iXF{v$nz$cUM;S%e0Ds7Vf69Sglx<9yD{vOHv$^8zvWkc}UT;>? z7kDvso~-#gpfR_=+0~?Osa5NnuLHUZ+nE*qn$Q4){eG{hMdgs}q7+!kiU35WCe>_t zUIpkKal^6n-k+6Y7I=ShyLIsJ4!NTt@mGe^JwXHB4{jl%3uVkkUH||907*qoM6N<$ Ef&;MT>Hq)$ literal 0 HcmV?d00001 diff --git a/assets/images/lv/lv1.png b/assets/images/lv/lv1.png new file mode 100644 index 0000000000000000000000000000000000000000..9973e4e7e26d2354e4b6eb18e2ef47f381e70ab3 GIT binary patch literal 524 zcmV+n0`vWeP)_Jh<1iAI49-AcZs9Rb_RBf8vh?0QMHpRBHte$%?v4 zMh6~~EYu%;){7`;PL?dWcI49ywOB$o0&3Jky~rk*;y+qz(WF z%gp)!o-G3|l}}$Kql@L59>6Is6ZqV@{b%rGg50TBRaIU3!-+s=B$IDzma#d<`RND% O0000r1wA}>f3=TAf|siM4nUGTKz{+j8&t2=*Cr_?)}<4^EMHmgOkoZC%H0A;B#C zUIIUjykMD=u2Xd*a_{1{D1k46-cipPoAEkcmWQJPc(Q;3-jNike`wtXM5jKCw80}U zh!v{qLIu7;=+d~#V!T=E*T7`5uL8P?7-isEr>z1&E9VWNjZcAPG4?)xzIM4B&}{}1 zMSz)rb%lT3KYdG+auK5p{MnB`I{2}J+`m?41MhYOIt4!d018W?4)GF+*#H0l07*qo IM6N<$g6IL`%>V!Z literal 0 HcmV?d00001 diff --git a/assets/images/lv/lv3.png b/assets/images/lv/lv3.png new file mode 100644 index 0000000000000000000000000000000000000000..54e08d2d733d5569f51a802308a521fc02e65ce3 GIT binary patch literal 541 zcmV+&0^+y=}C4!WxV zz^ozLx5EyDPa3rCJ3OrB4KLI+a5!O_Ede~i4s8JB2J;pGMrYsvs3p*kjCg@zq1*y^ zD}Eco9_>vhGUEN3TL!)ZumX@6tn~%81pH5rD?roJ z9}d&(0AQ_vri^$wA0J7MuyvSb6WttV#9LMcj|!7>?z}Ge{vxbY;ki9Q7uJ@Y-47Ku zRSIrXj@PPx@V?2h%-_VYfw3C==d|fts7HOxaksBI9`ReGZUYlv6Fjfc)(F6Bh4F(n zLwzE+9NbGPbkJl!-X{1sOta|)d?bKB>(L9kwYh7Kn#XIP~Y2Zzy_fcgaPiM8-z^I3{a#>w?HyM z=?1CF0AzwDn~(|U!_hQ_y8u#1kAPo5$g=$5-wM#$XyHnPQeE&^$~mozsb$5ucj80vEn264T6V5?eTf>C7E4f26PR+ zE7s7dVO`ynp4ELYO>LF>tn}71AkggK2Ov1EtG^cXx5twxu7TG>D;bCV1y{Xjg8w)J zonG@mq)Cs*MN%ePY7(B4L?C2Xf`8H|cKUq7p25Ez?Y%&f8i~GzKqnX1M0l& zEr2$G`z?o=-@50o0eza6CPyd#d`YPuzpfiE3i=+`*}j1Fib2krF1L7QUyta1bIn%FHq`vQn`n-aje z4sanC2I*`Ce00y%g^b%A<6c)|d`4jPwd)EYr>eoA6wp})xuqH=G35^ z^{T}>Lj6*nw;bQVXHz}f&O`->c<{P1xGxzF9u)0r*130Z^^FH0>cZy|_;!!}quAw8 zFEsFS=w|3Rx(psD;O`aC&r9pvQqB2SCh3eW@NG!uw}J7}23qHrrnc+GQvI=xuNePI z27JZZ#9)++t&g6X- createState() => _ReplyItemState(); +} + +class _ReplyItemState extends State { + ReplyItemModel? replyItem; + bool isUp = false; + + @override + void initState() { + super.initState(); + replyItem = widget.replyItem; + isUp = widget.isUp!; + } + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () {}, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 8, 4), + child: content(context), + ), + Divider( + height: 1, + color: Theme.of(context).dividerColor.withOpacity(0.08), + ) + ], + ), + ); + } + + Widget lfAvtar() { + return Container( + margin: const EdgeInsets.only(top: 5), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary.withOpacity(0.03)), + ), + child: NetworkImgLayer( + src: replyItem!.member!.avatar, + width: 34, + height: 34, + type: 'avatar', + ), + ); + } + + Widget content(context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 头像、昵称 + Row( + // 两端对齐 + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + // onTap: () => + // Get.toNamed('/member/${reply.userName}', parameters: { + // 'memberAvatar': reply.avatar, + // 'heroTag': reply.userName + heroTag, + // }), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + lfAvtar(), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + replyItem!.member!.uname!, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith( + color: isUp + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + const SizedBox(width: 6), + Image.asset( + 'assets/images/lv/lv${replyItem!.member!.level}.png', + height: 13, + ), + ], + ), + Text( + Utils.dateFormat(replyItem!.ctime), + style: Theme.of(context).textTheme.labelSmall!.copyWith( + color: Theme.of(context).colorScheme.outline), + ), + ], + ) + ], + ), + ), + // SizedBox( + // width: 35, + // height: 35, + // child: IconButton( + // padding: const EdgeInsets.all(2.0), + // icon: const Icon(Icons.more_horiz_outlined, size: 18.0), + // onPressed: () {}, + // ), + // ) + ], + ), + // title + Container( + margin: const EdgeInsets.only(top: 6, left: 45, right: 8), + child: SelectionArea( + child: Text( + replyItem!.content!.message!, + style: const TextStyle(height: 1.8), + ), + ), + ), + bottonAction(), + ], + ); + } + + // 感谢、回复、复制 + Widget bottonAction() { + var color = Theme.of(context).colorScheme.outline; + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // const SizedBox(width: 42), + SizedBox( + height: 35, + child: TextButton( + child: Row( + children: [ + Icon( + Icons.thumb_up_alt_outlined, + size: 16, + color: color, + ), + const SizedBox(width: 4), + Text( + replyItem!.like.toString(), + style: TextStyle( + color: color, + fontSize: + Theme.of(context).textTheme.labelSmall!.fontSize), + ), + ], + ), + onPressed: () {}, + ), + ), + const SizedBox(width: 5) + ], + ); + } +} diff --git a/lib/http/reply.dart b/lib/http/reply.dart index 1762ee4e..88000996 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -14,8 +14,11 @@ class ReplyHttp { 'type': type, 'sort': 1, }); - print(res); if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; } else { Map errMap = { -400: '请求错误', diff --git a/lib/models/video/reply/config.dart b/lib/models/video/reply/config.dart new file mode 100644 index 00000000..90574f7c --- /dev/null +++ b/lib/models/video/reply/config.dart @@ -0,0 +1,17 @@ +class ReplyConfig { + ReplyConfig({ + this.showtopic, + this.showUpFlag, + this.readOnly, + }); + + int? showtopic; + bool? showUpFlag; + bool? readOnly; + + ReplyConfig.fromJson(Map json) { + showtopic = json['showtopic']; + showUpFlag = json['show_up_flag']; + readOnly = json['read_only']; + } +} diff --git a/lib/models/video/reply/content.dart b/lib/models/video/reply/content.dart new file mode 100644 index 00000000..f924d3f2 --- /dev/null +++ b/lib/models/video/reply/content.dart @@ -0,0 +1,23 @@ +class ReplyContent { + ReplyContent({ + this.message, + this.atNameToMid, // @的用户的mid + this.memebers, // 被@的用户List 如果有的话 + this.emote, // 表情包 如果有的话 + this.jumpUrl, + }); + + String? message; + Map? atNameToMid; + List? memebers; + Map? emote; + Map? jumpUrl; + + ReplyContent.fromJson(Map json) { + message = json['message']; + atNameToMid = json['at_name_to_mid']; + memebers = json['memebers']; + emote = json['emote']; + jumpUrl = json['jumpUrl']; + } +} diff --git a/lib/models/video/reply/data.dart b/lib/models/video/reply/data.dart new file mode 100644 index 00000000..b47ff656 --- /dev/null +++ b/lib/models/video/reply/data.dart @@ -0,0 +1,34 @@ +import 'package:pilipala/models/video/reply/item.dart'; + +import 'config.dart'; +import 'page.dart'; +import 'upper.dart'; + +class ReplyData { + ReplyData({ + this.page, + this.config, + this.replies, + this.topReplies, + this.upper, + }); + + ReplyPage? page; + ReplyConfig? config; + late List? replies; + late List? topReplies; + ReplyUpper? upper; + + ReplyData.fromJson(Map json) { + page = ReplyPage.fromJson(json['page']); + config = ReplyConfig.fromJson(json['config']); + replies = + json['replies'].map((item) => ReplyItemModel.fromJson(item)).toList(); + topReplies = json['top_replies'] != null + ? json['top_replies'] + .map((item) => ReplyItemModel.fromJson(item)) + .toList() + : []; + upper = ReplyUpper.fromJson(json['upper']); + } +} diff --git a/lib/models/video/reply/item.dart b/lib/models/video/reply/item.dart new file mode 100644 index 00000000..f8c79187 --- /dev/null +++ b/lib/models/video/reply/item.dart @@ -0,0 +1,125 @@ +import 'content.dart'; +import 'member.dart'; + +class ReplyItemModel { + ReplyItemModel({ + this.rpid, + this.oid, + this.type, + this.mid, + this.root, + this.parent, + this.dialog, + this.count, + this.floor, + this.state, + this.fansgrade, + this.attr, + this.ctime, + this.rpidStr, + this.rootStr, + this.parentStr, + this.like, + this.action, + this.member, + this.content, + this.replies, + this.assist, + this.upAction, + this.invisible, + this.replyControl, + }); + + int? rpid; + int? oid; + int? type; + int? mid; + int? root; + int? parent; + int? dialog; + int? count; + int? floor; + int? state; + int? fansgrade; + int? attr; + int? ctime; + String? rpidStr; + String? rootStr; + String? parentStr; + int? like; + int? action; + ReplyMember? member; + ReplyContent? content; + List? replies; + int? assist; + UpAction? upAction; + bool? invisible; + ReplyControl? replyControl; + + ReplyItemModel.fromJson(Map json) { + rpid = json['rpid']; + oid = json['oid']; + type = json['type']; + mid = json['mid']; + root = json['root']; + parent = json['parent']; + dialog = json['dialog']; + count = json['count']; + floor = json['floor']; + state = json['state']; + fansgrade = json['fansgrade']; + attr = json['attr']; + ctime = json['ctime']; + rpidStr = json['rpid_str']; + rootStr = json['root_str']; + parentStr = json['parent_str']; + like = json['like']; + action = json['action']; + member = ReplyMember.fromJson(json['member']); + content = ReplyContent.fromJson(json['content']); + replies = json['replies']; + assist = json['assist']; + upAction = UpAction.fromJson(json['up_action']); + invisible = json['invisible']; + replyControl = ReplyControl.fromJson(json['reply_control']); + } +} + +class UpAction { + UpAction({this.like, this.reply}); + + bool? like; + bool? reply; + + UpAction.fromJson(Map json) { + like = json['like']; + reply = json['reply']; + } +} + +class ReplyControl { + ReplyControl({ + this.upReply, + this.isUpTop, + this.entryText, + this.titleText, + this.time, + this.location, + }); + + bool? upReply; + bool? isUpTop; + String? entryText; + String? titleText; + String? time; + String? location; + + ReplyControl.fromJson(Map json) { + upReply = json['up_reply']; + isUpTop = json['is_up_top']; + entryText = json['sub_reply_entry_text']; + titleText = json['sub_reply_title_text']; + time = json['time_desc']; + location = json['location']; + } +} diff --git a/lib/models/video/reply/member.dart b/lib/models/video/reply/member.dart new file mode 100644 index 00000000..196f252b --- /dev/null +++ b/lib/models/video/reply/member.dart @@ -0,0 +1,55 @@ +import 'dart:convert' as convert; + +class ReplyMember { + ReplyMember({ + this.mid, + this.uname, + this.sign, + this.avatar, + this.level, + this.pendant, + this.officialVerify, + this.vip, + this.fansDetail, + }); + + String? mid; + String? uname; + String? sign; + String? avatar; + int? level; + Pendant? pendant; + Map? officialVerify; + Map? vip; + Map? fansDetail; + + ReplyMember.fromJson(Map json) { + mid = json['mid']; + uname = json['uname']; + sign = json['sign']; + avatar = json['avatar']; + level = json['level_info']['current_level']; + pendant = Pendant.fromJson(json['pendant']); + officialVerify = json['officia_vVerify']; + vip = json['vip']; + fansDetail = json['fans_detail']; + } +} + +class Pendant { + Pendant({ + this.pid, + this.name, + this.image, + }); + + int? pid; + String? name; + String? image; + + Pendant.fromJson(Map json) { + pid = json['pid']; + name = json['name']; + image = json['image']; + } +} diff --git a/lib/models/video/reply/page.dart b/lib/models/video/reply/page.dart new file mode 100644 index 00000000..771b0515 --- /dev/null +++ b/lib/models/video/reply/page.dart @@ -0,0 +1,20 @@ +class ReplyPage { + ReplyPage({ + this.num, + this.size, + this.count, + this.acount, + }); + + int? num; + int? size; + int? count; + int? acount; + + ReplyPage.fromJson(Map json) { + num = json['num']; + size = json['size']; + count = json['count']; + acount = json['acount']; + } +} diff --git a/lib/models/video/reply/top_replies.dart b/lib/models/video/reply/top_replies.dart new file mode 100644 index 00000000..f769a834 --- /dev/null +++ b/lib/models/video/reply/top_replies.dart @@ -0,0 +1 @@ +class ReplyTop {} diff --git a/lib/models/video/reply/upper.dart b/lib/models/video/reply/upper.dart new file mode 100644 index 00000000..530513aa --- /dev/null +++ b/lib/models/video/reply/upper.dart @@ -0,0 +1,18 @@ +import 'item.dart'; + +class ReplyUpper { + ReplyUpper({ + this.mid, + this.top, + }); + + int? mid; + ReplyItemModel? top; + + ReplyUpper.fromJson(Map json) { + mid = json['mid']; + top = json['top'] != null + ? ReplyItemModel.fromJson(json['top']) + : ReplyItemModel(); + } +} diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index d18a74b2..5a3a00e0 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'package:pilipala/http/reply.dart'; +import 'package:pilipala/models/video/reply/data.dart'; class VideoReplyController extends GetxController { // 视频aid @@ -13,5 +14,10 @@ class VideoReplyController extends GetxController { Future queryReplyList() async { var res = await ReplyHttp.replyList(oid: aid, pageNum: 1, type: 1); + if (res['status']) { + res['data'] = ReplyData.fromJson(res['data']); + print(res['data'].replies); + } + return res; } } diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index 13661065..ff201b16 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -1,4 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/skeleton/video_card_h.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/reply_item.dart'; +import 'controller.dart'; class VideoReplyPanel extends StatefulWidget { const VideoReplyPanel({super.key}); @@ -8,10 +13,46 @@ class VideoReplyPanel extends StatefulWidget { } class _VideoReplyPanelState extends State { + final VideoReplyController _videoReplyController = + Get.put(VideoReplyController(), tag: Get.arguments['heroTag']); + @override Widget build(BuildContext context) { - return const SliverToBoxAdapter( - child: Text('评论'), + return FutureBuilder( + future: _videoReplyController.queryReplyList(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data['status']) { + List replies = snapshot.data['data'].replies; + replies.addAll(snapshot.data['data'].topReplies); + // 请求成功 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + if (index == replies.length) { + return SizedBox(height: MediaQuery.of(context).padding.bottom); + } else { + return ReplyItem( + replyItem: replies[index], + isUp: + replies[index].mid == snapshot.data['data'].upper.mid); + } + }, childCount: replies.length + 1)); + } else { + // 请求错误 + return HttpError( + errMsg: snapshot.data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoCardHSkeleton(); + }, childCount: 5), + ); + } + }, ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 6d3b671c..7eaab185 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,6 +76,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/images/ + - assets/images/lv/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware From 1d97d1848d9ff469287aa0b962cc90d5af91b5d4 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Mon, 24 Apr 2023 21:27:36 +0800 Subject: [PATCH 10/30] =?UTF-8?q?mod:=20=E6=A5=BC=E4=B8=AD=E6=A5=BC?= =?UTF-8?q?=E5=9B=9E=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/widgets/reply_item.dart | 127 +++++++++++++---- lib/models/video/reply/item.dart | 28 +++- lib/pages/video/detail/reply/controller.dart | 1 - lib/pages/video/detail/reply/view.dart | 7 +- pubspec.lock | 140 +++++++++---------- 5 files changed, 202 insertions(+), 101 deletions(-) diff --git a/lib/common/widgets/reply_item.dart b/lib/common/widgets/reply_item.dart index 893bae18..f7b65ff5 100644 --- a/lib/common/widgets/reply_item.dart +++ b/lib/common/widgets/reply_item.dart @@ -1,28 +1,14 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/utils/utils.dart'; -class ReplyItem extends StatefulWidget { - ReplyItem({super.key, this.replyItem, this.isUp}); - ReplyItemModel? replyItem; - bool? isUp; - - @override - State createState() => _ReplyItemState(); -} - -class _ReplyItemState extends State { +class ReplyItem extends StatelessWidget { + ReplyItem({super.key, this.replyItem, required this.isUp}); ReplyItemModel? replyItem; bool isUp = false; - @override - void initState() { - super.initState(); - replyItem = widget.replyItem; - isUp = widget.isUp!; - } - @override Widget build(BuildContext context) { return InkWell( @@ -30,7 +16,7 @@ class _ReplyItemState extends State { child: Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB(12, 8, 8, 4), + padding: const EdgeInsets.fromLTRB(12, 8, 8, 14), child: content(context), ), Divider( @@ -42,7 +28,7 @@ class _ReplyItemState extends State { ); } - Widget lfAvtar() { + Widget lfAvtar(context) { return Container( margin: const EdgeInsets.only(top: 5), clipBehavior: Clip.hardEdge, @@ -78,7 +64,7 @@ class _ReplyItemState extends State { crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - lfAvtar(), + lfAvtar(context), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -93,7 +79,7 @@ class _ReplyItemState extends State { .textTheme .titleSmall! .copyWith( - color: isUp + color: isUp! ? Theme.of(context).colorScheme.primary : null, ), @@ -136,18 +122,24 @@ class _ReplyItemState extends State { ), ), ), - bottonAction(), + bottonAction(context), + // Text(replyItem!.replies!.length.toString()), + if (replyItem!.replies!.isNotEmpty) + ReplyItemRow( + replies: replyItem!.replies, + replyControl: replyItem!.replyControl, + ) ], ); } // 感谢、回复、复制 - Widget bottonAction() { + Widget bottonAction(context) { var color = Theme.of(context).colorScheme.outline; return Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.start, children: [ - // const SizedBox(width: 42), + const SizedBox(width: 42), SizedBox( height: 35, child: TextButton( @@ -176,3 +168,88 @@ class _ReplyItemState extends State { ); } } + +class ReplyItemRow extends StatelessWidget { + ReplyItemRow({super.key, this.replies, this.replyControl}); + List? replies; + var replyControl; + + @override + Widget build(BuildContext context) { + bool isShow = replyControl.isShow; + int extraRow = replyControl != null && isShow ? 1 : 0; + return Container( + margin: const EdgeInsets.only(left: 45, right: 10), + padding: const EdgeInsets.only(top: 4, bottom: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + ), + child: Material( + color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.7), + borderRadius: BorderRadius.circular(6), + clipBehavior: Clip.hardEdge, + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: replies!.length + extraRow, + itemBuilder: (context, index) { + if (extraRow == 1 && index == replies!.length) { + return ListTile( + onTap: () {}, + dense: true, + contentPadding: const EdgeInsets.only(left: 10, right: 6), + title: Text.rich( + TextSpan( + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: + Theme.of(context).textTheme.titleSmall!.fontSize, + ), + children: [ + if (replyControl.upReply) const TextSpan(text: 'up回复了'), + if (replyControl.isUpTop) const TextSpan(text: 'up点赞了'), + TextSpan(text: replyControl.entryText) + ], + ), + ), + ); + } else { + return ListTile( + onTap: () {}, + dense: true, + contentPadding: const EdgeInsets.only(left: 10, right: 6), + title: Text.rich( + overflow: TextOverflow.ellipsis, + maxLines: 2, + TextSpan( + children: [ + TextSpan( + text: replies![index].member.uname + ':', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleSmall! + .fontSize, + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => {print('跳转至用户主页')}), + TextSpan( + text: replies![index].content.message, + style: TextStyle( + fontSize: + Theme.of(context).textTheme.titleSmall!.fontSize, + ), + ) + ], + ), + ), + ); + } + }, + ), + ), + ); + } +} diff --git a/lib/models/video/reply/item.dart b/lib/models/video/reply/item.dart index f8c79187..c012f73d 100644 --- a/lib/models/video/reply/item.dart +++ b/lib/models/video/reply/item.dart @@ -77,11 +77,15 @@ class ReplyItemModel { action = json['action']; member = ReplyMember.fromJson(json['member']); content = ReplyContent.fromJson(json['content']); - replies = json['replies']; + replies = json['replies'] != null + ? json['replies'].map((item) => ReplyItemModel.fromJson(item)).toList() + : []; assist = json['assist']; upAction = UpAction.fromJson(json['up_action']); invisible = json['invisible']; - replyControl = ReplyControl.fromJson(json['reply_control']); + replyControl = json['reply_control'] == null + ? null + : ReplyControl.fromJson(json['reply_control']); } } @@ -101,6 +105,8 @@ class ReplyControl { ReplyControl({ this.upReply, this.isUpTop, + this.upLike, + this.isShow, this.entryText, this.titleText, this.time, @@ -109,14 +115,28 @@ class ReplyControl { bool? upReply; bool? isUpTop; + bool? upLike; + bool? isShow; String? entryText; String? titleText; String? time; String? location; ReplyControl.fromJson(Map json) { - upReply = json['up_reply']; - isUpTop = json['is_up_top']; + upReply = json['up_reply'] ?? false; + isUpTop = json['is_up_top'] ?? false; + upLike = json['up_like'] ?? false; + if (json['sub_reply_entry_text'] == null) { + final RegExp regex = RegExp(r"\d+"); + final RegExpMatch match = regex.firstMatch( + json['sub_reply_entry_text'] == null + ? '' + : json['sub_reply_entry_text']!)!; + isShow = int.parse(match.group(0)!) >= 3; + } else { + isShow = false; + } + entryText = json['sub_reply_entry_text']; titleText = json['sub_reply_title_text']; time = json['time_desc']; diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index 5a3a00e0..a823778f 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -16,7 +16,6 @@ class VideoReplyController extends GetxController { var res = await ReplyHttp.replyList(oid: aid, pageNum: 1, type: 1); if (res['status']) { res['data'] = ReplyData.fromJson(res['data']); - print(res['data'].replies); } return res; } diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index ff201b16..dd45ef11 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -12,10 +12,15 @@ class VideoReplyPanel extends StatefulWidget { State createState() => _VideoReplyPanelState(); } -class _VideoReplyPanelState extends State { +class _VideoReplyPanelState extends State + with AutomaticKeepAliveClientMixin { final VideoReplyController _videoReplyController = Get.put(VideoReplyController(), tag: Get.arguments['heroTag']); + // 添加页面缓存 + @override + bool get wantKeepAlive => true; + @override Widget build(BuildContext context) { return FutureBuilder( diff --git a/pubspec.lock b/pubspec.lock index cc2b687d..af1e0e5e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: args sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.0" async: @@ -14,7 +14,7 @@ packages: description: name: async sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.10.0" boolean_selector: @@ -22,7 +22,7 @@ packages: description: name: boolean_selector sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.1" cached_network_image: @@ -30,7 +30,7 @@ packages: description: name: cached_network_image sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.3" cached_network_image_platform_interface: @@ -38,7 +38,7 @@ packages: description: name: cached_network_image_platform_interface sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.0" cached_network_image_web: @@ -46,7 +46,7 @@ packages: description: name: cached_network_image_web sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.2" characters: @@ -54,7 +54,7 @@ packages: description: name: characters sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" clock: @@ -62,7 +62,7 @@ packages: description: name: clock sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" collection: @@ -70,7 +70,7 @@ packages: description: name: collection sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.17.0" connectivity_plus: @@ -78,7 +78,7 @@ packages: description: name: connectivity_plus sha256: d73575bb66216738db892f72ba67dc478bd3b5490fbbcf43644b57645eabc822 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.4" connectivity_plus_platform_interface: @@ -86,7 +86,7 @@ packages: description: name: connectivity_plus_platform_interface sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.4" cookie_jar: @@ -94,7 +94,7 @@ packages: description: name: cookie_jar sha256: d1cc6516a190ba667941f722b6365d202caff3dacb38de24268b8d6ff1ec8a1d - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.1" crypto: @@ -102,7 +102,7 @@ packages: description: name: crypto sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.2" cupertino_icons: @@ -110,7 +110,7 @@ packages: description: name: cupertino_icons sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.5" dbus: @@ -118,7 +118,7 @@ packages: description: name: dbus sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.8" dio: @@ -126,7 +126,7 @@ packages: description: name: dio sha256: "0894a098594263fe1caaba3520e3016d8a855caeb010a882273189cca10f11e9" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.1.1" dio_cookie_manager: @@ -134,7 +134,7 @@ packages: description: name: dio_cookie_manager sha256: b45f11c2fcbccf39c5952ab68910b3a155486c4fa730ceb4ce867c4943169ea1 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" dio_http2_adapter: @@ -142,7 +142,7 @@ packages: description: name: dio_http2_adapter sha256: b06a02faaff972c4809c4ada7a2f71f6c74ce21f0feee79b357f2a9590c049d4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" dynamic_color: @@ -150,7 +150,7 @@ packages: description: name: dynamic_color sha256: bbebb1b7ebed819e0ec83d4abdc2a8482d934f6a85289ffc1c6acf7589fa2aad - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.6.3" fake_async: @@ -158,7 +158,7 @@ packages: description: name: fake_async sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.1" ffi: @@ -166,7 +166,7 @@ packages: description: name: ffi sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.1" file: @@ -174,7 +174,7 @@ packages: description: name: file sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.1.4" flutter: @@ -187,7 +187,7 @@ packages: description: name: flutter_blurhash sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.0" flutter_cache_manager: @@ -195,7 +195,7 @@ packages: description: name: flutter_cache_manager sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.3.0" flutter_lints: @@ -203,7 +203,7 @@ packages: description: name: flutter_lints sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_test: @@ -221,7 +221,7 @@ packages: description: name: get sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.6.5" http: @@ -229,7 +229,7 @@ packages: description: name: http sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.13.5" http2: @@ -237,7 +237,7 @@ packages: description: name: http2 sha256: "58805ebc6513eed3b98ee0a455a8357e61d187bf2e0fdc1e53120770f78de258" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.1" http_parser: @@ -245,7 +245,7 @@ packages: description: name: http_parser sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.0.2" js: @@ -253,7 +253,7 @@ packages: description: name: js sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.6.5" lints: @@ -261,7 +261,7 @@ packages: description: name: lints sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.1" matcher: @@ -269,7 +269,7 @@ packages: description: name: matcher sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.12.13" material_color_utilities: @@ -277,7 +277,7 @@ packages: description: name: material_color_utilities sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.0" meta: @@ -285,7 +285,7 @@ packages: description: name: meta sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.8.0" nm: @@ -293,7 +293,7 @@ packages: description: name: nm sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.5.0" octo_image: @@ -301,7 +301,7 @@ packages: description: name: octo_image sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.2" path: @@ -309,7 +309,7 @@ packages: description: name: path sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.8.2" path_provider: @@ -317,7 +317,7 @@ packages: description: name: path_provider sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.14" path_provider_android: @@ -325,7 +325,7 @@ packages: description: name: path_provider_android sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.25" path_provider_foundation: @@ -333,7 +333,7 @@ packages: description: name: path_provider_foundation sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.2" path_provider_linux: @@ -341,7 +341,7 @@ packages: description: name: path_provider_linux sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.10" path_provider_platform_interface: @@ -349,7 +349,7 @@ packages: description: name: path_provider_platform_interface sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.6" path_provider_windows: @@ -357,7 +357,7 @@ packages: description: name: path_provider_windows sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.5" pedantic: @@ -365,7 +365,7 @@ packages: description: name: pedantic sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.11.1" petitparser: @@ -373,7 +373,7 @@ packages: description: name: petitparser sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.1.0" platform: @@ -381,7 +381,7 @@ packages: description: name: platform sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: @@ -389,7 +389,7 @@ packages: description: name: plugin_platform_interface sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" process: @@ -397,7 +397,7 @@ packages: description: name: process sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.2.4" rxdart: @@ -405,7 +405,7 @@ packages: description: name: rxdart sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.27.7" sky_engine: @@ -418,31 +418,31 @@ packages: description: name: source_span sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.9.1" sqflite: dependency: transitive description: name: sqflite - sha256: "500d6fec583d2c021f2d25a056d96654f910662c64f836cd2063167b8f1fa758" - url: "https://pub.flutter-io.cn" + sha256: e7dfb6482d5d02b661d0b2399efa72b98909e5aa7b8336e1fb37e226264ade00 + url: "https://pub.dev" source: hosted - version: "2.2.6" + version: "2.2.7" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "963dad8c4aa2f814ce7d2d5b1da2f36f31bd1a439d8f27e3dc189bb9d26bc684" - url: "https://pub.flutter-io.cn" + sha256: "220831bf0bd5333ff2445eee35ec131553b804e6b5d47a4a37ca6f5eb66e282c" + url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.4" stack_trace: dependency: transitive description: name: stack_trace sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.11.0" stream_channel: @@ -450,7 +450,7 @@ packages: description: name: stream_channel sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.1" string_scanner: @@ -458,23 +458,23 @@ packages: description: name: string_scanner sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.0" synchronized: dependency: transitive description: name: synchronized - sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" - url: "https://pub.flutter-io.cn" + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" term_glyph: dependency: transitive description: name: term_glyph sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: @@ -482,7 +482,7 @@ packages: description: name: test_api sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.16" typed_data: @@ -490,7 +490,7 @@ packages: description: name: typed_data sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.1" uuid: @@ -498,7 +498,7 @@ packages: description: name: uuid sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.7" vector_math: @@ -506,7 +506,7 @@ packages: description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" win32: @@ -514,7 +514,7 @@ packages: description: name: win32 sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.4" xdg_directories: @@ -522,7 +522,7 @@ packages: description: name: xdg_directories sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.0" xml: @@ -530,7 +530,7 @@ packages: description: name: xml sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.2.2" sdks: From 0ec926839c231895459a592b76886a9f258d93c2 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 26 Apr 2023 22:09:39 +0800 Subject: [PATCH 11/30] =?UTF-8?q?mod:=20=E8=AF=84=E8=AE=BA=E8=A1=A8?= =?UTF-8?q?=E6=83=85=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Podfile.lock | 2 +- lib/common/widgets/network_img_layer.dart | 1 + lib/common/widgets/reply_item.dart | 255 ---------- lib/models/video/reply/content.dart | 19 +- lib/models/video/reply/data.dart | 14 +- lib/models/video/reply/item.dart | 15 +- lib/models/video/reply/upper.dart | 2 +- lib/pages/video/detail/reply/view.dart | 23 +- .../detail/reply/widgets/reply_item.dart | 448 ++++++++++++++++++ 9 files changed, 503 insertions(+), 276 deletions(-) delete mode 100644 lib/common/widgets/reply_item.dart create mode 100644 lib/pages/video/detail/reply/widgets/reply_item.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 14e569a5..e467820a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -45,4 +45,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index e8249b80..ce98c9a9 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -36,6 +36,7 @@ class NetworkImgLayer extends StatelessWidget { imageUrl: src!, width: width ?? double.infinity, height: height ?? double.infinity, + alignment: Alignment.center, maxWidthDiskCache: ((cacheW ?? width!) * pr).toInt(), // maxHeightDiskCache: (cacheH ?? height!).toInt(), memCacheWidth: ((cacheW ?? width!) * pr).toInt(), diff --git a/lib/common/widgets/reply_item.dart b/lib/common/widgets/reply_item.dart deleted file mode 100644 index f7b65ff5..00000000 --- a/lib/common/widgets/reply_item.dart +++ /dev/null @@ -1,255 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:pilipala/common/widgets/network_img_layer.dart'; -import 'package:pilipala/models/video/reply/item.dart'; -import 'package:pilipala/utils/utils.dart'; - -class ReplyItem extends StatelessWidget { - ReplyItem({super.key, this.replyItem, required this.isUp}); - ReplyItemModel? replyItem; - bool isUp = false; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: () {}, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(12, 8, 8, 14), - child: content(context), - ), - Divider( - height: 1, - color: Theme.of(context).dividerColor.withOpacity(0.08), - ) - ], - ), - ); - } - - Widget lfAvtar(context) { - return Container( - margin: const EdgeInsets.only(top: 5), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.primary.withOpacity(0.03)), - ), - child: NetworkImgLayer( - src: replyItem!.member!.avatar, - width: 34, - height: 34, - type: 'avatar', - ), - ); - } - - Widget content(context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 头像、昵称 - Row( - // 两端对齐 - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - // onTap: () => - // Get.toNamed('/member/${reply.userName}', parameters: { - // 'memberAvatar': reply.avatar, - // 'heroTag': reply.userName + heroTag, - // }), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - lfAvtar(context), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - replyItem!.member!.uname!, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: Theme.of(context) - .textTheme - .titleSmall! - .copyWith( - color: isUp! - ? Theme.of(context).colorScheme.primary - : null, - ), - ), - const SizedBox(width: 6), - Image.asset( - 'assets/images/lv/lv${replyItem!.member!.level}.png', - height: 13, - ), - ], - ), - Text( - Utils.dateFormat(replyItem!.ctime), - style: Theme.of(context).textTheme.labelSmall!.copyWith( - color: Theme.of(context).colorScheme.outline), - ), - ], - ) - ], - ), - ), - // SizedBox( - // width: 35, - // height: 35, - // child: IconButton( - // padding: const EdgeInsets.all(2.0), - // icon: const Icon(Icons.more_horiz_outlined, size: 18.0), - // onPressed: () {}, - // ), - // ) - ], - ), - // title - Container( - margin: const EdgeInsets.only(top: 6, left: 45, right: 8), - child: SelectionArea( - child: Text( - replyItem!.content!.message!, - style: const TextStyle(height: 1.8), - ), - ), - ), - bottonAction(context), - // Text(replyItem!.replies!.length.toString()), - if (replyItem!.replies!.isNotEmpty) - ReplyItemRow( - replies: replyItem!.replies, - replyControl: replyItem!.replyControl, - ) - ], - ); - } - - // 感谢、回复、复制 - Widget bottonAction(context) { - var color = Theme.of(context).colorScheme.outline; - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(width: 42), - SizedBox( - height: 35, - child: TextButton( - child: Row( - children: [ - Icon( - Icons.thumb_up_alt_outlined, - size: 16, - color: color, - ), - const SizedBox(width: 4), - Text( - replyItem!.like.toString(), - style: TextStyle( - color: color, - fontSize: - Theme.of(context).textTheme.labelSmall!.fontSize), - ), - ], - ), - onPressed: () {}, - ), - ), - const SizedBox(width: 5) - ], - ); - } -} - -class ReplyItemRow extends StatelessWidget { - ReplyItemRow({super.key, this.replies, this.replyControl}); - List? replies; - var replyControl; - - @override - Widget build(BuildContext context) { - bool isShow = replyControl.isShow; - int extraRow = replyControl != null && isShow ? 1 : 0; - return Container( - margin: const EdgeInsets.only(left: 45, right: 10), - padding: const EdgeInsets.only(top: 4, bottom: 4), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - ), - child: Material( - color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.7), - borderRadius: BorderRadius.circular(6), - clipBehavior: Clip.hardEdge, - child: ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: replies!.length + extraRow, - itemBuilder: (context, index) { - if (extraRow == 1 && index == replies!.length) { - return ListTile( - onTap: () {}, - dense: true, - contentPadding: const EdgeInsets.only(left: 10, right: 6), - title: Text.rich( - TextSpan( - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontSize: - Theme.of(context).textTheme.titleSmall!.fontSize, - ), - children: [ - if (replyControl.upReply) const TextSpan(text: 'up回复了'), - if (replyControl.isUpTop) const TextSpan(text: 'up点赞了'), - TextSpan(text: replyControl.entryText) - ], - ), - ), - ); - } else { - return ListTile( - onTap: () {}, - dense: true, - contentPadding: const EdgeInsets.only(left: 10, right: 6), - title: Text.rich( - overflow: TextOverflow.ellipsis, - maxLines: 2, - TextSpan( - children: [ - TextSpan( - text: replies![index].member.uname + ':', - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .titleSmall! - .fontSize, - color: Theme.of(context).colorScheme.primary, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => {print('跳转至用户主页')}), - TextSpan( - text: replies![index].content.message, - style: TextStyle( - fontSize: - Theme.of(context).textTheme.titleSmall!.fontSize, - ), - ) - ], - ), - ), - ); - } - }, - ), - ), - ); - } -} diff --git a/lib/models/video/reply/content.dart b/lib/models/video/reply/content.dart index f924d3f2..83dbd556 100644 --- a/lib/models/video/reply/content.dart +++ b/lib/models/video/reply/content.dart @@ -1,10 +1,11 @@ class ReplyContent { ReplyContent({ this.message, - this.atNameToMid, // @的用户的mid - this.memebers, // 被@的用户List 如果有的话 - this.emote, // 表情包 如果有的话 - this.jumpUrl, + this.atNameToMid, // @的用户的mid null + this.memebers, // 被@的用户List 如果有的话 [] + this.emote, // 表情包 如果有的话 null + this.jumpUrl, // {} + this.pictures, // {} }); String? message; @@ -12,12 +13,14 @@ class ReplyContent { List? memebers; Map? emote; Map? jumpUrl; + List? pictures; ReplyContent.fromJson(Map json) { message = json['message']; - atNameToMid = json['at_name_to_mid']; - memebers = json['memebers']; - emote = json['emote']; - jumpUrl = json['jumpUrl']; + atNameToMid = json['at_name_to_mid'] ?? {}; + memebers = json['memebers'] ?? []; + emote = json['emote'] ?? {}; + jumpUrl = json['jumpUrl'] ?? {}; + pictures = json['pictures'] ?? []; } } diff --git a/lib/models/video/reply/data.dart b/lib/models/video/reply/data.dart index b47ff656..3b94a008 100644 --- a/lib/models/video/reply/data.dart +++ b/lib/models/video/reply/data.dart @@ -15,18 +15,22 @@ class ReplyData { ReplyPage? page; ReplyConfig? config; - late List? replies; - late List? topReplies; + late List? replies; + late List? topReplies; ReplyUpper? upper; ReplyData.fromJson(Map json) { page = ReplyPage.fromJson(json['page']); config = ReplyConfig.fromJson(json['config']); - replies = - json['replies'].map((item) => ReplyItemModel.fromJson(item)).toList(); + replies = json['replies'] + .map( + (item) => ReplyItemModel.fromJson(item, json['upper']['mid'])) + .toList(); topReplies = json['top_replies'] != null ? json['top_replies'] - .map((item) => ReplyItemModel.fromJson(item)) + .map((item) => ReplyItemModel.fromJson( + item, json['upper']['mid'], + isTopStatus: true)) .toList() : []; upper = ReplyUpper.fromJson(json['upper']); diff --git a/lib/models/video/reply/item.dart b/lib/models/video/reply/item.dart index c012f73d..53b71b6e 100644 --- a/lib/models/video/reply/item.dart +++ b/lib/models/video/reply/item.dart @@ -28,6 +28,8 @@ class ReplyItemModel { this.upAction, this.invisible, this.replyControl, + this.isUp, + this.isTop, }); int? rpid; @@ -55,8 +57,11 @@ class ReplyItemModel { UpAction? upAction; bool? invisible; ReplyControl? replyControl; + bool? isUp; + bool? isTop = false; - ReplyItemModel.fromJson(Map json) { + ReplyItemModel.fromJson(Map json, upperMid, + {isTopStatus = false}) { rpid = json['rpid']; oid = json['oid']; type = json['type']; @@ -78,7 +83,9 @@ class ReplyItemModel { member = ReplyMember.fromJson(json['member']); content = ReplyContent.fromJson(json['content']); replies = json['replies'] != null - ? json['replies'].map((item) => ReplyItemModel.fromJson(item)).toList() + ? json['replies'] + .map((item) => ReplyItemModel.fromJson(item, upperMid)) + .toList() : []; assist = json['assist']; upAction = UpAction.fromJson(json['up_action']); @@ -86,6 +93,8 @@ class ReplyItemModel { replyControl = json['reply_control'] == null ? null : ReplyControl.fromJson(json['reply_control']); + isUp = upperMid.toString() == json['member']['mid']; + isTop = isTopStatus; } } @@ -126,7 +135,7 @@ class ReplyControl { upReply = json['up_reply'] ?? false; isUpTop = json['is_up_top'] ?? false; upLike = json['up_like'] ?? false; - if (json['sub_reply_entry_text'] == null) { + if (json['sub_reply_entry_text'] != null) { final RegExp regex = RegExp(r"\d+"); final RegExpMatch match = regex.firstMatch( json['sub_reply_entry_text'] == null diff --git a/lib/models/video/reply/upper.dart b/lib/models/video/reply/upper.dart index 530513aa..4bdb62aa 100644 --- a/lib/models/video/reply/upper.dart +++ b/lib/models/video/reply/upper.dart @@ -12,7 +12,7 @@ class ReplyUpper { ReplyUpper.fromJson(Map json) { mid = json['mid']; top = json['top'] != null - ? ReplyItemModel.fromJson(json['top']) + ? ReplyItemModel.fromJson(json['top'], json['mid']) : ReplyItemModel(); } } diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index dd45ef11..67341100 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/widgets/http_error.dart'; -import 'package:pilipala/common/widgets/reply_item.dart'; +import 'package:pilipala/models/video/reply/item.dart'; import 'controller.dart'; +import 'widgets/reply_item.dart'; class VideoReplyPanel extends StatefulWidget { const VideoReplyPanel({super.key}); @@ -28,8 +29,24 @@ class _VideoReplyPanelState extends State builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.data['status']) { - List replies = snapshot.data['data'].replies; - replies.addAll(snapshot.data['data'].topReplies); + List replies = snapshot.data['data'].replies; + // 添加置顶回复 + if (snapshot.data['data'].upper.top != null) { + bool flag = false; + for (var i = 0; + i < snapshot.data['data'].topReplies.length; + i++) { + if (snapshot.data['data'].topReplies[i].rpid == + snapshot.data['data'].upper.top.rpid) { + flag = true; + } + } + if (!flag) { + replies.insert(0, snapshot.data['data'].upper.top); + } + } + + replies.insertAll(0, snapshot.data['data'].topReplies); // 请求成功 return SliverList( delegate: SliverChildBuilderDelegate((context, index) { diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart new file mode 100644 index 00000000..184ced82 --- /dev/null +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -0,0 +1,448 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/video/reply/item.dart'; +import 'package:pilipala/utils/utils.dart'; + +class ReplyItem extends StatelessWidget { + ReplyItem({super.key, this.replyItem, required this.isUp}); + ReplyItemModel? replyItem; + bool isUp = false; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () {}, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 8, 0), + child: content(context), + ), + Divider( + height: 1, + color: Theme.of(context).dividerColor.withOpacity(0.08), + ) + ], + ), + ); + } + + Widget lfAvtar(context) { + return Container( + margin: const EdgeInsets.only(top: 5), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary.withOpacity(0.03)), + ), + child: NetworkImgLayer( + src: replyItem!.member!.avatar, + width: 30, + height: 30, + type: 'avatar', + ), + ); + } + + Widget content(context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 头像、昵称 + Row( + // 两端对齐 + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + // onTap: () => + // Get.toNamed('/member/${reply.userName}', parameters: { + // 'memberAvatar': reply.avatar, + // 'heroTag': reply.userName + heroTag, + // }), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + lfAvtar(context), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + replyItem!.member!.uname!, + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith( + color: replyItem!.isUp! + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + ), + ), + const SizedBox(width: 6), + Image.asset( + 'assets/images/lv/lv${replyItem!.member!.level}.png', + height: 13, + ), + ], + ), + ], + ) + ], + ), + ), + ], + ), + // title + Container( + margin: const EdgeInsets.only(top: 0, left: 45, right: 6), + child: SelectableRegion( + magnifierConfiguration: const TextMagnifierConfiguration(), + focusNode: FocusNode(), + selectionControls: MaterialTextSelectionControls(), + child: Text.rich( + style: const TextStyle(height: 1.6), + TextSpan( + children: [ + buildContent(context, replyItem!.content!), + ], + ), + ), + ), + ), + // 操作区域 + bottonAction(context, replyItem!.replyControl), + if (replyItem!.replies!.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.only(top: 2, bottom: 12), + child: ReplyItemRow( + replies: replyItem!.replies, + replyControl: replyItem!.replyControl, + ), + ), + ], + ], + ); + } + + // 感谢、回复、复制 + Widget bottonAction(context, replyControl) { + var color = Theme.of(context).colorScheme.outline; + return Row( + children: [ + const SizedBox(width: 48), + Text( + Utils.dateFormat(replyItem!.ctime), + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Theme.of(context).colorScheme.outline), + ), + if (replyItem!.isTop!) ...[ + Text( + ' • 置顶', + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + if (replyControl!.isUpTop!) ...[ + Text( + ' • 超赞', + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + // const SizedBox(width: 4), + ], + const Spacer(), + SizedBox( + height: 35, + child: TextButton( + child: Row( + children: [ + Icon( + Icons.thumb_up_alt_outlined, + size: 16, + color: color, + ), + const SizedBox(width: 4), + Text( + replyItem!.like.toString(), + style: TextStyle( + color: color, + fontSize: + Theme.of(context).textTheme.labelSmall!.fontSize), + ), + ], + ), + onPressed: () {}, + ), + ), + const SizedBox(width: 5) + ], + ); + } +} + +// ignore: must_be_immutable +class ReplyItemRow extends StatelessWidget { + ReplyItemRow({ + super.key, + this.replies, + this.replyControl, + }); + List? replies; + ReplyControl? replyControl; + + @override + Widget build(BuildContext context) { + bool isShow = replyControl!.isShow!; + int extraRow = replyControl != null && isShow ? 1 : 0; + return Container( + margin: const EdgeInsets.only(left: 42, right: 4, top: 0), + child: Material( + color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.7), + borderRadius: BorderRadius.circular(6), + clipBehavior: Clip.hardEdge, + animationDuration: Duration.zero, + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: replies!.length + extraRow, + itemBuilder: (context, index) { + if (extraRow == 1 && index == replies!.length) { + // 有楼中楼回复,在最后显示 + return InkWell( + onTap: () {}, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 8), + child: Text.rich( + TextSpan( + style: TextStyle( + fontSize: + Theme.of(context).textTheme.labelMedium!.fontSize, + ), + children: [ + if (replyControl!.upReply!) + const TextSpan(text: 'up主等人 '), + TextSpan( + text: replyControl!.entryText!, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ) + ], + ), + ), + ), + ); + } else { + return InkWell( + onTap: () {}, + child: Padding( + padding: EdgeInsets.fromLTRB(8, index == 0 ? 8 : 4, 8, 4), + child: Text.rich( + overflow: TextOverflow.ellipsis, + maxLines: 2, + TextSpan( + children: [ + if (replies![index].isUp) + TextSpan( + text: 'UP • ', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: Theme.of(context) + .textTheme + .labelMedium! + .fontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + TextSpan( + text: replies![index].member.uname + ' ', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleSmall! + .fontSize, + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => { + print('跳转至用户主页'), + }, + ), + buildContent(context, replies![index].content), + ], + ), + ), + )); + } + }, + ), + ), + ); + } +} + +InlineSpan buildContent(BuildContext context, content) { + if (content.emote.isEmpty && + content.atNameToMid.isEmpty && + content.jumpUrl.isEmpty && + content.pictures.isEmpty) { + return TextSpan(text: content.message); + } + List spanChilds = []; + // if (content.atNameToMid.isNotEmpty) { + // print(content.message); + // content.atNameToMid.forEach((key, value) { + // key = '@' + key; + // int lastIndex = content.message.indexOf(key); + // int endIndex = (lastIndex + key.length).toInt(); + // if (lastIndex >= 0) { + // spanChilds.add(TextSpan( + // text: '@' + key, + // style: TextStyle(color: Theme.of(context).colorScheme.primary))); + // content.message = content.message.replaceRange(lastIndex, endIndex, ''); + // spanChilds.add(TextSpan(text: content.message)); + // } + // spanChilds.add(TextSpan(text: content.message.substring(lastIndex))); + // }); + // // return TextSpan(children: spanChilds); + // } + // if (content.emote.isNotEmpty) { + // content.emote.forEach((key, value) { + // int lastIndex = content.message.indexOf(key); + // int endIndex = content.message.indexOf(key) + key.length; + // if (lastIndex >= 0) { + // content.message = content.message.replaceRange(lastIndex, endIndex, ''); + // spanChilds.add(TextSpan(text: content.message.substring(0, lastIndex))); + // } + // spanChilds.add(WidgetSpan( + // child: NetworkImgLayer( + // src: value["url"], + // width: 20, + // height: 20, + // ))); + // }); + // // return TextSpan(children: spanChilds); + // } + // if (content.pictures.isNotEmpty) { + // spanChilds.add(TextSpan(text: content.message)); + // spanChilds.add(const WidgetSpan( + // child: SizedBox( + // height: 4, + // ))); + // for (var i = 0; i < content.pictures.length; i++) { + // spanChilds.add( + // WidgetSpan( + // child: SizedBox( + // height: 180, + // child: NetworkImgLayer( + // src: content.pictures[i]['img_src'], + // width: 200, + // height: 200 * + // content.pictures[i]['img_height'] / + // content.pictures[i]['img_width'], + // ), + // ), + // ), + // ); + // } + // return TextSpan(children: spanChilds); + // } + content.message.splitMapJoin( + RegExp(r"\[.*?\]"), + onMatch: (Match match) { + String matchStr = match[0]!; + if (content.emote.isNotEmpty) { + if (content.emote.keys.contains(matchStr)) { + spanChilds.add( + WidgetSpan( + child: NetworkImgLayer( + src: content.emote[matchStr]['url'], + width: 20, + height: 20, + ), + ), + ); + } else { + spanChilds.add(TextSpan(text: matchStr)); + return matchStr; + } + } + return matchStr; + }, + onNonMatch: (String str) { + try { + if (content.atNameToMid.isNotEmpty) { + return str.splitMapJoin( + RegExp(r"@.*:"), + onMatch: (Match match) { + if (match[0] != null) { + content.atNameToMid.forEach((key, value) { + spanChilds.add( + TextSpan( + text: '@$key ', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.titleSmall!.fontSize, + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => { + print('跳转至用户主页'), + }, + ), + ); + }); + } + return match[0]!; + }, + onNonMatch: (String str) { + spanChilds.add(TextSpan(text: str)); + return str; + }, + ); + } else { + spanChilds.add(TextSpan(text: str)); + return str; + } + } catch (e) { + spanChilds.add(TextSpan(text: str)); + return str; + } + }, + ); + if (content.pictures.isNotEmpty) { + spanChilds.add(const WidgetSpan( + child: SizedBox( + height: 4, + ))); + for (var i = 0; i < content.pictures.length; i++) { + spanChilds.add( + WidgetSpan( + child: SizedBox( + height: 180, + child: NetworkImgLayer( + src: content.pictures[i]['img_src'], + width: 200, + height: 200 * + content.pictures[i]['img_height'] / + content.pictures[i]['img_width'], + ), + ), + ), + ); + } + } + return TextSpan(children: spanChilds); +} From 0aba791e2c68e3314bd9bce77815fc5b966af3b8 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 27 Apr 2023 13:54:55 +0800 Subject: [PATCH 12/30] =?UTF-8?q?mod:=20@=E7=94=A8=E6=88=B7=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E3=80=81jumpUrl=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/models/video/reply/content.dart | 2 +- lib/models/video/reply/upper.dart | 2 +- .../detail/reply/widgets/reply_item.dart | 185 ++++++++---------- 3 files changed, 80 insertions(+), 109 deletions(-) diff --git a/lib/models/video/reply/content.dart b/lib/models/video/reply/content.dart index 83dbd556..42ccaded 100644 --- a/lib/models/video/reply/content.dart +++ b/lib/models/video/reply/content.dart @@ -20,7 +20,7 @@ class ReplyContent { atNameToMid = json['at_name_to_mid'] ?? {}; memebers = json['memebers'] ?? []; emote = json['emote'] ?? {}; - jumpUrl = json['jumpUrl'] ?? {}; + jumpUrl = json['jump_url'] ?? {}; pictures = json['pictures'] ?? []; } } diff --git a/lib/models/video/reply/upper.dart b/lib/models/video/reply/upper.dart index 4bdb62aa..1d1f6071 100644 --- a/lib/models/video/reply/upper.dart +++ b/lib/models/video/reply/upper.dart @@ -13,6 +13,6 @@ class ReplyUpper { mid = json['mid']; top = json['top'] != null ? ReplyItemModel.fromJson(json['top'], json['mid']) - : ReplyItemModel(); + : null; } } diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 184ced82..27c22287 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -16,7 +16,7 @@ class ReplyItem extends StatelessWidget { child: Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB(12, 8, 8, 0), + padding: const EdgeInsets.fromLTRB(12, 6, 8, 0), child: content(context), ), Divider( @@ -31,11 +31,6 @@ class ReplyItem extends StatelessWidget { Widget lfAvtar(context) { return Container( margin: const EdgeInsets.only(top: 5), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.primary.withOpacity(0.03)), - ), child: NetworkImgLayer( src: replyItem!.member!.avatar, width: 30, @@ -104,7 +99,7 @@ class ReplyItem extends StatelessWidget { focusNode: FocusNode(), selectionControls: MaterialTextSelectionControls(), child: Text.rich( - style: const TextStyle(height: 1.6), + style: const TextStyle(height: 1.65), TextSpan( children: [ buildContent(context, replyItem!.content!), @@ -301,65 +296,8 @@ InlineSpan buildContent(BuildContext context, content) { return TextSpan(text: content.message); } List spanChilds = []; - // if (content.atNameToMid.isNotEmpty) { - // print(content.message); - // content.atNameToMid.forEach((key, value) { - // key = '@' + key; - // int lastIndex = content.message.indexOf(key); - // int endIndex = (lastIndex + key.length).toInt(); - // if (lastIndex >= 0) { - // spanChilds.add(TextSpan( - // text: '@' + key, - // style: TextStyle(color: Theme.of(context).colorScheme.primary))); - // content.message = content.message.replaceRange(lastIndex, endIndex, ''); - // spanChilds.add(TextSpan(text: content.message)); - // } - // spanChilds.add(TextSpan(text: content.message.substring(lastIndex))); - // }); - // // return TextSpan(children: spanChilds); - // } - // if (content.emote.isNotEmpty) { - // content.emote.forEach((key, value) { - // int lastIndex = content.message.indexOf(key); - // int endIndex = content.message.indexOf(key) + key.length; - // if (lastIndex >= 0) { - // content.message = content.message.replaceRange(lastIndex, endIndex, ''); - // spanChilds.add(TextSpan(text: content.message.substring(0, lastIndex))); - // } - // spanChilds.add(WidgetSpan( - // child: NetworkImgLayer( - // src: value["url"], - // width: 20, - // height: 20, - // ))); - // }); - // // return TextSpan(children: spanChilds); - // } - // if (content.pictures.isNotEmpty) { - // spanChilds.add(TextSpan(text: content.message)); - // spanChilds.add(const WidgetSpan( - // child: SizedBox( - // height: 4, - // ))); - // for (var i = 0; i < content.pictures.length; i++) { - // spanChilds.add( - // WidgetSpan( - // child: SizedBox( - // height: 180, - // child: NetworkImgLayer( - // src: content.pictures[i]['img_src'], - // width: 200, - // height: 200 * - // content.pictures[i]['img_height'] / - // content.pictures[i]['img_width'], - // ), - // ), - // ), - // ); - // } - // return TextSpan(children: spanChilds); - // } - content.message.splitMapJoin( + // 匹配表情 + String matchEmote = content.message.splitMapJoin( RegExp(r"\[.*?\]"), onMatch: (Match match) { String matchStr = match[0]!; @@ -379,54 +317,86 @@ InlineSpan buildContent(BuildContext context, content) { return matchStr; } } - return matchStr; + return ''; }, onNonMatch: (String str) { - try { - if (content.atNameToMid.isNotEmpty) { - return str.splitMapJoin( - RegExp(r"@.*:"), - onMatch: (Match match) { - if (match[0] != null) { - content.atNameToMid.forEach((key, value) { - spanChilds.add( - TextSpan( - text: '@$key ', - style: TextStyle( - fontSize: - Theme.of(context).textTheme.titleSmall!.fontSize, - color: Theme.of(context).colorScheme.primary, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => { - print('跳转至用户主页'), - }, + // 匹配@用户 + String matchMember = str; + if (content.atNameToMid.isNotEmpty) { + matchMember = str.splitMapJoin( + RegExp(r"@.*:"), + onMatch: (Match match) { + if (match[0] != null) { + content.atNameToMid.forEach((key, value) { + spanChilds.add( + TextSpan( + text: '@$key ', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.titleSmall!.fontSize, + color: Theme.of(context).colorScheme.primary, ), - ); - }); - } - return match[0]!; - }, - onNonMatch: (String str) { - spanChilds.add(TextSpan(text: str)); - return str; - }, - ); - } else { - spanChilds.add(TextSpan(text: str)); - return str; - } - } catch (e) { - spanChilds.add(TextSpan(text: str)); - return str; + recognizer: TapGestureRecognizer() + ..onTap = () => { + print('跳转至用户主页'), + }, + ), + ); + }); + } + return ''; + }, + onNonMatch: (String str) { + spanChilds.add(TextSpan(text: str)); + return str; + }, + ); + } else { + matchMember = str; } + + // 匹配 jumpUrl + String matchUrl = matchMember; + if (content.jumpUrl.isNotEmpty) { + List urlKeys = content.jumpUrl.keys.toList(); + matchUrl = matchMember.splitMapJoin( + RegExp("(?:${urlKeys.join("|")})"), + onMatch: (Match match) { + String matchStr = match[0]!; + // spanChilds.add(TextSpan(text: matchStr)); + spanChilds.add( + TextSpan( + text: content.jumpUrl[matchStr]['title'], + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => { + print('Url 点击'), + }, + ), + ); + return ''; + }, + onNonMatch: (String str) { + spanChilds.add(TextSpan(text: str)); + return str; + }, + ); + } + + if (content.atNameToMid.isEmpty && content.jumpUrl.isEmpty) { + spanChilds.add(TextSpan(text: str)); + } + return str; }, ); + + // 图片渲染 if (content.pictures.isNotEmpty) { - spanChilds.add(const WidgetSpan( - child: SizedBox( - height: 4, - ))); + spanChilds.add( + const TextSpan(text: '\n'), + ); for (var i = 0; i < content.pictures.length; i++) { spanChilds.add( WidgetSpan( @@ -444,5 +414,6 @@ InlineSpan buildContent(BuildContext context, content) { ); } } + // spanChilds.add(TextSpan(text: matchMember)); return TextSpan(children: spanChilds); } From c6c41386407c98ea15e73d108019d5973573d181 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 27 Apr 2023 14:06:44 +0800 Subject: [PATCH 13/30] fix: .lock --- pubspec.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index af1e0e5e..2a6cbf57 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -77,10 +77,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: d73575bb66216738db892f72ba67dc478bd3b5490fbbcf43644b57645eabc822 + sha256: "5c7ad2d90aae958c230b27450044a29f5b0a69ea4b1792e17164b3a53de33e47" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.5" connectivity_plus_platform_interface: dependency: transitive description: @@ -324,10 +324,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a + sha256: "3e58242edc02624f2c712e3f8bea88e0e341c4ae1abd3a6ff661318a3aefd829" url: "https://pub.dev" source: hosted - version: "2.0.25" + version: "2.0.26" path_provider_foundation: dependency: transitive description: @@ -356,10 +356,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" pedantic: dependency: transitive description: @@ -513,10 +513,10 @@ packages: dependency: transitive description: name: win32 - sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 + sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "4.1.3" xdg_directories: dependency: transitive description: From 6fbfd2db9ee49bb6e2f01ac75620c1d2191a67cb Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 28 Apr 2023 12:48:45 +0800 Subject: [PATCH 14/30] =?UTF-8?q?mod:=20=E8=AF=84=E8=AE=BA=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E3=80=81=E7=BF=BB=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/models/video/reply/data.dart | 10 +- lib/pages/video/detail/reply/controller.dart | 56 ++++- lib/pages/video/detail/reply/view.dart | 129 +++++++----- .../detail/reply/widgets/reply_item.dart | 151 +++++++------- lib/pages/video/detail/view.dart | 196 +++++++++--------- pubspec.lock | 18 +- pubspec.yaml | 2 + 7 files changed, 324 insertions(+), 238 deletions(-) diff --git a/lib/models/video/reply/data.dart b/lib/models/video/reply/data.dart index 3b94a008..cc419777 100644 --- a/lib/models/video/reply/data.dart +++ b/lib/models/video/reply/data.dart @@ -22,10 +22,12 @@ class ReplyData { ReplyData.fromJson(Map json) { page = ReplyPage.fromJson(json['page']); config = ReplyConfig.fromJson(json['config']); - replies = json['replies'] - .map( - (item) => ReplyItemModel.fromJson(item, json['upper']['mid'])) - .toList(); + replies = json['replies'] != null + ? json['replies'] + .map( + (item) => ReplyItemModel.fromJson(item, json['upper']['mid'])) + .toList() + : []; topReplies = json['top_replies'] != null ? json['top_replies'] .map((item) => ReplyItemModel.fromJson( diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index a823778f..23447a37 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -1,22 +1,62 @@ +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/reply.dart'; import 'package:pilipala/models/video/reply/data.dart'; +import 'package:pilipala/models/video/reply/item.dart'; class VideoReplyController extends GetxController { + final ScrollController scrollController = ScrollController(); // 视频aid String aid = Get.parameters['aid']!; + RxList replyList = [ReplyItemModel()].obs; + // 当前页 + int currentPage = 0; + bool isLoadingMore = false; + bool noMore = false; - @override - void onInit() { - super.onInit(); - queryReplyList(); - } - - Future queryReplyList() async { - var res = await ReplyHttp.replyList(oid: aid, pageNum: 1, type: 1); + Future queryReplyList({type = 'init'}) async { + isLoadingMore = true; + var res = + await ReplyHttp.replyList(oid: aid, pageNum: currentPage + 1, type: 1); if (res['status']) { res['data'] = ReplyData.fromJson(res['data']); + if (res['data'].replies.isNotEmpty) { + currentPage = currentPage + 1; + noMore = false; + } else { + if (currentPage == 0) { + } else { + noMore = true; + } + } + if (type == 'init') { + List replies = res['data'].replies; + // 添加置顶回复 + if (res['data'].upper.top != null) { + bool flag = false; + for (var i = 0; i < res['data'].topReplies.length; i++) { + if (res['data'].topReplies[i].rpid == res['data'].upper.top.rpid) { + flag = true; + } + } + if (!flag) { + replies.insert(0, res['data'].upper.top); + } + } + replies.insertAll(0, res['data'].topReplies); + res['data'].replies = replies; + replyList.value = res['data'].replies!; + } else { + replyList.addAll(res['data'].replies!); + res['data'].replies.addAll(replyList); + } } + isLoadingMore = false; return res; } + + // 上拉加载 + Future onLoad() async { + queryReplyList(type: 'onLoad'); + } } diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index 67341100..b1ce3fad 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -17,64 +17,95 @@ class _VideoReplyPanelState extends State with AutomaticKeepAliveClientMixin { final VideoReplyController _videoReplyController = Get.put(VideoReplyController(), tag: Get.arguments['heroTag']); - + // List? replyList; + Future? _futureBuilderFuture; // 添加页面缓存 @override bool get wantKeepAlive => true; @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _videoReplyController.queryReplyList(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.data['status']) { - List replies = snapshot.data['data'].replies; - // 添加置顶回复 - if (snapshot.data['data'].upper.top != null) { - bool flag = false; - for (var i = 0; - i < snapshot.data['data'].topReplies.length; - i++) { - if (snapshot.data['data'].topReplies[i].rpid == - snapshot.data['data'].upper.top.rpid) { - flag = true; - } - } - if (!flag) { - replies.insert(0, snapshot.data['data'].upper.top); - } - } + void initState() { + super.initState(); - replies.insertAll(0, snapshot.data['data'].topReplies); - // 请求成功 - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - if (index == replies.length) { - return SizedBox(height: MediaQuery.of(context).padding.bottom); - } else { - return ReplyItem( - replyItem: replies[index], - isUp: - replies[index].mid == snapshot.data['data'].upper.mid); - } - }, childCount: replies.length + 1)); - } else { - // 请求错误 - return HttpError( - errMsg: snapshot.data['msg'], - fn: () => setState(() {}), - ); + _futureBuilderFuture = _videoReplyController.queryReplyList(); + _videoReplyController.scrollController.addListener( + () { + if (_videoReplyController.scrollController.position.pixels >= + _videoReplyController.scrollController.position.maxScrollExtent - + 300) { + if (!_videoReplyController.isLoadingMore) { + _videoReplyController.onLoad(); } - } else { - // 骨架屏 - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return const VideoCardHSkeleton(); - }, childCount: 5), - ); } }, ); } + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async { + setState(() {}); + _videoReplyController.currentPage = 0; + return await _videoReplyController.queryReplyList(); + }, + child: CustomScrollView( + controller: _videoReplyController.scrollController, + key: const PageStorageKey('评论'), + slivers: [ + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + // 请求成功 + return Obx( + () => SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == _videoReplyController.replyList.length) { + return Container( + padding: EdgeInsets.only( + bottom: + MediaQuery.of(context).padding.bottom), + height: + MediaQuery.of(context).padding.bottom + 60, + child: Center( + child: Text(_videoReplyController.noMore + ? '没有更多了' + : '加载中'), + ), + ); + } else { + return ReplyItem( + replyItem: _videoReplyController.replyList[index], + ); + } + }, + childCount: _videoReplyController.replyList.length + 1, + ), + ), + ); + } else { + // 请求错误 + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoCardHSkeleton(); + }, childCount: 5), + ); + } + }, + ) + ], + ), + ); + } } diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 27c22287..b5b78747 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -5,9 +5,8 @@ import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/utils/utils.dart'; class ReplyItem extends StatelessWidget { - ReplyItem({super.key, this.replyItem, required this.isUp}); + ReplyItem({super.key, this.replyItem}); ReplyItemModel? replyItem; - bool isUp = false; @override Widget build(BuildContext context) { @@ -45,51 +44,46 @@ class ReplyItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ // 头像、昵称 - Row( - // 两端对齐 - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - // onTap: () => - // Get.toNamed('/member/${reply.userName}', parameters: { - // 'memberAvatar': reply.avatar, - // 'heroTag': reply.userName + heroTag, - // }), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - lfAvtar(context), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + GestureDetector( + // onTap: () => + // Get.toNamed('/member/${reply.userName}', parameters: { + // 'memberAvatar': reply.avatar, + // 'heroTag': reply.userName + heroTag, + // }), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + lfAvtar(context), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Row( - children: [ - Text( - replyItem!.member!.uname!, - style: Theme.of(context) - .textTheme - .titleSmall! - .copyWith( - color: replyItem!.isUp! - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, - ), - ), - const SizedBox(width: 6), - Image.asset( - 'assets/images/lv/lv${replyItem!.member!.level}.png', - height: 13, - ), - ], + Text( + replyItem!.member!.uname!, + style: TextStyle( + color: replyItem!.isUp! + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + fontSize: + Theme.of(context).textTheme.titleSmall!.fontSize, + ), ), + const SizedBox(width: 6), + Image.asset( + 'assets/images/lv/lv${replyItem!.member!.level}.png', + height: 11, + ), + const SizedBox(width: 6), + if (replyItem!.isUp!) UpTag() ], - ) + ), ], - ), - ), - ], + ) + ], + ), ), // title Container( @@ -102,6 +96,8 @@ class ReplyItem extends StatelessWidget { style: const TextStyle(height: 1.65), TextSpan( children: [ + if (replyItem!.isTop!) + WidgetSpan(child: UpTag(tagText: '置顶')), buildContent(context, replyItem!.content!), ], ), @@ -136,26 +132,9 @@ class ReplyItem extends StatelessWidget { .labelMedium! .copyWith(color: Theme.of(context).colorScheme.outline), ), - if (replyItem!.isTop!) ...[ - Text( - ' • 置顶', - style: TextStyle( - fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - if (replyControl!.isUpTop!) ...[ - Text( - ' • 超赞', - style: TextStyle( - fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.primary, - ), - ), - // const SizedBox(width: 4), - ], const Spacer(), + if (replyControl!.isUpTop!) + Icon(Icons.favorite, color: Colors.red[400], size: 18), SizedBox( height: 35, child: TextButton( @@ -202,7 +181,7 @@ class ReplyItemRow extends StatelessWidget { return Container( margin: const EdgeInsets.only(left: 42, right: 4, top: 0), child: Material( - color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.7), + color: Theme.of(context).colorScheme.onInverseSurface, borderRadius: BorderRadius.circular(6), clipBehavior: Clip.hardEdge, animationDuration: Duration.zero, @@ -245,21 +224,15 @@ class ReplyItemRow extends StatelessWidget { child: Padding( padding: EdgeInsets.fromLTRB(8, index == 0 ? 8 : 4, 8, 4), child: Text.rich( - overflow: TextOverflow.ellipsis, - maxLines: 2, + overflow: extraRow == 1 + ? TextOverflow.ellipsis + : TextOverflow.visible, + maxLines: extraRow == 1 ? 2 : null, TextSpan( children: [ if (replies![index].isUp) - TextSpan( - text: 'UP • ', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: Theme.of(context) - .textTheme - .labelMedium! - .fontSize, - color: Theme.of(context).colorScheme.primary, - ), + WidgetSpan( + child: UpTag(), ), TextSpan( text: replies![index].member.uname + ' ', @@ -417,3 +390,31 @@ InlineSpan buildContent(BuildContext context, content) { // spanChilds.add(TextSpan(text: matchMember)); return TextSpan(children: spanChilds); } + +class UpTag extends StatelessWidget { + String? tagText; + UpTag({super.key, this.tagText = 'UP'}); + + @override + Widget build(BuildContext context) { + return Container( + width: tagText == 'UP' ? 28 : 38, + height: tagText == 'UP' ? 17 : 19, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(3), + // color: Theme.of(context).colorScheme.primary, + border: Border.all(color: Theme.of(context).colorScheme.primary)), + margin: const EdgeInsets.only(right: 4), + // padding: const EdgeInsets.symmetric(vertical: 0.5, horizontal: 4), + child: Center( + child: Text( + tagText!, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ); + } +} diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index ed2e1047..1ff41764 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -1,3 +1,4 @@ +import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; @@ -19,6 +20,10 @@ class _VideoDetailPageState extends State { @override Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + final double pinnedHeaderHeight = statusBarHeight + + kToolbarHeight + + MediaQuery.of(context).size.width * 9 / 16; return DefaultTabController( initialIndex: videoDetailController.tabInitialIndex, length: videoDetailController.tabs.length, // tab的数量. @@ -26,126 +31,115 @@ class _VideoDetailPageState extends State { top: false, bottom: false, child: Scaffold( - body: NestedScrollView( + body: ExtendedNestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ - SliverOverlapAbsorber( - handle: - NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - title: const Text("视频详情"), - // floating: true, - // snap: true, - pinned: true, - elevation: 0, - scrolledUnderElevation: 0, - forceElevated: innerBoxIsScrolled, - expandedHeight: MediaQuery.of(context).size.width * 9 / 16, - collapsedHeight: MediaQuery.of(context).size.width * 9 / 16, - toolbarHeight: kToolbarHeight, - flexibleSpace: FlexibleSpaceBar( - background: Padding( - padding: EdgeInsets.only( - bottom: 50, - top: MediaQuery.of(context).padding.top), - child: LayoutBuilder( - builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - double PR = MediaQuery.of(context).devicePixelRatio; - return Hero( - tag: videoDetailController.heroTag, - child: NetworkImgLayer( - src: videoDetailController.videoItem['pic'], - width: maxWidth, - height: maxHeight, - ), - ); - }, - ), - ), - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(50.0), - child: Container( - height: 50, - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context) - .dividerColor - .withOpacity(0.1)))), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Container( - width: 280, - margin: const EdgeInsets.only(left: 20), - child: Obx( - () => TabBar( - splashBorderRadius: BorderRadius.circular(6), - dividerColor: Colors.transparent, - tabs: videoDetailController.tabs - .map((String name) => Tab(text: name)) - .toList(), - ), - ), + SliverAppBar( + title: const Text("视频详情"), + pinned: true, + elevation: 0, + scrolledUnderElevation: 0, + forceElevated: innerBoxIsScrolled, + expandedHeight: MediaQuery.of(context).size.width * 9 / 16, + collapsedHeight: MediaQuery.of(context).size.width * 9 / 16, + flexibleSpace: FlexibleSpaceBar( + background: Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = MediaQuery.of(context).devicePixelRatio; + return Hero( + tag: videoDetailController.heroTag, + child: NetworkImgLayer( + src: videoDetailController.videoItem['pic'], + width: maxWidth, + height: maxHeight, ), - // 弹幕开关 - // const Spacer(), - // Flexible( - // flex: 2, - // child: Container( - // height: 50, - // ), - // ), - ], - ), + ); + }, ), ), ), ), ]; }, - body: TabBarView( + pinnedHeaderSliverHeightBuilder: () { + return pinnedHeaderHeight; + }, + onlyOneScrollInBody: true, + body: Column( children: [ - Builder(builder: (context) { - return CustomScrollView( - key: const PageStorageKey('简介'), - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context), + Container( + height: 50, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.withOpacity(0.1), ), - const VideoIntroPanel(), - SliverPadding( - padding: const EdgeInsets.only(top: 8, bottom: 5), - sliver: SliverToBoxAdapter( - child: Divider( - height: 1, - color: - Theme.of(context).dividerColor.withOpacity(0.1), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Container( + width: 280, + margin: const EdgeInsets.only(left: 20), + child: Obx( + () => TabBar( + splashBorderRadius: BorderRadius.circular(6), + dividerColor: Colors.transparent, + tabs: videoDetailController.tabs + .map((String name) => Tab(text: name)) + .toList(), ), ), ), - const RelatedVideoPanel(), + // 弹幕开关 + // const Spacer(), + // Flexible( + // flex: 2, + // child: Container( + // height: 50, + // ), + // ), ], - ); - }), - Builder(builder: (context) { - return CustomScrollView( - key: const PageStorageKey('评论'), - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context), + ), + ), + Expanded( + child: TabBarView( + children: [ + Builder( + builder: (context) { + return CustomScrollView( + key: const PageStorageKey('简介'), + slivers: [ + const VideoIntroPanel(), + SliverPadding( + padding: + const EdgeInsets.only(top: 8, bottom: 5), + sliver: SliverToBoxAdapter( + child: Divider( + height: 1, + color: Theme.of(context) + .dividerColor + .withOpacity(0.1), + ), + ), + ), + const RelatedVideoPanel(), + ], + ); + }, ), const VideoReplyPanel() ], - ); - }) + ), + ), ], ), ), diff --git a/pubspec.lock b/pubspec.lock index 2a6cbf57..24787131 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -153,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.3" + extended_nested_scroll_view: + dependency: "direct main" + description: + name: extended_nested_scroll_view + sha256: fc55b8f7e2c78701320d7eccda3b256387290b8498f0363d8ffd6f16760949d7 + url: "https://pub.dev" + source: hosted + version: "6.0.0" fake_async: dependency: transitive description: @@ -509,6 +517,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d" + url: "https://pub.dev" + source: hosted + version: "0.3.3" win32: dependency: transitive description: @@ -535,4 +551,4 @@ packages: version: "6.2.2" sdks: dart: ">=2.19.6 <3.0.0" - flutter: ">=3.4.0-17.0.pre" + flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7eaab185..45b193f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,8 @@ dependencies: # 存储 path_provider: ^2.0.14 + extended_nested_scroll_view: ^6.0.0 + dev_dependencies: flutter_test: sdk: flutter From b85c89e805701f71899e9daa29b50acc88af749f Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 28 Apr 2023 17:36:47 +0800 Subject: [PATCH 15/30] =?UTF-8?q?mod:=20=E5=9B=BE=E7=89=87=E9=A2=84?= =?UTF-8?q?=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Podfile.lock | 44 ++++ lib/common/widgets/appbar.dart | 33 +++ lib/pages/preview/controller.dart | 77 +++++++ lib/pages/preview/index.dart | 4 + lib/pages/preview/view.dart | 188 +++++++++++++++++ .../detail/reply/widgets/reply_item.dart | 74 +++++-- lib/router/app_pages.dart | 3 + lib/utils/utils.dart | 21 ++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 6 + pubspec.lock | 196 +++++++++++++++++- pubspec.yaml | 14 +- .../flutter/generated_plugin_registrant.cc | 9 + windows/flutter/generated_plugins.cmake | 3 + 15 files changed, 658 insertions(+), 19 deletions(-) create mode 100644 lib/common/widgets/appbar.dart create mode 100644 lib/pages/preview/controller.dart create mode 100644 lib/pages/preview/index.dart create mode 100644 lib/pages/preview/view.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e467820a..8576a625 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,46 +2,90 @@ PODS: - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift + - device_info_plus (0.0.1): + - Flutter - Flutter (1.0.0) + - flutter_inappwebview (0.0.1): + - Flutter + - flutter_inappwebview/Core (= 0.0.1) + - OrderedSet (~> 5.0) + - flutter_inappwebview/Core (0.0.1): + - Flutter + - OrderedSet (~> 5.0) - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) + - image_gallery_saver (1.5.0): + - Flutter + - OrderedSet (5.0.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - permission_handler_apple (9.0.4): + - Flutter - ReachabilitySwift (5.0.0) + - share_plus (0.0.1): + - Flutter - sqflite (0.0.2): - Flutter - FMDB (>= 2.7.5) + - url_launcher_ios (0.0.1): + - Flutter DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) + - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) + - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: trunk: - FMDB + - OrderedSet - ReachabilitySwift EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" Flutter: :path: Flutter + flutter_inappwebview: + :path: ".symlinks/plugins/flutter_inappwebview/ios" + image_gallery_saver: + :path: ".symlinks/plugins/image_gallery_saver/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e + device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2 + OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 diff --git a/lib/common/widgets/appbar.dart b/lib/common/widgets/appbar.dart new file mode 100644 index 00000000..ad2b0e3b --- /dev/null +++ b/lib/common/widgets/appbar.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class AppBarWidget extends StatelessWidget implements PreferredSizeWidget { + const AppBarWidget({ + required this.child, + required this.controller, + required this.visible, + Key? key, + }) : super(key: key); + + final PreferredSizeWidget child; + final AnimationController controller; + final bool visible; + + @override + // TODO: implement preferredSize + Size get preferredSize => child.preferredSize; + + @override + Widget build(BuildContext context) { + visible ? controller.reverse() : controller.forward(); + return SlideTransition( + position: Tween( + begin: Offset.zero, + end: const Offset(0, -1), + ).animate(CurvedAnimation( + parent: controller, + curve: Curves.easeInOutBack, + )), + child: child, + ); + } +} diff --git a/lib/pages/preview/controller.dart b/lib/pages/preview/controller.dart new file mode 100644 index 00000000..f2af8b9c --- /dev/null +++ b/lib/pages/preview/controller.dart @@ -0,0 +1,77 @@ +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:get/get.dart'; +import 'dart:typed_data'; +import 'package:dio/dio.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:pilipala/utils/utils.dart'; +import 'package:share_plus/share_plus.dart'; + +class PreviewController extends GetxController { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + RxInt initialPage = 0.obs; + RxInt currentPage = 1.obs; + RxList imgList = [].obs; + bool storage = true; + bool videos = true; + bool photos = true; + bool visiable = true; + + @override + void onInit() { + super.onInit(); + if (Get.arguments != null) { + initialPage.value = Get.arguments['initialPage']!; + currentPage.value = Get.arguments['initialPage']! + 1; + imgList.value = Get.arguments['imgList']; + } + } + + requestPermission() async { + Map statuses = await [ + Permission.storage, + // Permission.photos + ].request(); + + final info = statuses[Permission.storage].toString(); + // final photosInfo = statuses[Permission.photos].toString(); + + print('授权状态:$info'); + } + + // 图片保存 + void onSaveImg() async { + var response = await Dio().get(imgList[initialPage.value], + options: Options(responseType: ResponseType.bytes)); + final result = await ImageGallerySaver.saveImage( + Uint8List.fromList(response.data), + quality: 100, + name: "pic_vvex${DateTime.now().toString().split('-').join()}"); + if (result != null) { + if (result['isSuccess']) { + print('已保存到相册'); + } + } + } + + // 图片分享 + void onShareImg() async { + requestPermission(); + var response = await Dio().get(imgList[initialPage.value], + options: Options(responseType: ResponseType.bytes)); + final temp = await getTemporaryDirectory(); + String imgName = + "pic_vvex${DateTime.now().toString().split('-').join()}.jpg"; + var path = '${temp.path}/$imgName'; + File(path).writeAsBytesSync(response.data); + Share.shareXFiles([XFile(path)], subject: imgList[initialPage.value]); + } + + // 浏览器中查看 + void onBrowserImg() async { + Utils.openURL(imgList[initialPage.value]); + } +} diff --git a/lib/pages/preview/index.dart b/lib/pages/preview/index.dart new file mode 100644 index 00000000..9fb82e8d --- /dev/null +++ b/lib/pages/preview/index.dart @@ -0,0 +1,4 @@ +library preview; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/preview/view.dart b/lib/pages/preview/view.dart new file mode 100644 index 00000000..d82cd45e --- /dev/null +++ b/lib/pages/preview/view.dart @@ -0,0 +1,188 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:extended_image/extended_image.dart'; +import 'package:pilipala/common/widgets/appbar.dart'; +import 'controller.dart'; + +typedef DoubleClickAnimationListener = void Function(); + +class ImagePreview extends StatefulWidget { + const ImagePreview({Key? key}) : super(key: key); + + @override + _ImagePreviewState createState() => _ImagePreviewState(); +} + +class _ImagePreviewState extends State + with TickerProviderStateMixin { + final PreviewController _previewController = Get.put(PreviewController()); + late AnimationController animationController; + late AnimationController _doubleClickAnimationController; + Animation? _doubleClickAnimation; + late DoubleClickAnimationListener _doubleClickAnimationListener; + List doubleTapScales = [1.0, 2.0]; + + @override + void initState() { + super.initState(); + animationController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 400)); + _doubleClickAnimationController = AnimationController( + duration: const Duration(milliseconds: 250), vsync: this); + } + + @override + void dispose() { + animationController.dispose(); + _doubleClickAnimationController.dispose(); + clearGestureDetailsCache(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBarWidget( + controller: animationController, + visible: _previewController.visiable, + child: AppBar( + backgroundColor: Theme.of(context).colorScheme.background, + elevation: 0, + centerTitle: false, + title: Obx( + () => Text.rich( + TextSpan(children: [ + TextSpan(text: _previewController.currentPage.toString()), + const TextSpan(text: ' / '), + TextSpan(text: _previewController.imgList.length.toString()), + ]), + ), + ), + actions: [ + PopupMenuButton( + icon: const Icon(Icons.more_vert), + tooltip: 'action', + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + value: 'share', + onTap: _previewController.onShareImg, + child: const Text('分享'), + ), + PopupMenuItem( + value: 'save', + onTap: _previewController.onSaveImg, + child: const Text('保存'), + ), + PopupMenuItem( + value: 'browser', + onTap: _previewController.onBrowserImg, + child: const Text('浏览器中查看'), + ), + ], + ), + ], + ), + ), + body: GestureDetector( + onTap: () { + _previewController.visiable = !_previewController.visiable; + setState(() {}); + }, + child: ExtendedImageGesturePageView.builder( + controller: ExtendedPageController( + initialPage: _previewController.initialPage.value, + pageSpacing: 0, + ), + onPageChanged: (int index) { + _previewController.initialPage.value = index; + _previewController.currentPage.value = index + 1; + }, + canScrollPage: (GestureDetails? gestureDetails) => + gestureDetails!.totalScale! <= 1.0, + preloadPagesCount: 2, + itemCount: _previewController.imgList.length, + itemBuilder: (BuildContext context, int index) { + return ExtendedImage.network( + _previewController.imgList[index], + fit: BoxFit.contain, + mode: ExtendedImageMode.gesture, + onDoubleTap: (ExtendedImageGestureState state) { + final Offset? pointerDownPosition = state.pointerDownPosition; + final double? begin = state.gestureDetails!.totalScale; + double end; + + //remove old + _doubleClickAnimation + ?.removeListener(_doubleClickAnimationListener); + + //stop pre + _doubleClickAnimationController.stop(); + + //reset to use + _doubleClickAnimationController.reset(); + + if (begin == doubleTapScales[0]) { + end = doubleTapScales[1]; + } else { + end = doubleTapScales[0]; + } + + _doubleClickAnimationListener = () { + state.handleDoubleTap( + scale: _doubleClickAnimation!.value, + doubleTapPosition: pointerDownPosition); + }; + _doubleClickAnimation = _doubleClickAnimationController + .drive(Tween(begin: begin, end: end)); + + _doubleClickAnimation! + .addListener(_doubleClickAnimationListener); + + _doubleClickAnimationController.forward(); + }, + loadStateChanged: (ExtendedImageState state) { + if (state.extendedImageLoadState == LoadState.loading) { + final ImageChunkEvent? loadingProgress = + state.loadingProgress; + final double? progress = + loadingProgress?.expectedTotalBytes != null + ? loadingProgress!.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 150.0, + child: LinearProgressIndicator(value: progress), + ), + const SizedBox(height: 10.0), + Text('${((progress ?? 0.0) * 100).toInt()}%'), + ], + ), + ); + } + }, + initGestureConfigHandler: (ExtendedImageState state) { + return GestureConfig( + inPageView: true, + initialScale: 1.0, + maxScale: 5.0, + animationMaxScale: 6.0, + initialAlignment: InitialAlignment.center, + ); + }, + ); + }, + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _previewController.onSaveImg(), + child: const Icon(Icons.save_alt_rounded), + ), + ); + } +} diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index b5b78747..93ca58d1 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -1,5 +1,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/utils/utils.dart'; @@ -367,25 +368,66 @@ InlineSpan buildContent(BuildContext context, content) { // 图片渲染 if (content.pictures.isNotEmpty) { - spanChilds.add( - const TextSpan(text: '\n'), - ); - for (var i = 0; i < content.pictures.length; i++) { - spanChilds.add( - WidgetSpan( - child: SizedBox( - height: 180, - child: NetworkImgLayer( - src: content.pictures[i]['img_src'], - width: 200, - height: 200 * - content.pictures[i]['img_height'] / - content.pictures[i]['img_width'], - ), - ), + List list = []; + List picList = []; + int len = content.pictures.length; + for (var i = 0; i < len; i++) { + picList.add(content.pictures[i]['img_src']); + list.add( + LayoutBuilder( + builder: (context, BoxConstraints box) { + return GestureDetector( + onTap: () { + Get.toNamed('/preview', + arguments: {'initialPage': i, 'imgList': picList}); + }, + child: NetworkImgLayer( + src: content.pictures[i]['img_src'], + width: box.maxWidth, + height: box.maxWidth, + ), + ); + }, ), ); } + spanChilds.add( + WidgetSpan( + child: LayoutBuilder( + builder: (context, BoxConstraints box) { + double maxWidth = box.maxWidth; + double crossCount = len < 3 ? 2 : 3; + double height = maxWidth / + crossCount * + (len % crossCount == 0 + ? len ~/ crossCount + : len ~/ crossCount + 1) + + 6; + return Container( + padding: const EdgeInsets.only(top: 6), + height: height, + child: GridView( + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + // 子Item排列规则 + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + //横轴元素个数 + crossAxisCount: crossCount.toInt(), + //纵轴间距 + mainAxisSpacing: 4.0, + //横轴间距 + crossAxisSpacing: 4.0, + //子组件宽高长度比例 + // childAspectRatio: 1, + ), + //GridView中使用的子Widegt + children: list, + ), + ); + }, + ), + ), + ); } // spanChilds.add(TextSpan(text: matchMember)); return TextSpan(children: spanChilds); diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 9ec967f8..0a0bd88b 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/hot/index.dart'; +import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/video/detail/index.dart'; class Routes { @@ -11,5 +12,7 @@ class Routes { GetPage(name: '/hot', page: () => const HotPage()), // 视频详情 GetPage(name: '/video', page: () => const VideoDetailPage()), + // 图片预览 + GetPage(name: '/preview', page: () => const ImagePreview()) ]; } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 084a6e6f..03d30181 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,10 +1,13 @@ // 工具函数 import 'dart:io'; import 'dart:math'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:get/get_utils/get_utils.dart'; import 'package:path_provider/path_provider.dart'; class Utils { + final ChromeSafariBrowser browser = ChromeSafariBrowser(); + static Future getCookiePath() async { Directory tempDir = await getApplicationSupportDirectory(); String tempPath = "${tempDir.path}/.plpl/"; @@ -135,4 +138,22 @@ class Utils { static String makeHeroTag(v) { return v.toString() + Random().nextInt(9999).toString(); } + + static openURL(aUrl) async { + try { + await Utils().browser.open( + url: Uri.parse(aUrl), + options: ChromeSafariBrowserClassOptions( + android: AndroidChromeCustomTabsOptions( + shareState: CustomTabsShareState.SHARE_STATE_OFF, + isSingleInstance: false, + isTrustedWebActivity: false, + keepAliveEnabled: true, + ), + ), + ); + } catch (err) { + await InAppBrowser.openWithSystemBrowser(url: Uri.parse(aUrl)); + } + } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 675b7194..fe56f8d8 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 3e303c15..18366213 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 65feaf20..bb5265fd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,13 +6,19 @@ import FlutterMacOS import Foundation import connectivity_plus +import device_info_plus import dynamic_color import path_provider_foundation +import share_plus import sqflite +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 24787131..9f021f27 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + url: "https://pub.dev" + source: hosted + version: "0.3.3+4" crypto: dependency: transitive description: @@ -121,6 +129,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.8" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903 + url: "https://pub.dev" + source: hosted + version: "8.2.2" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" dio: dependency: "direct main" description: @@ -153,6 +177,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.3" + extended_image: + dependency: "direct main" + description: + name: extended_image + sha256: "75e4b0ad0f8f63eed7935ff2506c809a670f5e6dd0f61304525879d53fc41a17" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + extended_image_library: + dependency: transitive + description: + name: extended_image_library + sha256: "550743b43ab093aed35ef234500fcc7a304cbac1eca47b0cc991e07e88750758" + url: "https://pub.dev" + source: hosted + version: "3.4.2" extended_nested_scroll_view: dependency: "direct main" description: @@ -206,6 +246,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.0" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: "1c370ac07de80a579a0047c94c5bb586128d4ef50c0f3f501d6e77010374a319" + url: "https://pub.dev" + source: hosted + version: "5.4.4" flutter_lints: dependency: "direct dev" description: @@ -248,6 +296,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + http_client_helper: + dependency: transitive + description: + name: http_client_helper + sha256: "1f32359bd07a064ad256d1f84ae5f973f69bc972e7287223fa198abe1d969c28" + url: "https://pub.dev" + source: hosted + version: "2.0.3" http_parser: dependency: transitive description: @@ -256,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_gallery_saver: + dependency: "direct main" + description: + name: image_gallery_saver + sha256: be812580c7a320d3bf583af89cac6b376f170d48000aca75215a73285a3223a0 + url: "https://pub.dev" + source: hosted + version: "1.7.1" js: dependency: transitive description: @@ -296,6 +360,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" nm: dependency: transitive description: @@ -376,6 +448,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85 + url: "https://pub.dev" + source: hosted + version: "9.0.8" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84" + url: "https://pub.dev" + source: hosted + version: "3.9.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + url: "https://pub.dev" + source: hosted + version: "0.1.2" petitparser: dependency: transitive description: @@ -416,6 +528,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: b1f15232d41e9701ab2f04181f21610c36c83a12ae426b79b4bd011c567934b1 + url: "https://pub.dev" + source: hosted + version: "6.3.4" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981" + url: "https://pub.dev" + source: hosted + version: "3.2.1" sky_engine: dependency: transitive description: flutter @@ -501,6 +629,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" + url: "https://pub.dev" + source: hosted + version: "6.1.10" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "22f8db4a72be26e9e3a4aa3f194b1f7afbc76d20ec141f84be1d787db2155cbd" + url: "https://pub.dev" + source: hosted + version: "6.0.31" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + url: "https://pub.dev" + source: hosted + version: "2.0.16" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" + url: "https://pub.dev" + source: hosted + version: "3.0.6" uuid: dependency: transitive description: @@ -529,10 +721,10 @@ packages: dependency: transitive description: name: win32 - sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "3.1.4" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 45b193f6..efadde4b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,10 +48,22 @@ dependencies: # 图片 cached_network_image: ^3.2.3 - + extended_image: ^7.0.2 + image_gallery_saver: ^1.7.1 + # 存储 path_provider: ^2.0.14 + # 设备信息 + device_info_plus: ^8.2.0 + # 权限 + permission_handler: ^10.2.0 + # 分享 + share_plus: ^6.3.1 + # webView + url_launcher: ^6.1.9 + flutter_inappwebview: 5.4.4 + extended_nested_scroll_view: ^6.0.0 dev_dependencies: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index ae0cf3ff..efd465a4 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,19 @@ #include #include +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e47678f2..afce192f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus dynamic_color + permission_handler_windows + share_plus + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From e971fbc8768287e42f0adb2a9a281b7ac5f527fb Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 28 Apr 2023 23:05:29 +0800 Subject: [PATCH 16/30] =?UTF-8?q?mod:=20=E6=A5=BC=E4=B8=AD=E6=A5=BC?= =?UTF-8?q?=E5=9B=9E=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 3 + lib/http/reply.dart | 34 +++++++++++ lib/models/video/reply/item.dart | 2 +- lib/pages/video/detail/reply/controller.dart | 23 +++++-- lib/pages/video/detail/reply/view.dart | 24 +++++++- .../detail/reply/widgets/reply_item.dart | 60 ++++++++++++++++++- lib/pages/video/detail/view.dart | 2 +- 7 files changed, 136 insertions(+), 12 deletions(-) diff --git a/lib/http/api.dart b/lib/http/api.dart index de89bee9..068d7d24 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -16,4 +16,7 @@ class Api { // 评论列表 static const String replyList = '/x/v2/reply'; + + // 楼中楼 + static const String replyReplyList = '/x/v2/reply/reply'; } diff --git a/lib/http/reply.dart b/lib/http/reply.dart index 88000996..e9a609d2 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -33,4 +33,38 @@ class ReplyHttp { }; } } + + static Future replyReplyList({ + required String oid, + required String root, + required int pageNum, + required int type, + int sort = 1, + }) async { + var res = await Request().get(Api.replyReplyList, data: { + 'oid': oid, + 'root': root, + 'pn': pageNum, + 'type': type, + 'sort': 1, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + Map errMap = { + -400: '请求错误', + -404: '无此项', + 12002: '评论区已关闭', + 12009: '评论主体的type不合法', + }; + return { + 'status': false, + 'date': [], + 'msg': errMap[res.data['code']] ?? '请求异常', + }; + } + } } diff --git a/lib/models/video/reply/item.dart b/lib/models/video/reply/item.dart index 53b71b6e..c6838a13 100644 --- a/lib/models/video/reply/item.dart +++ b/lib/models/video/reply/item.dart @@ -149,6 +149,6 @@ class ReplyControl { entryText = json['sub_reply_entry_text']; titleText = json['sub_reply_title_text']; time = json['time_desc']; - location = json['location']; + location = json['location'] ?? ''; } } diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index 23447a37..a82241d5 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/reply.dart'; @@ -5,9 +7,18 @@ import 'package:pilipala/models/video/reply/data.dart'; import 'package:pilipala/models/video/reply/item.dart'; class VideoReplyController extends GetxController { + VideoReplyController( + this.aid, + this.rpid, + this.level, + ); final ScrollController scrollController = ScrollController(); - // 视频aid - String aid = Get.parameters['aid']!; + // 视频aid 请求时使用的oid + String? aid; + // 层级 2为楼中楼 + String? level; + // rpid 请求楼中楼回复 + String? rpid; RxList replyList = [ReplyItemModel()].obs; // 当前页 int currentPage = 0; @@ -16,8 +27,11 @@ class VideoReplyController extends GetxController { Future queryReplyList({type = 'init'}) async { isLoadingMore = true; - var res = - await ReplyHttp.replyList(oid: aid, pageNum: currentPage + 1, type: 1); + var res = level == '1' + ? await ReplyHttp.replyList( + oid: aid!, pageNum: currentPage + 1, type: 1) + : await ReplyHttp.replyReplyList( + oid: aid!, root: rpid!, pageNum: currentPage + 1, type: 1); if (res['status']) { res['data'] = ReplyData.fromJson(res['data']); if (res['data'].replies.isNotEmpty) { @@ -27,6 +41,7 @@ class VideoReplyController extends GetxController { if (currentPage == 0) { } else { noMore = true; + return; } } if (type == 'init') { diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index b1ce3fad..c91ae3fa 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -7,7 +7,15 @@ import 'controller.dart'; import 'widgets/reply_item.dart'; class VideoReplyPanel extends StatefulWidget { - const VideoReplyPanel({super.key}); + int oid; + int rpid; + String level; + VideoReplyPanel({ + this.oid = 0, + this.rpid = 0, + this.level = '', + super.key, + }); @override State createState() => _VideoReplyPanelState(); @@ -15,8 +23,8 @@ class VideoReplyPanel extends StatefulWidget { class _VideoReplyPanelState extends State with AutomaticKeepAliveClientMixin { - final VideoReplyController _videoReplyController = - Get.put(VideoReplyController(), tag: Get.arguments['heroTag']); + late VideoReplyController _videoReplyController; + // List? replyList; Future? _futureBuilderFuture; // 添加页面缓存 @@ -26,6 +34,16 @@ class _VideoReplyPanelState extends State @override void initState() { super.initState(); + if (widget.level == '2') { + _videoReplyController = Get.put( + VideoReplyController( + widget.oid.toString(), widget.rpid.toString(), '2'), + tag: widget.rpid.toString()); + } else { + _videoReplyController = Get.put( + VideoReplyController(Get.parameters['aid']!, '', '1'), + tag: Get.arguments['heroTag']); + } _futureBuilderFuture = _videoReplyController.queryReplyList(); _videoReplyController.scrollController.addListener( diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 93ca58d1..2faba135 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/video/reply/item.dart'; +import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/utils/utils.dart'; class ReplyItem extends StatelessWidget { @@ -16,11 +17,13 @@ class ReplyItem extends StatelessWidget { child: Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB(12, 6, 8, 0), + padding: const EdgeInsets.fromLTRB(12, 8, 8, 2), child: content(context), ), Divider( height: 1, + indent: 52, + endIndent: 10, color: Theme.of(context).dividerColor.withOpacity(0.08), ) ], @@ -113,6 +116,7 @@ class ReplyItem extends StatelessWidget { child: ReplyItemRow( replies: replyItem!.replies, replyControl: replyItem!.replyControl, + f_rpid: replyItem!.rpid, ), ), ], @@ -171,9 +175,11 @@ class ReplyItemRow extends StatelessWidget { super.key, this.replies, this.replyControl, + this.f_rpid, }); List? replies; ReplyControl? replyControl; + int? f_rpid; @override Widget build(BuildContext context) { @@ -195,7 +201,7 @@ class ReplyItemRow extends StatelessWidget { if (extraRow == 1 && index == replies!.length) { // 有楼中楼回复,在最后显示 return InkWell( - onTap: () {}, + onTap: () => replyReply(context), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), @@ -223,7 +229,13 @@ class ReplyItemRow extends StatelessWidget { return InkWell( onTap: () {}, child: Padding( - padding: EdgeInsets.fromLTRB(8, index == 0 ? 8 : 4, 8, 4), + padding: EdgeInsets.fromLTRB( + 8, + index == 0 && (extraRow == 1 || replies!.length > 1) + ? 8 + : 5, + 8, + 5), child: Text.rich( overflow: extraRow == 1 ? TextOverflow.ellipsis @@ -260,6 +272,48 @@ class ReplyItemRow extends StatelessWidget { ), ); } + + void replyReply(context) { + Get.bottomSheet( + barrierColor: Colors.transparent, + useRootNavigator: true, + isScrollControlled: true, + Container( + height: Get.size.height - Get.size.width * 9 / 16 - 50, + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + AppBar( + automaticallyImplyLeading: false, + centerTitle: false, + title: Text( + '评论详情', + style: Theme.of(context).textTheme.titleMedium, + ), + actions: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + await Future.delayed(const Duration(milliseconds: 200)); + Get.back(); + }, + ) + ], + ), + Expanded( + child: VideoReplyPanel( + oid: replies!.first.oid, + rpid: f_rpid!, + level: '2', + ), + ) + ], + ), + ), + persistent: false, + backgroundColor: Theme.of(context).bottomSheetTheme.backgroundColor, + ); + } } InlineSpan buildContent(BuildContext context, content) { diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 1ff41764..6747ea69 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -136,7 +136,7 @@ class _VideoDetailPageState extends State { ); }, ), - const VideoReplyPanel() + VideoReplyPanel() ], ), ), From 42fd0d7f6264406830985e4782d7331ae1cb15c3 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 29 Apr 2023 17:30:54 +0800 Subject: [PATCH 17/30] =?UTF-8?q?mod:=20=E6=8E=92=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle | 1 + assets/fonts/fansCard.ttf | Bin 0 -> 3152 bytes lib/common/widgets/network_img_layer.dart | 7 +- lib/models/video/reply/member.dart | 20 ++- lib/pages/video/detail/reply/controller.dart | 9 +- lib/pages/video/detail/reply/view.dart | 7 +- .../detail/reply/widgets/reply_item.dart | 144 +++++++++++++----- pubspec.yaml | 18 +-- 8 files changed, 144 insertions(+), 62 deletions(-) create mode 100644 assets/fonts/fansCard.ttf diff --git a/android/app/build.gradle b/android/app/build.gradle index 4c258354..262f823b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -51,6 +51,7 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + minSdkVersion 17 } buildTypes { diff --git a/assets/fonts/fansCard.ttf b/assets/fonts/fansCard.ttf new file mode 100644 index 0000000000000000000000000000000000000000..09ade3a0d4324f31a43d08e6eda79c4a721b4e91 GIT binary patch literal 3152 zcmeHJTTB#J82--80aE)Y!`3~k_HLljo^i)L87Z=g>_k3b~no+SX<*m z9|#gcEgI4oqNW;a2{DETVhlD*V;?l6Qqs`Gho(u}#56uxW18&H@5~Oiwqn|*4{g#j zIdlH=pa1gx-N%DiginF03w5ZuRnP7Q27TZ zdjO?{eSM41+idf+UIZ*>Y5#f)6{-Eo1n1M7zueLm>ss5KlFIpG&P#*gCa-#5JpiOQ zIbYr8?TTO#GCAMPxfb%a`EH*oDCRr@q@Ig}+he$mYd}U8*UMp19*TaXV1WXK3Suxu z%Ecm`Gm&aB)GP-=y3Doe%57Uw0S9)ek=gZ#sS(_P1+wM%{N6rkEA6wD@x(cCOMg$iLwjYWZ=iRmm)r^*Wc*Ra=aZIaUqg)S*~;jk{>_Kt zIxVG3;_(-Tt%tT_Ni4!ZK|DmHtk%mFk6td~iI0TiLbtefwnul9gE@QEKu=S%0_ia$ z^4aO3czUn&oa#yB3wb1sdMBBKd*9QlCE?v^64Vq0XHyGEzlJp>ra!X#-%alg4b7^?Er@xyOS4bQZ z`;_uvju)}U&aF|V2K3Q01Hxlb?(3_?w~3|Vk!~e7$y)Ew-q>&Mq&<((l{>^fqkk$Y z=#W_2CngOZWqddvEdJ!Jg>U=>C3(E>PBm}fD!#6tcvn6ZMs^q{Q>qOg{HEy~?{VelkAJ ze%_xRKeM|wST`|dvvziNI-)+`;eewl+-9@Z)|M5OlsHRWg(bzsHtP;=AXw&>%ZI|D zSVg!a77T}5rKy1`8M<+iuRK09@&2*+i9(0d?elj8z0rBs=Uu4xMcV`6kj5CA_7p7b zDO8#^KPp144ir=d{G_ML8weFt2E9$KTPy1s^uNU~HX)1%jv|Tx{AfW8`DnsAXei=2 zDTIrX8=HAmV>{*3$ihDC!G3(lx9UdN_+71|pBEweYlajsd^#9WLj&(}O=G4ge3WC< z9HFG4iuZOSZ*o1%@3F>HA44;xS)N20eJ6VxI*rW7ag%6PG0H-m#5ntTW=+Q#>_#nu zMDp11>7;!pW5|p?9A*v&s|quYj9AO7jP;f9a`NB6#X3t+%uBBBVDtcEl`+$t_aXX* zh^vBQ2ge}CFj}8Ponu9g5@RF(jePv;8Nyv0<1bAiEuBU@KmCKu5Pibr^L_qY;`kT?VHiLo2IqB6nM9QEwvs|0(<@ lp2BB+v3~uAWy-&G*s&N(kd38ShUNSwt>kIS;Vu*bzW`2oYTf_< literal 0 HcmV?d00001 diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index ce98c9a9..a09ec850 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -30,8 +30,11 @@ class NetworkImgLayer extends StatelessWidget { // double pr = 2; return src != '' ? ClipRRect( - borderRadius: BorderRadius.circular( - type == 'avatar' ? 50 : StyleString.imgRadius.x), + borderRadius: BorderRadius.circular(type == 'avatar' + ? 50 + : type == 'emote' + ? 0 + : StyleString.imgRadius.x), child: CachedNetworkImage( imageUrl: src!, width: width ?? double.infinity, diff --git a/lib/models/video/reply/member.dart b/lib/models/video/reply/member.dart index 196f252b..5576cbd1 100644 --- a/lib/models/video/reply/member.dart +++ b/lib/models/video/reply/member.dart @@ -1,4 +1,4 @@ -import 'dart:convert' as convert; +import 'package:get/get.dart'; class ReplyMember { ReplyMember({ @@ -22,6 +22,7 @@ class ReplyMember { Map? officialVerify; Map? vip; Map? fansDetail; + UserSailing? userSailing; ReplyMember.fromJson(Map json) { mid = json['mid']; @@ -30,9 +31,12 @@ class ReplyMember { avatar = json['avatar']; level = json['level_info']['current_level']; pendant = Pendant.fromJson(json['pendant']); - officialVerify = json['officia_vVerify']; + officialVerify = json['officia_verify']; vip = json['vip']; fansDetail = json['fans_detail']; + userSailing = json['user_sailing'] != null + ? UserSailing.fromJson(json['user_sailing']) + : UserSailing(); } } @@ -53,3 +57,15 @@ class Pendant { image = json['image']; } } + +class UserSailing { + UserSailing({this.pendant, this.cardbg}); + + Map? pendant; + Map? cardbg; + + UserSailing.fromJson(Map json) { + pendant = json['pendant']; + cardbg = json['cardbg']; + } +} diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index a82241d5..6e813fc8 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -23,7 +23,7 @@ class VideoReplyController extends GetxController { // 当前页 int currentPage = 0; bool isLoadingMore = false; - bool noMore = false; + RxBool noMore = false.obs; Future queryReplyList({type = 'init'}) async { isLoadingMore = true; @@ -36,14 +36,17 @@ class VideoReplyController extends GetxController { res['data'] = ReplyData.fromJson(res['data']); if (res['data'].replies.isNotEmpty) { currentPage = currentPage + 1; - noMore = false; + noMore.value = false; } else { if (currentPage == 0) { } else { - noMore = true; + noMore.value = true; return; } } + if (res['data'].replies.length >= res['data'].page.count) { + noMore.value = true; + } if (type == 'init') { List replies = res['data'].replies; // 添加置顶回复 diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index c91ae3fa..e1b2544a 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -90,9 +90,10 @@ class _VideoReplyPanelState extends State height: MediaQuery.of(context).padding.bottom + 60, child: Center( - child: Text(_videoReplyController.noMore - ? '没有更多了' - : '加载中'), + child: Obx(() => Text( + _videoReplyController.noMore.value + ? '没有更多了' + : '加载中')), ), ); } else { diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 2faba135..73bafe2f 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -33,14 +33,42 @@ class ReplyItem extends StatelessWidget { Widget lfAvtar(context) { return Container( - margin: const EdgeInsets.only(top: 5), - child: NetworkImgLayer( - src: replyItem!.member!.avatar, - width: 30, - height: 30, - type: 'avatar', - ), - ); + margin: const EdgeInsets.only(top: 5), + child: Stack( + children: [ + NetworkImgLayer( + src: replyItem!.member!.avatar, + width: 34, + height: 34, + type: 'avatar', + ), + if (replyItem!.member!.officialVerify != null && + replyItem!.member!.officialVerify!['type'] == 0) + Positioned( + right: 0, + bottom: 0, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(7), + color: Theme.of(context).colorScheme.background, + ), + child: Icon( + Icons.offline_bolt, + color: Theme.of(context).colorScheme.primary, + size: 16, + ), + ), + ), + ], + ) + // child: + // NetworkImgLayer( + // src: replyItem!.member!.avatar, + // width: 30, + // height: 30, + // type: 'avatar', + // ), + ); } Widget content(context) { @@ -54,39 +82,73 @@ class ReplyItem extends StatelessWidget { // 'memberAvatar': reply.avatar, // 'heroTag': reply.userName + heroTag, // }), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - lfAvtar(context), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - replyItem!.member!.uname!, - style: TextStyle( - color: replyItem!.isUp! - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, - fontSize: - Theme.of(context).textTheme.titleSmall!.fontSize, - ), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + image: replyItem!.member!.userSailing!.cardbg != null + ? DecorationImage( + fit: BoxFit.cover, + image: NetworkImage( + replyItem!.member!.userSailing!.cardbg!['image'], ), - const SizedBox(width: 6), - Image.asset( - 'assets/images/lv/lv${replyItem!.member!.level}.png', - height: 11, + ) + : null, + ), + child: Stack( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + lfAvtar(context), + const SizedBox(width: 12), + Text( + replyItem!.member!.uname!, + style: TextStyle( + color: replyItem!.isUp! || + replyItem!.member!.vip!['vipType'] > 0 + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + fontSize: + Theme.of(context).textTheme.titleSmall!.fontSize, ), - const SizedBox(width: 6), - if (replyItem!.isUp!) UpTag() - ], + ), + const SizedBox(width: 6), + Image.asset( + 'assets/images/lv/lv${replyItem!.member!.level}.png', + height: 11, + ), + const SizedBox(width: 6), + if (replyItem!.isUp!) UpTag(), + ], + ), + if (replyItem!.member!.userSailing!.cardbg != null && + replyItem!.member!.userSailing!.cardbg!['fan']['number'] > + 0) + Positioned( + top: 8, + left: Get.size.width / 7 * 5.6, + child: DefaultTextStyle( + style: TextStyle( + fontFamily: 'fansCard', + fontSize: 9, + color: Theme.of(context).colorScheme.primary, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('NO.'), + Text( + replyItem!.member!.userSailing!.cardbg!['fan'] + ['num_desc'], + ), + ], + ), + ), ), - ], - ) - ], + ], + ), ), ), // title @@ -329,14 +391,16 @@ InlineSpan buildContent(BuildContext context, content) { RegExp(r"\[.*?\]"), onMatch: (Match match) { String matchStr = match[0]!; + int size = content.emote[matchStr]['meta']['size']; if (content.emote.isNotEmpty) { if (content.emote.keys.contains(matchStr)) { spanChilds.add( WidgetSpan( child: NetworkImgLayer( src: content.emote[matchStr]['url'], - width: 20, - height: 20, + type: 'emote', + width: size * 20, + height: size * 20, ), ), ); diff --git a/pubspec.yaml b/pubspec.yaml index efadde4b..7b78fe6a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -102,17 +102,11 @@ flutter: # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # + fonts: + - family: fansCard + fonts: + - asset: assets/fonts/fansCard.ttf + + # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages From 5dabb94a476a5def75e9cf6de444d5857332b90a Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 29 Apr 2023 19:08:39 +0800 Subject: [PATCH 18/30] =?UTF-8?q?mod:=20=E9=AA=A8=E6=9E=B6=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/skeleton/video_reply.dart | 84 ++++++++++++++++++++++++++ lib/pages/video/detail/reply/view.dart | 3 +- 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 lib/common/skeleton/video_reply.dart diff --git a/lib/common/skeleton/video_reply.dart b/lib/common/skeleton/video_reply.dart new file mode 100644 index 00000000..09efa8ce --- /dev/null +++ b/lib/common/skeleton/video_reply.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'skeleton.dart'; + +class VideoReplySkeleton extends StatelessWidget { + const VideoReplySkeleton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Color bgColor = Theme.of(context).colorScheme.onInverseSurface; + return Skeleton( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 8, 2), + child: Row( + children: [ + ClipOval( + child: Container( + width: 34, + height: 34, + color: bgColor, + ), + ), + const SizedBox(width: 12), + Container( + width: 120, + height: 16, + color: bgColor, + ) + ], + ), + ), + Container( + width: double.infinity, + margin: + const EdgeInsets.only(top: 4, left: 57, right: 6, bottom: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 300, + height: 16, + margin: const EdgeInsets.only(bottom: 4), + color: bgColor, + ), + Container( + width: 180, + height: 16, + margin: const EdgeInsets.only(bottom: 6), + color: bgColor, + ), + Row( + children: [ + Container( + width: 60, + height: 16, + margin: const EdgeInsets.only(bottom: 4), + color: bgColor, + ), + const Spacer(), + Container( + width: 60, + height: 16, + margin: const EdgeInsets.only(bottom: 4), + color: bgColor, + ), + const SizedBox(width: 8) + ], + ) + ], + ), + ), + Divider( + height: 1, + indent: 52, + endIndent: 10, + color: Theme.of(context).dividerColor.withOpacity(0.08), + ) + ], + ), + ); + } +} diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index e1b2544a..0f8cae68 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; +import 'package:pilipala/common/skeleton/video_reply.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/models/video/reply/item.dart'; import 'controller.dart'; @@ -117,7 +118,7 @@ class _VideoReplyPanelState extends State // 骨架屏 return SliverList( delegate: SliverChildBuilderDelegate((context, index) { - return const VideoCardHSkeleton(); + return const VideoReplySkeleton(); }, childCount: 5), ); } From a66bb08ca85fe8a0480e7ac124625216e1bddf26 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 7 May 2023 17:36:06 +0800 Subject: [PATCH 19/30] =?UTF-8?q?mod:=20=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/fonts/ArchivoNarrow-BoldItalic.ttf | Bin 0 -> 60736 bytes lib/common/skeleton/video_card_h.dart | 2 +- lib/common/skeleton/video_card_v.dart | 6 - lib/common/skeleton/video_reply.dart | 18 +- lib/main.dart | 6 +- lib/models/video/reply/content.dart | 3 + lib/pages/home/widgets/app_bar.dart | 14 +- lib/pages/main/view.dart | 2 +- lib/pages/mine/view.dart | 184 +++++++++++- lib/pages/video/detail/introduction/view.dart | 253 ++++++++--------- lib/pages/video/detail/related/view.dart | 1 + .../detail/reply/widgets/reply_item.dart | 268 +++++++++++------- pubspec.lock | 8 + pubspec.yaml | 4 + 14 files changed, 509 insertions(+), 260 deletions(-) create mode 100644 assets/fonts/ArchivoNarrow-BoldItalic.ttf diff --git a/assets/fonts/ArchivoNarrow-BoldItalic.ttf b/assets/fonts/ArchivoNarrow-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69fb6a58e756296e3a733f9acccec7f1bc98c03 GIT binary patch literal 60736 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 D*#@&= literal 0 HcmV?d00001 diff --git a/lib/common/skeleton/video_card_h.dart b/lib/common/skeleton/video_card_h.dart index dc5e9c32..46b858d3 100644 --- a/lib/common/skeleton/video_card_h.dart +++ b/lib/common/skeleton/video_card_h.dart @@ -59,7 +59,7 @@ class _VideoCardHSkeletonState extends State { .colorScheme .onInverseSurface, width: 200, - height: 13, + height: 11, margin: const EdgeInsets.only(bottom: 5), ), Container( diff --git a/lib/common/skeleton/video_card_v.dart b/lib/common/skeleton/video_card_v.dart index 4f83501e..aeff595f 100644 --- a/lib/common/skeleton/video_card_v.dart +++ b/lib/common/skeleton/video_card_v.dart @@ -25,12 +25,6 @@ class VideoCardVSkeleton extends StatelessWidget { decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: BorderRadius.circular(6), - border: Border.all( - color: Theme.of(context) - .colorScheme - .outline - .withOpacity(0.1), - ), ), ); }, diff --git a/lib/common/skeleton/video_reply.dart b/lib/common/skeleton/video_reply.dart index 09efa8ce..219540eb 100644 --- a/lib/common/skeleton/video_reply.dart +++ b/lib/common/skeleton/video_reply.dart @@ -23,8 +23,8 @@ class VideoReplySkeleton extends StatelessWidget { ), const SizedBox(width: 12), Container( - width: 120, - height: 16, + width: 80, + height: 13, color: bgColor, ) ], @@ -40,28 +40,28 @@ class VideoReplySkeleton extends StatelessWidget { children: [ Container( width: 300, - height: 16, + height: 14, margin: const EdgeInsets.only(bottom: 4), color: bgColor, ), Container( width: 180, - height: 16, - margin: const EdgeInsets.only(bottom: 6), + height: 14, + margin: const EdgeInsets.only(bottom: 10), color: bgColor, ), Row( children: [ Container( - width: 60, - height: 16, + width: 40, + height: 14, margin: const EdgeInsets.only(bottom: 4), color: bgColor, ), const Spacer(), Container( - width: 60, - height: 16, + width: 40, + height: 14, margin: const EdgeInsets.only(bottom: 4), color: bgColor, ), diff --git a/lib/main.dart b/lib/main.dart index b3eb8796..08059adf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,7 +27,11 @@ class MyApp extends StatelessWidget { ColorScheme.fromSeed( seedColor: Colors.green, brightness: Brightness.light), useMaterial3: true), - darkTheme: ThemeData(colorScheme: darkDynamic, useMaterial3: true), + darkTheme: ThemeData( + colorScheme: darkDynamic ?? + ColorScheme.fromSeed( + seedColor: Colors.green, brightness: Brightness.dark), + useMaterial3: true), getPages: Routes.getPages, home: const MainApp(), // home: const Scaffold(), diff --git a/lib/models/video/reply/content.dart b/lib/models/video/reply/content.dart index 42ccaded..97897f75 100644 --- a/lib/models/video/reply/content.dart +++ b/lib/models/video/reply/content.dart @@ -6,6 +6,7 @@ class ReplyContent { this.emote, // 表情包 如果有的话 null this.jumpUrl, // {} this.pictures, // {} + this.vote, }); String? message; @@ -14,6 +15,7 @@ class ReplyContent { Map? emote; Map? jumpUrl; List? pictures; + Map? vote; ReplyContent.fromJson(Map json) { message = json['message']; @@ -22,5 +24,6 @@ class ReplyContent { emote = json['emote'] ?? {}; jumpUrl = json['jump_url'] ?? {}; pictures = json['pictures'] ?? []; + vote = json['vote'] ?? {}; } } diff --git a/lib/pages/home/widgets/app_bar.dart b/lib/pages/home/widgets/app_bar.dart index 56a53861..eb62ece2 100644 --- a/lib/pages/home/widgets/app_bar.dart +++ b/lib/pages/home/widgets/app_bar.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class HomeAppBar extends StatelessWidget { const HomeAppBar({super.key}); @@ -29,19 +30,26 @@ class HomeAppBar extends StatelessWidget { title: const Text( 'PiLiPaLa', style: TextStyle( - fontSize: 18, + fontSize: 19, fontWeight: FontWeight.bold, letterSpacing: 1, + fontFamily: 'ArchivoNarrow', ), ), actions: [ IconButton( onPressed: () {}, - icon: const Icon(Icons.notifications_none_rounded), + icon: const FaIcon( + FontAwesomeIcons.magnifyingGlass, + size: 18, + ), ), IconButton( onPressed: () {}, - icon: const Icon(Icons.search_rounded), + icon: const FaIcon( + FontAwesomeIcons.envelope, + size: 20, + ), ), const SizedBox(width: 10) ], diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 0b7556f4..89b952c0 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -19,7 +19,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { late AnimationController? _animationController; late Animation? _fadeAnimation; late Animation? _slideAnimation; - int selectedIndex = 0; + int selectedIndex = 2; int? _lastSelectTime; //上次点击时间 @override diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index fd455db0..c2ae6532 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:pilipala/common/constants.dart'; class MinePage extends StatefulWidget { const MinePage({super.key}); @@ -12,7 +14,187 @@ class _MinePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('我的'), + title: null, + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.light_mode_rounded), + ), + IconButton( + onPressed: () {}, + icon: const FaIcon( + FontAwesomeIcons.sliders, + size: 18, + ), + ), + const SizedBox(width: 10), + ], + ), + body: Padding( + padding: const EdgeInsets.only(left: 12, right: 12), + child: Column( + children: [ + Row( + children: [ + const SizedBox(width: 20), + ClipOval( + child: Container( + width: 75, + height: 75, + color: Theme.of(context).colorScheme.onInverseSurface, + child: Center( + child: Image.asset('assets/images/loading.png'), + ), + ), + ), + const SizedBox(width: 14), + Text( + '点击登录', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + const SizedBox(height: 20), + LayoutBuilder( + builder: (context, constraints) { + TextStyle style = TextStyle( + fontSize: Theme.of(context).textTheme.titleMedium!.fontSize, + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold); + return SizedBox( + height: constraints.maxWidth / 3 * 0.6, + child: GridView.count( + primary: false, + padding: const EdgeInsets.all(0), + crossAxisCount: 3, + childAspectRatio: 1.67, + children: [ + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('-', style: style), + const SizedBox(height: 8), + Text( + '动态', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '50', + style: style, + ), + const SizedBox(height: 8), + Text( + '关注', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '-', + style: style, + ), + const SizedBox(height: 8), + Text( + '粉丝', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + ], + ), + ); + }, + ), + const SizedBox(height: 20), + LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + height: constraints.maxWidth / 4 * 0.8, + child: GridView.count( + primary: false, + padding: const EdgeInsets.all(0), + crossAxisCount: 4, + childAspectRatio: 1.25, + children: [ + ActionItem( + icon: const Icon(FontAwesomeIcons.download), + onTap: () => {}, + text: '离线缓存', + ), + ActionItem( + icon: const Icon(FontAwesomeIcons.clockRotateLeft), + onTap: () => {}, + text: '历史记录', + ), + ActionItem( + icon: const Icon(FontAwesomeIcons.star), + onTap: () => {}, + text: '我的收藏', + ), + ActionItem( + icon: const Icon(FontAwesomeIcons.film), + onTap: () => {}, + text: '稍后再看', + ), + ], + ), + ); + }, + ), + ], + ), + ), + ); + } +} + +class ActionItem extends StatelessWidget { + Icon? icon; + Function? onTap; + String? text; + + ActionItem({ + Key? key, + this.icon, + this.onTap, + this.text, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon!.icon!), + const SizedBox(height: 8), + Text( + text!, + style: Theme.of(context).textTheme.labelMedium, + ), + ], ), ); } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 52a6efb0..8aa0d570 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -1,3 +1,4 @@ +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/constants.dart'; @@ -114,116 +115,29 @@ class _VideoInfoState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { return SliverPadding( - padding: const EdgeInsets.only(left: 12, right: 12, top: 25), + padding: const EdgeInsets.only(left: 12, right: 12, top: 20), sliver: SliverToBoxAdapter( child: !widget.loadingStatus || videoItem.isNotEmpty ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - NetworkImgLayer( - type: 'avatar', - src: !widget.loadingStatus - ? widget.videoDetail!.owner!.face - : videoItem['owner'].face, - width: 38, - height: 38, - fadeInDuration: Duration.zero, - fadeOutDuration: Duration.zero, - ), - const SizedBox(width: 14), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(!widget.loadingStatus - ? widget.videoDetail!.owner!.name - : videoItem['owner'].name), - const SizedBox(height: 2), - Text( - widget.loadingStatus - ? '- 粉丝' - : '${Utils.numFormat(widget.videoIntroController!.userStat['follower'])}粉丝', - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .labelSmall! - .fontSize, - color: Theme.of(context).colorScheme.outline), - ) - ]), - const Spacer(), - AnimatedOpacity( - opacity: widget.loadingStatus ? 0 : 1, - duration: const Duration(milliseconds: 150), - child: SizedBox( - height: 35, - child: ElevatedButton( - onPressed: () {}, child: const Text('+ 关注')), - ), - ), - const SizedBox(width: 4), - ], + SelectableRegion( + magnifierConfiguration: const TextMagnifierConfiguration(), + focusNode: FocusNode(), + selectionControls: MaterialTextSelectionControls(), + child: Text( + !widget.loadingStatus + ? widget.videoDetail!.title + : videoItem['title'], + style: Theme.of(context).textTheme.titleMedium!.copyWith( + letterSpacing: 0.5, + ), + ), ), - const SizedBox(height: 13), - // 标题 超过两行收起 - // Container( - // color: Colors.blue[50], - // child: SizedOverflowBox( - // size: const Size(50.0, 50.0), - // alignment: AlignmentDirectional.bottomStart, - // child: Container(height: 150.0, width: 150.0, color: Colors.blue,), - // ), - // ), - // Row( - // children: [ - // Expanded( - // child: ExpandedSection( - // expand: false, - // begin: 1, - // end: 1, - // child: Text( - // !widget.loadingStatus - // ? widget.videoDetail!.title - // : videoItem['title'], - // overflow: TextOverflow.ellipsis, - // maxLines: 1, - // ), - // ), - // ), - // const SizedBox(width: 10), - // RotationTransition( - // turns: _manualAnimation!, - // child: IconButton( - // onPressed: () { - // /// 获取动画当前的值 - // var value = _manualController!.value; - - // /// 0.5代表 180弧度 - // if (value == 0) { - // _manualController!.animateTo(0.5); - // } else { - // _manualController!.animateTo(0); - // } - // setState(() { - // isExpand = !isExpand; - // }); - // }, - // icon: const Icon(Icons.expand_less)), - // ), - // ], - // ), - - Text( - !widget.loadingStatus - ? widget.videoDetail!.title - : videoItem['title'], - ), - // const SizedBox(height: 5), - // 播放量、评论、日期 - InkWell( splashColor: Colors.transparent, + hoverColor: Colors.transparent, + highlightColor: Colors.transparent, onTap: () { _manualController!.animateTo(isExpand ? 0 : 0.5); setState(() { @@ -276,8 +190,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { }); }, icon: Icon( - Icons.expand_less, - size: 22, + FontAwesomeIcons.angleUp, + size: 15, color: Theme.of(context).colorScheme.outline, ), ), @@ -287,8 +201,65 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ], ), ), - - // const SizedBox(height: 5), + const SizedBox(height: 12), + Row( + children: [ + NetworkImgLayer( + type: 'avatar', + src: !widget.loadingStatus + ? widget.videoDetail!.owner!.face + : videoItem['owner'].face, + width: 38, + height: 38, + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, + ), + const SizedBox(width: 14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(!widget.loadingStatus + ? widget.videoDetail!.owner!.name + : videoItem['owner'].name), + // const SizedBox(width: 10), + Text( + widget.loadingStatus + ? '- 粉丝' + : '${Utils.numFormat(widget.videoIntroController!.userStat['follower'])}粉丝', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize, + color: Theme.of(context).colorScheme.outline), + ), + ], + ), + const Spacer(), + AnimatedOpacity( + opacity: widget.loadingStatus ? 0 : 1, + duration: const Duration(milliseconds: 150), + child: SizedBox( + height: 36, + child: ElevatedButton( + onPressed: () {}, + child: Row( + children: const [ + Icon( + FontAwesomeIcons.lemon, + size: 17, + ), + SizedBox(width: 4), + Text('关注'), + ], + ), + ), + ), + ), + const SizedBox(width: 4), + ], + ), + const SizedBox(height: 10), // 简介 默认收起 if (!widget.loadingStatus) ExpandedSection( @@ -320,8 +291,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ), ), ), + const SizedBox(height: 5), _actionGrid(context), - // const SizedBox(height: 5), ], ) : const Center(child: CircularProgressIndicator()), @@ -333,14 +304,15 @@ class _VideoInfoState extends State with TickerProviderStateMixin { Widget _actionGrid(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { return SizedBox( - height: constraints.maxWidth / 5, + height: constraints.maxWidth / 5 * 0.8, child: GridView.count( primary: false, padding: const EdgeInsets.all(0), crossAxisCount: 5, + childAspectRatio: 1.25, children: [ ActionItem( - icon: const Icon(Icons.thumb_up), + icon: const Icon(FontAwesomeIcons.thumbsUp), onTap: () => {}, selectStatus: false, loadingStatus: widget.loadingStatus, @@ -348,13 +320,13 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ? widget.videoDetail!.stat!.like!.toString() : '-'), ActionItem( - icon: const Icon(Icons.thumb_down), + icon: const Icon(FontAwesomeIcons.thumbsDown), onTap: () => {}, selectStatus: false, loadingStatus: widget.loadingStatus, text: '不喜欢'), ActionItem( - icon: const Icon(Icons.generating_tokens), + icon: const Icon(FontAwesomeIcons.b), onTap: () => {}, selectStatus: false, loadingStatus: widget.loadingStatus, @@ -362,7 +334,10 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ? widget.videoDetail!.stat!.coin!.toString() : '-'), ActionItem( - icon: const Icon(Icons.star), + icon: const Icon( + FontAwesomeIcons.heart, + size: 17, + ), onTap: () => {}, selectStatus: false, loadingStatus: widget.loadingStatus, @@ -370,7 +345,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ? widget.videoDetail!.stat!.favorite!.toString() : '-'), ActionItem( - icon: const Icon(Icons.share), + icon: const Icon(FontAwesomeIcons.shareFromSquare), onTap: () => {}, selectStatus: false, loadingStatus: widget.loadingStatus, @@ -403,33 +378,35 @@ class ActionItem extends StatelessWidget { @override Widget build(BuildContext context) { return Material( - child: Ink( - child: InkWell( - onTap: () {}, - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon!.icon!, - color: selectStatus - ? Theme.of(context).primaryColor - : Theme.of(context).colorScheme.outline), - const SizedBox(height: 2), - AnimatedOpacity( - opacity: loadingStatus! ? 0 : 1, - duration: const Duration(milliseconds: 200), - child: Text( - text!, - style: TextStyle( - color: selectStatus - ? Theme.of(context).primaryColor - : Theme.of(context).colorScheme.outline, - fontSize: Theme.of(context).textTheme.labelSmall?.fontSize), + child: Ink( + child: InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon!.icon!, + color: selectStatus + ? Theme.of(context).primaryColor + : Theme.of(context).colorScheme.outline), + const SizedBox(height: 2), + AnimatedOpacity( + opacity: loadingStatus! ? 0 : 1, + duration: const Duration(milliseconds: 200), + child: Text( + text!, + style: TextStyle( + color: selectStatus + ? Theme.of(context).primaryColor + : Theme.of(context).colorScheme.outline, + fontSize: + Theme.of(context).textTheme.labelSmall?.fontSize), + ), ), - ), - ], + ], + ), ), ), - )); + ); } } diff --git a/lib/pages/video/detail/related/view.dart b/lib/pages/video/detail/related/view.dart index 4034cdfb..3c79ee50 100644 --- a/lib/pages/video/detail/related/view.dart +++ b/lib/pages/video/detail/related/view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; +import 'package:pilipala/common/widgets/video_card_v.dart'; import './controller.dart'; class RelatedVideoPanel extends StatefulWidget { diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 73bafe2f..3885d8f1 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -1,5 +1,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/video/reply/item.dart'; @@ -289,45 +290,46 @@ class ReplyItemRow extends StatelessWidget { ); } else { return InkWell( - onTap: () {}, - child: Padding( - padding: EdgeInsets.fromLTRB( - 8, - index == 0 && (extraRow == 1 || replies!.length > 1) - ? 8 - : 5, - 8, - 5), - child: Text.rich( - overflow: extraRow == 1 - ? TextOverflow.ellipsis - : TextOverflow.visible, - maxLines: extraRow == 1 ? 2 : null, - TextSpan( - children: [ - if (replies![index].isUp) - WidgetSpan( - child: UpTag(), - ), - TextSpan( - text: replies![index].member.uname + ' ', - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .titleSmall! - .fontSize, - color: Theme.of(context).colorScheme.primary, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => { - print('跳转至用户主页'), - }, + onTap: () {}, + child: Padding( + padding: EdgeInsets.fromLTRB( + 8, + index == 0 && (extraRow == 1 || replies!.length > 1) + ? 8 + : 5, + 8, + 5), + child: Text.rich( + overflow: extraRow == 1 + ? TextOverflow.ellipsis + : TextOverflow.visible, + maxLines: extraRow == 1 ? 2 : null, + TextSpan( + children: [ + if (replies![index].isUp) + WidgetSpan( + child: UpTag(), ), - buildContent(context, replies![index].content), - ], - ), + TextSpan( + text: replies![index].member.uname + ' ', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleSmall! + .fontSize, + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => { + print('跳转至用户主页'), + }, + ), + buildContent(context, replies![index].content), + ], ), - )); + ), + ), + ); } }, ), @@ -382,6 +384,7 @@ InlineSpan buildContent(BuildContext context, content) { if (content.emote.isEmpty && content.atNameToMid.isEmpty && content.jumpUrl.isEmpty && + content.vote.isEmpty && content.pictures.isEmpty) { return TextSpan(text: content.message); } @@ -416,7 +419,7 @@ InlineSpan buildContent(BuildContext context, content) { String matchMember = str; if (content.atNameToMid.isNotEmpty) { matchMember = str.splitMapJoin( - RegExp(r"@.*:"), + RegExp(r"@.*( |:)"), onMatch: (Match match) { if (match[0] != null) { content.atNameToMid.forEach((key, value) { @@ -455,7 +458,6 @@ InlineSpan buildContent(BuildContext context, content) { RegExp("(?:${urlKeys.join("|")})"), onMatch: (Match match) { String matchStr = match[0]!; - // spanChilds.add(TextSpan(text: matchStr)); spanChilds.add( TextSpan( text: content.jumpUrl[matchStr]['title'], @@ -468,6 +470,16 @@ InlineSpan buildContent(BuildContext context, content) { }, ), ); + spanChilds.add( + WidgetSpan( + child: Icon( + FontAwesomeIcons.magnifyingGlass, + size: 9, + color: Theme.of(context).colorScheme.primary, + ), + alignment: PlaceholderAlignment.top, + ), + ); return ''; }, onNonMatch: (String str) { @@ -477,6 +489,29 @@ InlineSpan buildContent(BuildContext context, content) { ); } + str = matchUrl.splitMapJoin( + RegExp(r"\d{1,2}:\d{1,2}"), + onMatch: (Match match) { + String matchStr = match[0]!; + spanChilds.add( + TextSpan( + text: ' $matchStr ', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => { + print('time 点击'), + }, + ), + ); + return ''; + }, + onNonMatch: (str) { + return str; + }, + ); + if (content.atNameToMid.isEmpty && content.jumpUrl.isEmpty) { spanChilds.add(TextSpan(text: str)); } @@ -486,66 +521,98 @@ InlineSpan buildContent(BuildContext context, content) { // 图片渲染 if (content.pictures.isNotEmpty) { - List list = []; List picList = []; int len = content.pictures.length; - for (var i = 0; i < len; i++) { - picList.add(content.pictures[i]['img_src']); - list.add( - LayoutBuilder( - builder: (context, BoxConstraints box) { - return GestureDetector( - onTap: () { - Get.toNamed('/preview', - arguments: {'initialPage': i, 'imgList': picList}); - }, - child: NetworkImgLayer( - src: content.pictures[i]['img_src'], - width: box.maxWidth, - height: box.maxWidth, - ), - ); - }, + if (len == 1) { + Map pictureItem = content.pictures.first; + picList.add(pictureItem['img_src']); + spanChilds.add(const TextSpan(text: '\n')); + spanChilds.add( + WidgetSpan( + child: LayoutBuilder( + builder: (context, BoxConstraints box) { + return GestureDetector( + onTap: () { + Get.toNamed('/preview', + arguments: {'initialPage': 0, 'imgList': picList}); + }, + child: Padding( + padding: EdgeInsets.only(top: 4), + child: NetworkImgLayer( + src: pictureItem['img_src'], + width: box.maxWidth / 2, + height: box.maxWidth * + 0.5 * + pictureItem['img_height'] / + pictureItem['img_width'], + ), + ), + ); + }, + ), ), ); } - spanChilds.add( - WidgetSpan( - child: LayoutBuilder( - builder: (context, BoxConstraints box) { - double maxWidth = box.maxWidth; - double crossCount = len < 3 ? 2 : 3; - double height = maxWidth / - crossCount * - (len % crossCount == 0 - ? len ~/ crossCount - : len ~/ crossCount + 1) + - 6; - return Container( - padding: const EdgeInsets.only(top: 6), - height: height, - child: GridView( - padding: EdgeInsets.zero, - physics: const NeverScrollableScrollPhysics(), - // 子Item排列规则 - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - //横轴元素个数 - crossAxisCount: crossCount.toInt(), - //纵轴间距 - mainAxisSpacing: 4.0, - //横轴间距 - crossAxisSpacing: 4.0, - //子组件宽高长度比例 - // childAspectRatio: 1, + if (len > 1) { + List list = []; + for (var i = 0; i < len; i++) { + picList.add(content.pictures[i]['img_src']); + list.add( + LayoutBuilder( + builder: (context, BoxConstraints box) { + return GestureDetector( + onTap: () { + Get.toNamed('/preview', + arguments: {'initialPage': i, 'imgList': picList}); + }, + child: NetworkImgLayer( + src: content.pictures[i]['img_src'], + width: box.maxWidth, + height: box.maxWidth, ), - //GridView中使用的子Widegt - children: list, - ), - ); - }, + ); + }, + ), + ); + } + spanChilds.add( + WidgetSpan( + child: LayoutBuilder( + builder: (context, BoxConstraints box) { + double maxWidth = box.maxWidth; + double crossCount = len < 3 ? 2 : 3; + double height = maxWidth / + crossCount * + (len % crossCount == 0 + ? len ~/ crossCount + : len ~/ crossCount + 1) + + 6; + return Container( + padding: const EdgeInsets.only(top: 6), + height: height, + child: GridView( + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + // 子Item排列规则 + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + //横轴元素个数 + crossAxisCount: crossCount.toInt(), + //纵轴间距 + mainAxisSpacing: 4.0, + //横轴间距 + crossAxisSpacing: 4.0, + //子组件宽高长度比例 + // childAspectRatio: 1, + ), + //GridView中使用的子Widegt + children: list, + ), + ); + }, + ), ), - ), - ); + ); + } } // spanChilds.add(TextSpan(text: matchMember)); return TextSpan(children: spanChilds); @@ -554,24 +621,25 @@ InlineSpan buildContent(BuildContext context, content) { class UpTag extends StatelessWidget { String? tagText; UpTag({super.key, this.tagText = 'UP'}); - @override Widget build(BuildContext context) { + Color primary = Theme.of(context).colorScheme.primary; return Container( - width: tagText == 'UP' ? 28 : 38, - height: tagText == 'UP' ? 17 : 19, + width: tagText == 'UP' ? 25 : 32, + height: tagText == 'UP' ? 16 : 18, decoration: BoxDecoration( borderRadius: BorderRadius.circular(3), - // color: Theme.of(context).colorScheme.primary, - border: Border.all(color: Theme.of(context).colorScheme.primary)), + color: tagText == 'UP' ? primary : null, + border: Border.all(color: primary)), margin: const EdgeInsets.only(right: 4), - // padding: const EdgeInsets.symmetric(vertical: 0.5, horizontal: 4), child: Center( child: Text( tagText!, style: TextStyle( - fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, - color: Theme.of(context).colorScheme.primary, + fontSize: 10, + color: tagText == 'UP' + ? Theme.of(context).colorScheme.onPrimary + : primary, ), ), ), diff --git a/pubspec.lock b/pubspec.lock index 9f021f27..36af3acf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -272,6 +272,14 @@ packages: description: flutter source: sdk version: "0.0.0" + font_awesome_flutter: + dependency: "direct main" + description: + name: font_awesome_flutter + sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206" + url: "https://pub.dev" + source: hosted + version: "10.4.0" get: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7b78fe6a..8251e578 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,7 @@ dependencies: flutter_inappwebview: 5.4.4 extended_nested_scroll_view: ^6.0.0 + font_awesome_flutter: ^10.4.0 dev_dependencies: flutter_test: @@ -106,6 +107,9 @@ flutter: - family: fansCard fonts: - asset: assets/fonts/fansCard.ttf + - family: ArchivoNarrow + fonts: + - asset: assets/fonts/ArchivoNarrow-BoldItalic.ttf # For details regarding fonts from package dependencies, From 89766a72d9c68488112f5a6a4a15abfe242d3e17 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Mon, 8 May 2023 10:17:35 +0800 Subject: [PATCH 20/30] =?UTF-8?q?mod:=20=E5=9B=BE=E6=A0=87&=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F&=E7=BD=91=E9=A1=B5=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle | 4 +- .../ic_launcher_monochrome.png | Bin 0 -> 4819 bytes .../drawable-hdpi/ic_launcher_foreground.png | Bin 0 -> 4819 bytes .../ic_launcher_monochrome.png | Bin 0 -> 3151 bytes .../drawable-mdpi/ic_launcher_foreground.png | Bin 0 -> 3151 bytes .../ic_launcher_monochrome.png | Bin 0 -> 6455 bytes .../drawable-xhdpi/ic_launcher_foreground.png | Bin 0 -> 6455 bytes .../ic_launcher_monochrome.png | Bin 0 -> 9818 bytes .../ic_launcher_foreground.png | Bin 0 -> 9818 bytes .../ic_launcher_monochrome.png | Bin 0 -> 13111 bytes .../ic_launcher_foreground.png | Bin 0 -> 13111 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 1950 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 1129 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 2698 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 4253 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 5627 bytes android/app/src/main/res/values/colors.xml | 4 + assets/images/logo/logo_android.png | Bin 0 -> 26112 bytes assets/images/logo/logo_ios.png | Bin 0 -> 13158 bytes ios/Podfile.lock | 12 + .../Icon-App-1024x1024@1x.png | Bin 10932 -> 25069 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 295 -> 309 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 406 -> 660 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 450 -> 1043 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 282 -> 443 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 462 -> 1030 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 704 -> 1708 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 406 -> 660 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 586 -> 1484 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 862 -> 2335 bytes .../AppIcon.appiconset/Icon-App-50x50@1x.png | Bin 0 -> 858 bytes .../AppIcon.appiconset/Icon-App-50x50@2x.png | Bin 0 -> 1925 bytes .../AppIcon.appiconset/Icon-App-57x57@1x.png | Bin 0 -> 1020 bytes .../AppIcon.appiconset/Icon-App-57x57@2x.png | Bin 0 -> 2312 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 862 -> 2335 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 1674 -> 3677 bytes .../AppIcon.appiconset/Icon-App-72x72@1x.png | Bin 0 -> 1343 bytes .../AppIcon.appiconset/Icon-App-72x72@2x.png | Bin 0 -> 2857 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 762 -> 1415 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 1226 -> 3015 bytes .../Icon-App-83.5x83.5@2x.png | Bin 1418 -> 3334 bytes lib/http/api.dart | 12 +- lib/http/user.dart | 9 + lib/main.dart | 25 +- lib/models/user/info.dart | 7 + lib/pages/home/widgets/app_bar.dart | 2 +- lib/pages/mine/controller.dart | 14 + lib/pages/mine/view.dart | 276 ++++++++++-------- lib/pages/video/detail/introduction/view.dart | 10 +- .../detail/reply/widgets/reply_item.dart | 13 +- lib/pages/video/detail/view.dart | 3 +- lib/pages/webview/controller.dart | 69 +++++ lib/pages/webview/index.dart | 4 + lib/pages/webview/view.dart | 29 ++ lib/router/app_pages.dart | 5 +- lib/utils/cookie.dart | 27 ++ pubspec.lock | 48 +++ pubspec.yaml | 19 ++ 59 files changed, 449 insertions(+), 149 deletions(-) create mode 100644 android/app/src/main/res/drawable-hdpi-v26/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/drawable-mdpi-v26/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/drawable-xhdpi-v26/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/drawable-xxhdpi-v26/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi-v26/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/app/src/main/res/values/colors.xml create mode 100644 assets/images/logo/logo_android.png create mode 100644 assets/images/logo/logo_ios.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png create mode 100644 lib/models/user/info.dart create mode 100644 lib/pages/webview/controller.dart create mode 100644 lib/pages/webview/index.dart create mode 100644 lib/pages/webview/view.dart create mode 100644 lib/utils/cookie.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 262f823b..5b25c505 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,11 +47,11 @@ android { applicationId "com.example.pilipala" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + // minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName - minSdkVersion 17 + minSdkVersion 19 } buildTypes { diff --git a/android/app/src/main/res/drawable-hdpi-v26/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-hdpi-v26/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..df13b1283ba33d36b3262f2d02320681d6ebf799 GIT binary patch literal 4819 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 EA8KhS@Bjb+ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..df13b1283ba33d36b3262f2d02320681d6ebf799 GIT binary patch literal 4819 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 EA8KhS@Bjb+ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-mdpi-v26/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-mdpi-v26/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..8cdf485185d5b5befeb08959eb0c6c9574325351 GIT binary patch literal 3151 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..1bdb8bb32d565fb5e5f50f174fb940d83a02a4a7 GIT binary patch literal 6455 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxhdpi-v26/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-xxhdpi-v26/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..140baca632010b81807ea6ae6dfbd02e79cc7abb GIT binary patch literal 9818 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..140baca632010b81807ea6ae6dfbd02e79cc7abb GIT binary patch literal 9818 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxxhdpi-v26/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-xxxhdpi-v26/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b58395470e24488490c0956b2952d9d2cb6f44 GIT binary patch literal 13111 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b58395470e24488490c0956b2952d9d2cb6f44 GIT binary patch literal 13111 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..f606c4d8 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..1cb4b0c494997ecdc9f0960464ee61ef8e4a85b8 100644 GIT binary patch literal 1950 zcmb8w`#Tc~0|#)IX`7vOFyyi_>f{n~E84ou!c;E9axbfnlDpiJMwmllQcYTDlS_%* zhZN?r4mQlDY%Ucda~XUBbGD8`hoaKXW+jYOk)8pjCZkB1a)T4F zh~G_77w>sGYXYZFhvi(RSH8x8VCp2<%q_ zuiwXvh0Sm}w%3{4t%A)DOJ^$BCd7+tVYK(ojR1Hm$QXj8RY_9+ z>qeZ#@yQ-6bAsA-)3~h$=}{3%x<j6iuZ$V_2NyrXi>H2LzNU@xlz0 zbV!N9Ln9n;DW_dzeX^`27H>8#Q}uW5GojH4=QvY*>{se?kYL zLvgL(&W{q|=Vag&A;ElPr917x3T(5J<7r%2(HE@}XY<)Y0R-C@=+Qfj%o^e`1p@-s zM62Vd06axw<$-l+j37US`5PZi=j;wO5AoH@_xkkEMQRw|;<;m5firYeU=H`cP zXP?Kn*+2Y2Du6O|<*wpE&o^>s8;?FtsGezGr(-q(E^RdrYrb_Fb*Lc@%*9zo20gwaLf)DSC<^puhVqsnKfjM9TM1%_$H=*?_la18865mQx zX5mR#al$**+?ffo@i#r^Hm@x`i(SxvZYj6|n2LPV zdEU=pKvKw#_X9n{@9USPc@BHU0UlB6Q8&J$@-DJoF)o{0)wX}@6*~R66mK`Qnwewq zA#Xtt6j_uYP}Fy6G)R99#Kq_ha+7VR^gh)VKo6cP1v@g)n4Gef zH+!V+T36RFdQhooDQS89sG99fJ&=RBdzdv*`e)D1-04!SxbUfw^}K0EKeE%v&(7&o zUFP2Ho)Y1z@5n5_v!#qde$t9EMHmWLL%ps+gPQ4oKMA)cKsz6Q>2s3Fj*1DD-F8oW zIUF^=rU~g!kX;WF8F6`%jR(tf5$h{P!6s%X8)ubkg&0>Q5i4C|?h`D++12B;%~n@7 zO;ufs5w2-E*tW7bekLBaVWmYb0Xldy3#x--xfHzG!JOL}bXvHjxO%%1(TW+Wn^9+L zp7b1R9DCKIe`R7e^-kx-Tq@wUP$m1qnTALj)R9qzX`&gVl7Qmy>agFcEZxUn1jTox z*rM23ia3GcOSy)(8in(RHP%kvUwolpY%in-2JxbLKQ4_=46(|2wB5@b-(BF()aRd#$ ztzLTHGUx80WYI?~%0WJB{6lsOJY-?vJvlGGRv#HrR+5^^A&&~?d+y0<+v3X3msNS_ zleYRp^H1Jah2sN}eBxRxqoFRUo)IY>R7g`?I{q}IvLY?10Us+&(QIiw*n8@ay;oqx z#BcfTH>TQSwb+h+{aHS|C-dzO0$UJ<94q{dki@Mi(LXkyCHw7R^=;E4Zp%HaErF+T zVVzpl_L_9Ql2`w(QD0=B;wS^PMy)j_@x?%u(}7y;DeUbdp39c(|A(vp;+81=Iv6<3 TFL4?E<*w8zTPK^B7((*DKbEh= literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..9eaf93928fd4976f24305ca3170bc29b386291ee 100644 GIT binary patch delta 1119 zcmV-l1fcu61L+8m8Gi-<00374`G)`i1TaZNK~#7FV__KLfMuW^Kq(mYz(9jh4-7OI z^}s-bQ4b6>81=wFgHaE_fT5_IE;k#S0F3|b_wO&~UOhZdE%R{!s(8RpS>I!=U)DAT zMj(WO_dmYA5SvycNe%Py0jhX_gN2p**S|kMB&HXO{{8pwzkiLiiuu;y{Jo-_yaKPk zfBrxXJ8%JtJdmmHknAX@;U&V!BgDqc!nxEtWkvJxE%le)JiP>xR~HgjfBp3HN$NNi zJD|t|Ej9sN7hgZQbmQIgTOb^!ZW>}KrEGEf+}?8^e|~?XCMcnCw%|VK0G?` z_~!1W}6@P%rPM{kZZoPYPLz0_MmXD2- zYw7h9t8c%5c}I$eUmPgK2rQ|1G=-!z0#yxzQ}yh!+fHt)117zW0rdbV2Dn+-`DJ;9 zBp-c#_Z*m{ZUEiNSZ(1|3(OxcfJ*+p{`UFhg;$TyU4QrdE=W(qv5n2yhEAzH_Mub1 z{{H!;_kYZ;DFfz#n{Qv<0tWp*bzo)%c>sig4r&fkGYny7WMTx?YpkMNyb_>#;_~Y! zXDSY@E!*|*+JPzO_s!;Ir*}=tF>oqO(6NdKCa3NJ@c;@pRzuOu<56t-#>f$<%0{G?_S*CEU)P{pdJ8~ zJnBT2_su7^bU6VXTxaE5@4J3-;MtcC&w?^FCH7M#2D}vYyopsm`P|-#%e+(93UhGt zeSi7=;|n_r3l}B!Qy~Uk-UJqvay+aY96$&CAf$fp!|VGP85x<(Bo)omra(`>hqHD)jQer>F9{BhF|NlE5Ufm)#Bd6)v zB?6n?9Hxs~bg#aBcAHZBsnVeVWn^t(DIG2rHjE6(%*4dN%gWBBDk!cNu5K2Vq-&kk za$;*WH5?BDRPn%$`&YKka*tc`QFU@SZdL#PUw`}JA}GyMnrx5(RXi~N>d|FqURXXj z&(F>&f<^lO?|*;4Km7Ri!HaL7-crLpWI(l1iOX-EQdAt03r5F<1{#cdV4%UM2L>99 ldSIZzs0RibjCx?80RS?ezIMK;>V*IR002ovPDHLkV1h=@Gc*7I literal 442 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 literal 721 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!iOI#yLg7ec#$`gxH85~pclTsBt za}(23gHjVyDhp4h+5i=O3-AeX1=1l$e`s#|#^}+&7(N@w0CIr{$Oe+Uk^K-ZP~83C zcc@hG6rikF&NPT(23>y!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..9c4d6c7abab92b4f39febd63f467946f2b82fe78 100644 GIT binary patch literal 4253 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 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372eebdb28e45604e46eeda8dd24651419bc0..59f64433ed1e2d361d5b9e7a8b62ae1e73005d45 100644 GIT binary patch literal 5627 zcmds*^;6VQ_s2g=EG$TONJvPB2m&q*(kvxNii9A&lDpKJlyrB;N`rJtmm;z(Agy$F z$CvN({1?v;=iZq+bLX5nbMKt@`xUCIqe?==NCW@?iMpDy{@vd3|3L`8TlHt02>^hm zQ(alX&=<6w0eL}XKiBaPkFs3em5uoUgzY;nRJVk>q^!?qdZYwvTD!eE@#nH~!+X1a z)@Zh3ylA_rZhXUd7K>$JnI$U0I^pZ-mCQU*U{g@UVV^0g+%2tpnxbyiVfB9Z_7r-U zeLKA0Db_x4nT43YZJ-2DqUDjU03b<84sPhLg#89=RTe+-LxlPDtJXenjSW8WBh5 z?^qF+rOXveT0hYSTmm?gRm-%tokF20u)G>G`EWE)3! z7)mRzF6@Wwf%b(pX9u;TbG(2EL8t2}gPZq&7T`sim{ zIGz<}pBhRZskOpOc)Hlw{MhrG*Xijb>iWA5ikIkl_Xz`xc@b`bfAVLQCq*$Mm*1KY z6>C*ZjQK!I%C#k`K}+r3R-NB%*+&2-^~Nbp5joWM%e>-@$puP61vTDluc?xl+G2b^ z_QObUBnc)V*+&TxHK($AvJ`JN#k5^q-swMxF)6>@g)W4&PJ@nExA5dMVuX-9+GgGd zAB8TVX+NmmKd`2H&%s*I8?V^-qg!I6lwTRA+pJu;hWDPk&vzXlz$1WNCRF@enmCg% zck9sW%h=+EH=dyPc)4m-&2$qO?69w*k-0&fCXb+vjjh7ywVRX(z12hc#HvR=NVRws z0aNPXi;u&_*cDTV-{NE*>|lGB0Wj9Q6YW;u zO|0CBjmI*5StCf92}8q=$|&Dxqgwv(_3jgrs*-cl61=8QI zqkBMqp#}-)itPcoyjm^}X16_xQS}=6i0q6Y;99>%FFY_h2`TJoE_g3;Sj15QAo!30 zsEC6P_+}$%Wx0k!W5LBS0NVNF-NiA&6V%)PzKj&^u;#Fdb}0TqwjcAQs5fuvz#%tT za%^M;*Bks4bmX!kWTUARGd$L3{Uyq@G(=X}+=fg3wF_d$Ibr!c@-2{tP=1G&P7(s7 z3X55PfWeS0JT~^rWr!z8mo;|^8i)mt` zA&q70cC%{LLYyX=e#Z737uc_&wc<|UTH{=dHC1f0x-szDCV@T-w$l~KXhkD{-(S(H z5c!)E#_afmHBz6GDH6|0+#+UcVho><0#cSJBU&fn#pM(ps6`n0Lh&+0)9C|OeQz}K z&|-FpN0mM6AuOFyjaOiF%togfeywHwW@R*PSxWUci8M!A9quhi6ZC7mx%T?CF@Dyj zqh2weG8;S0$_OXg_!Ux%D}K84vSxX(N*kg;LHtX78@mgh##z1}htowHJ+yLmo`Fv2 z3A$bSBPRzn^goWBjf6+t^@cMA?BZ(P;Z6*Cpd83oH*G&5a!8n<``Y7@$_^oACY0g( zYBoRRX~_5U=H?u?;GQLYefx$Z^^26Kla+M2$=~B3DmNs^3>3Ec^)MRnKv#+6eDPZR zs%Rk25hXtlw7Rx0cIMo+7YqMXHr~{BRPVUw38^=mQVI7CS)#gqbiKd(lJU^$fJ)Uz z2=>IW_5(cK59EwrDZ!arFLdvIylQl)%R|$fv4TbSd_Bsb`72YIykcwxXToa>g`rZB z97A%UBjRK4e`ZN2f#UHlh0C4&gGB_PUfUw;Uk(=8XDs1-_6 zteI<0{^mH8@Gp4s##QoN8Gzlq2{ECoPziOhH&9K(rmgBEe9k*cP<;RrI;{;rHx^O| zPB}-Ze%k%3(S3#={PTHapf9}pj7uXYU7+=^wU>qQ6ZVT-LNvx7yYU*sk+3HJ|jr>ksC${n1KgeGzZ-e^2j+hdv}(zTXI z9zA~<@sq?6-=Ur`CP@uI~nRUvsQ5YM-PZeisi34nv& zv#Gcjh>$L#m(S}Te}j7Y@3NB8zLcrl%%PbdfZ|ged9Co;U|myNDi^lHgHNP$BYuMC zaFR(1)KCMMWK;TKl;~M(9J?$#C@029sQVW ze*eR(EN=m5>sf?nTS*$(aZS-PnE$^nemS>^TCDYDCuu1@&zoOjc=Eov%95OA8%U|N zfVI?3#((W|7agk}i7qh0F!Rq%@SSb_Ddx4SppIEuLt3E)0SWP z1h0o`4V1CXit zz`YJt{@!zv%dvb^C7NB*MOBGF5k z;cQRFaN1trX%qDA#ygsa-t8Y_?Tt9(=Ub!CdOG*uusr_`YoWvNsxfSU=W(}?ftlE* zTY?{?6yIUpmE-X-xEJpl(>*U(4!Ze zY@X|w|Ela05(sdb$qSm7B_8-=pF1-ZP_x<^9?H~yF&kudWw~a?<~v?|)>mgPb#p8~ zEffFU6rg;oMPAb#YO}IwiA`GnRqZxiYjD|rN?j4D?)$g4imXi4IH331pRy`A=>YRr z9DNvc;k+_%&>|cBoRik>c19$xHl(fH9s}W50JN%%cr*pQuQUa|lxe##3E$X^)Rh$7 zo@`7KWSPtDEXp083%xr!)^%M7Ivb_a5rkeLt{@iPdTy7_8?M_^aR~UT6MRfAw>N=u z|Jw^hrNC@8E?+tfhJ3=wmdvI8s<~){daav*dI@ zyl*Z^^Pg`ig6UiaZZ7#7j7Q9!)IV9hN~+LKZe!T~{(;gvql8(E6;kOW5=dYV_5%@V zA$z-NJ-tKo3c>(S#Y!aI^S7a9dU%z?oRYT0ks6*LvXn&!ayHmsGJPgLVs_ zt(m?zvCulD$m+3pc_Q8yNkgr4FWnlXo$BiYUe83?wt!x$V91~Ri({(SLB2e~9HJGL z#!gO`O9!!H-2va&g2`3XaXQP_hQN=Htc%~b0-RIg($%)o+Q27+B%zs<6n|Umjy-MI z*N5#&5qbG9uJ(_(%3I9xo)({Tjs5Yy&!gI4YLS^p&isbG!@-2yM2NBk9mfW| zYW>`9x^ZTf6Y%b0)54AWcJIo^Ba0&(t`}_8P|D(WE6v&Q$ZUFr;U0q;st1#-!9#XG zzff9`4Nx>N?(z@CSHQy~?zS|gP58?^kdy0_6j%F6E<5fV?#O%{6r<1M_uNL}k%oAz z!!Wp4ZIWCVZC&|iy%Yt6Y?-=H*qF;{5O=8@xO2*X?qckX_4&zly}v~jLk#K6y>40F z7>K%F?~EBKb;)x=kB{0wpU@3h)sC8gK!gW92$F4*3_&hgC@fqVuuynz&qtJQi8{Q< z{rin6wJVNVBm8&x;qxkD7l`iIZ-+9^@T7!E84^PGv=+vIMNXCEPrx;rYRSRdmU}mc zI6D!nr2wf;DzAzhjd0rC`4|SrBiv(3MdI?JRJ4|(tkJG#!!`9*Qh@@%8F40FZR5vHut4vpQ?ANHbSeb^$#!YsA zU-5qM=cwd2+CY%-%idk=%=jz%VF_uDyK5T#bf#nU?DuRf_%RHUW_s~jbXIRsj;Xa? z_V<`X6Il4mYowKSGQoHa+L`E+HU1Qr zS-Y_95drUvi>PplQbTXQ(z6GkK>mpaB<4uPUnmMbBKO?5b3-uYl#&hj{KCeWV9M4rGB7Z$a383 z`NiyrBUL;U03w{O5p6AT?Qv%-cIuppPUbUc zrRnY}jg{{5LJl*m5S#{|oVZfS&@S6ftgO_`zam7tPV^U`{W>W2ngkK zx}9w?L5-vgBa_!PvPg$GxE7jt`?n+hi8?QE5JEH+NsES!EtA%mE1Hl??1{TjZH6 z38~G8%7^R3n zi4iSq|NTBIvpNv9Uh`c|H%m?w{dj1-$Wxk<90fLE)Sa*$B%=)ZSG>PwhUicy5Fg@m zR0vs`#vk#|OT-(lK7jZAsUg759zP=yl?V8v5r73ypu6!{>4Cqmf6};~7+ovP?She8 z=2IIhrh&+#8n>c3o+_gq{A>W{g@S<-ADg0}jbmu%;s{P5kKtD?4sHPpT5)&**}T#l zr`B&_2iHypq*m7-lLXmNG}W7Y^ju?lPdL1h+0unAQMUfJ2V*(__J0Hc11jehhe+A)q% zxleM!2=Q4x7whjoEKCMAEupoyd2Pn{fp-{2(eyc5XTqT=Ab8UL+lRAH>kL z@iw{7jk+*6-Z7v_cM14)(xFkhigBQ87XMSiyT5i*UK;;nI#BA`MaiC@x``3EV?ZZ# zFV!L%nUAhE;bQ$sYKP3TnPx}dk}wPv1xE3iGWy=zYwHJB#DW(0-T%&m*yM#vHLqX! z=$jx1FB+zt?tq8=L8I%t569fTe~o@Bye=11hQZ>>(dC7Owdm?@XMu4HmUd-){q0Y~ zrI*+{J%3cofs$d9XU$&=BOa|D9Nh-M1pjl4@xREZdzvU+Q)Tbpdr19p!RG HtM~s0oS9xB literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..ab983282 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #ffffff + \ No newline at end of file diff --git a/assets/images/logo/logo_android.png b/assets/images/logo/logo_android.png new file mode 100644 index 0000000000000000000000000000000000000000..5e220fb458905cde2b7f66d5d621c7d048c7884b GIT binary patch literal 26112 zcmeFYc|4T+`#;WcqH_}Ml&$5YO(%Q;T>sp`J^SbWGXROTD{=V&Z z5fPEKCr=ok6A}3}5Ba-V9Dd>KC}@U%M19Yh{VDpjUL^$n^PA5JJ6{ozH5-w?q9O?| zWkp1;rk^xEe&No`5$2j_%IdxcIttzaHQI{Mg||de_^W zZSPe6Ip+UQz1~>R;yhvMrSe_hABoHJ7m}y++R#NeXAd3Vl# zCwDO!?DMNsuC&EwUm6-dm^Qx}F895Xi{mSHG1!F;{>z&xL(if~lb8K3V+=*%qWqEC z$;qM&(&`y&!4=*&6NN(5ZZZjZs??LOt)B_Lge6MogK*R%O?{FIxdge88zNn3%cF8Qcn5h5d63pY z#%K@g!4uTK*6A8)^HT7W%XvYD{c7ioH;-_)Fk`~Iuid=F_zh2(k;yxCHmvkEvvLo4 z8M&L1eyPjH*f~?%RPEdaTl`g9CxgReY2+5lTf10AboB<4RqZ|($ykiiFYu#7%B4UQ zioteHmD#($_%?0H7^AlGyB*!Y02h$@`~)+{@DaD2Kh?$vsdZ3SVaY9Jn{hN7*63H+@;jd zS^0aC*8kscJO*{mMr&KmQH!+s4X~`w6DRG1?mbgxH5SRWfFDA|9u5xfnnyWqg9oNt z`%kpVQ5TVS=iRhL@n>hRKHi_NYG+$^6BB}*`i5KrW`Je#cz?ldx`8@bXgS4eEe>cu zc6#>aPa=Cu>O4R%5tlAd9Wzioj(qPkwsSKx8GaLy;9>d6)P@b#S1J#YwGnwqeZmTS zzNh}7&Ga^PKa)y1>He!)o`C`ZxmGQTXIAdNbM~DY>i&kotoawL^1O%Vn<-8hMBgG^ zw_}@ruM{I%jfFS8GWHBFACs-D9!}(jr2_mD5qZw7#+@5a{r*q?YrL7s(3ylw0lkL? zzVe>B7dU0s84n?A@^VPzdDC8YLCE!KKkCtEd(rHLPregDhvC|*)wNN+URx@wKU?x& zvEg(a2O@!l*Kl+yWkN@J?QB}&RG7;`bz4Gz+zMAlVPMg84 zDJQj~oLmOUc%T1HOf%StJ9}7Vr1CgMYWFaQJC)*2!&qYwEG*L1o-h-bCS!{)x*ZMs zea7M5%=dRm*FuRQFTPnjNpTgBIlE+bbFlLr!UctU?v%~`U?Khrim*l*@~o2gFZyA` z;n801Bfk*mPpv!}^d<=J)5R|xK{P=Qv>h+IV}Km>%tMEgDjA%F4Z&a?oTq(@A7LFR zA-+}GY(>B~+h6bbRn5E|Ow1Ee!oHY8iFV(=W~15hD{AU!*Jk%n{RJbvPrZ}~Mvre3 zn!@$s;i(%}Y@oQm&2TA-waUiH+;B5o|9E3bg?A&1SCyuLY+L6Qsl2&PHNO+JvNM|z z1zYz0|J1!%Achgcg{|JMee&$?hSVJ8O#5hC6BzZcra^`%`&Mk%zoUIVobZO2D@o9c1JGd z-9!Qobxk(KY4NF0!o0W+-MmiK|5Px8!8m8yeNVk&<6k8YhsjuVZseB(mAhB2i^18~ zI6k|;enH*(?3&mrO6N#DtVA-rEq z9V@prkME!^7CsH$x9EehqA?$=oFY+OMNR1i2h(b+^w??doBJ26JyPdAb|IOCdK9r+ zp&wO?&y1ZYX~yC_c{`6QjC?rNWx{WFgV6M%Bb!WEIaBX@ooclxv4s!RhAr6f0}S>- z>SEDWJiT(CPNx3K5_Dg6L2d$XfweoFY|1I1;)}PIS>3#1RmSbvBDCG{wGSUuYCq>T z?Q^L~&F$aT)G<9uT`Yb|VQ2Ndq^w(6gMB#4)@dN-Tp${5VEW3{Bf;k7LuIB8N&=4V za^xj$rX;dfz)6i?HQYka<^~@nk7>PIiC5F%2l!8fB9a~4pFVDK7xh{*X%6zC*DlcX zQ>xmA=hmYGLK_G@=v`?Z9p#kq=5Jg8X6&YEE^U6N?`KOKcssl&FMX8c7sAka9g-e; z^$Dsi`~?cXm%Oayx{y8pXMsoS0im~r9?T(?qfL;cnX|T`f$@Y}6Zsvbzw2XhfpS@dTA9`_PsKHG=K=?ep_i#wr>{^dfj_14eLumu`kV3QbG7@L%gnV)>y=k9xLtu2Ia2r2* zbpC2ijo+PB0nspz)zkec+o{YGV^xqU>BrY4IXi{|@_^@_(2&nIAuR!vrtE0D#vy4!@QS}m7XPXweL3%CZ}n1L`)P= z_F|~$bhMc;3Hg$ut7t=-ND8U6ihisgpQ{wdSJ+6XH5Ft9UK~j!&0iB5uR1?u-SqdGo z<5RZR*oI10qBVPd#i+|{-V+RH8$d=IBEK%!v%Vfd98&8rbMZ)U`*e$mGr{WrId?XL z{S+SihSVx|Sw;h3y(AKLl-4-%tVc;d&*Uxt-f%Nn@D|XG@K)vTr_Uem(H?MNmgJDpmUKQPT=SFWJJcI5aY00tnXXZ zuC9{P{Rb#KB6$jZ&zHPn4hVAkQglni@ZcTM@vCb>8_KU-8Z1T<{Kdt5mb}(K9Nqx+ z=vrNVx8DC_9b;r`j8yv8xcF%Kf;n@rj$!U&M64GTP59nU7b`1CT0{RkX(rX$%R6RnDMOZQb&1Vj+ zzz<)w1*>F~zBYn{GVr+ZvLGGQ{YDlM<29Z-1_l9#sqU7O7?1x~RfNnyaO?Dbebk*p zF=6MMmx|#yhi974-}e>V`Dq`5{e-O*)dB{Bf;?@;wp2XdDukH69Vfgxlh-s|$3uz^ zUkMGJIGVx!JEA^ppUI;P07ge@Tg&QBe>Dah=`=G2r$Y+JG#PvWxzgh2@!i7N5(6?K z2mB)eZ{VR03e;krPu1x7#M7*sDERT|WjlxWNr<^za$|imn=vwwzQH{L99h2*Pc|cR zg7CE=;XGacpb$Vk52X3Nxhx~X{(^W2hgb2*x87P%j@Z*kFZ#iZH^FJsPe(EkfTSw_ z07*?-26~K_IjkepxP2$j-$#tIXQll3Qh7@x#EbRItCPzJwngeb!0NJFY`nLDGr3uI zDnbwmyIH&br;l)mT(ql<2=jw5_ky439Au(!cQt1|;?sQLR9n-0w+%a%{Ac;={RKzL zy*EP0$tcHtyrKF8fm<1f7MQ!YFslOcbr1ctDH?NE|C-SK`rE;u0K}YTm9;UIjNS9L z4!AE~z9x-kl1x`TABi>}Q3nyXE343%aUi2jU{#q_h+H|#qB(NK_Ir5BMJ=n2(ULYI z*cVap^SKik@kFXNd#)y&0(OD8h_APN^)hm0A1Vj1ld<#n zWNeqW{gmdd)y`5*&w}>D%EsZ?a`EH_b^D8Dz8ERFdTYK$%~40x%b+Ig&)87xlxg-* zO~gun9w!VYNl|hIcF}r?4*rI6AlLRS{=J8SsAy8R=Fj67Jm5-zn=}qH0lGrK-*dS+ zBhUjQF1HopnnrMw!Qss54E%-voN74RlV9U6$kt}6nGOL~7VS@2lMO@!qTu}mrI$27 z*DIb7HXd!JDsr2Uv>s6}ouCk?%G3n|jG&w(NjkdF*}MN+DFG?RNn`}6GhJ0P|4p)h zDi+B&UA+*cG}%f&Q%O9B_Yy=KyL1R~LC73^AHJ!pWI)upw5vc_f5#oF|3qFz(3PKC z6=dliy>s?}72?XFxX`#E z7bROzwin8enR+C=Tx*R?o7YCOMSt3(Tvn|8ziJ|#5{MXpa|uhbAS|gP5drGP8vUS) zKixb-!cRg?WQ#DHHiZA#oNi8vS_v-77%HRi|AGsHO~_vp>a`nj8f$)Gi|<=nmCy=@ zx;K(pL>lL3uwtM`*W8y8@kTN)$C^J?GxW~jg1e(G1XmumL_;U@w8{UOSja92tZXE} z5WC93!qkbad=rQk4*t^NP1kj%M>szd=|Kkj_4uml_dRmPP(`x2nTHamKl#4E?SiLf z3js^Wd{$-Pf1$;K*sD|}Vq0b=j2Lsa<6jp{g%%S}(hP30yJE$$MrTUM; zP7GHp%=1qfTkHAvD;c=r)60*B%Uge%LSm1OOohm74iaIAGtu{&2cclN@p0B}Sp|v2 z_@B!-MBZ@Us_JAU$R3J!{>>nM%}S)W@dtxh01(q!P`3h6xr3<1OAutZJxXAO2v)c+ zV_SPhh%h3*WbsP-UaD@j>h%B5z6DwM+vC5uRIREW6y_ZWFbAor5R$7pdNfd(ix$L^ zpCS2TZ+5p@kZ9g6rU6p20rP^;7kkXy^PVCs9BZjjNNj*Fxa6)4XY1C=h+yaYz_O0$ zpU|IiX!S*0VWJ>dRcDX%Z3%!%o}D^>Ij<}K*Moy7%x&sk-{3b-WRXK=39vSlBPwIM z%0HbJilFt7ug^UJBf!&_aO1xqNCR;hRh>-s)nA+c3xm$-zKF31GqKg4RY;K0SCD}~ z@Nwy8s*J5f9B9JKD8@^k09z3Gl1||!ZPQy=27tah0RbHGhgVXHT)i$iIkJhpjAld| z8b0N4(+1Ikq5PidX*!yj__2AJOtOx3(qHP7X#oS9IylcuBP2!8fPH=wZP#%Um zLDdHaxq3$nnyq#}tW`)vAS`5%swB-}`9}qp+cGs!;(D<#U zAMtFR>AU26)7FV%gFq|4m%aroljM{Ct$2XGK#_GLiR%;xVSktb?0^)(ouUKj_%;&K|LP zSJjXWL8_$4CYJ&?>-kxh$)kjD!5v^o7QcP2)RR;MhEW|!%}0+!i48zH17yeZtf^ht z10I!)6w9>9l+aa=5|RbH+45TqTSKoGtAKC|btnnkCGp|bu^P%mW$LkL7Un z3zTmv6=x2l&MQIm@j}oYQXdO(;L_ncIw7|(;^_9gftQJq$@2wNi?v0&Ifl2HOHF40 zTl26WYxQ_wf1~9WG9>;$7_md~hN}(Y`Nr-())9*fp%YUinY@-W_V36P5;?vxfnILn zyaw*_P4{j+a9@IgMu=Iq=gFGdSu{d^r}>udzfA}}5d1WyILMk!r|evd80>dtecjui z8n7sYfCiwL%n}VkKpiTKcp1vYL%~Zj#zUEH7ie8;e1RAfkl__N*^N*P)`<12@K*qY z-w#{XN{nSJK4!`1W?c#x%OB|NZ=`PBKfS!4!S>E5xoYZ+j0l9KQ%>@u6i0*{s*vcU z$l|mBoq{Op^78oNWhcF(x!&nZ7S`;wNl(ERe0Hz3B;Vp4i|24JtyB=+uFbpkVY5U} zgtGpux?xmSo9$${o9AIJ35rtUuK(^&iH7c;9= z4n7lnB){?|KM?w!^By?ThlDLBp4N8p;&O%QlIwmiY}g_!rn$@tBPDU(IcKS(!&~W~ z%cYdIL64C}KF@+3mPXnLTij{s+`mS#8#prHlo3{2V)*N2K@Ce@su&58W!xmqrN!GA zin-%)$4)f6n?3gzl>w3B*jcFbFuWEw+wuw_-{c%%PijpA^=6#%z9+XFMT+L)LC$!; zilyM|NX3uOfqibegz&cgwHIT!3J3)amZcZpU3faWsV^Zek)HgL@^M@6+P&Bh(}Mm& z3`Z!E?cI>4^rcP~eA;o-rYJfZvE3M_t$KP@P*oTW68*1WLk_op5Xwa$Xhn!S!8Y)! z)$tsR)A!K0MBWG@6wVAE5Ery!n)=Sj#Wm>%F9aq7J7h7>9`%`MSd*?w=$x({Q5ICe7rC<;o!nqK z|A^%Y>1_MISG}*Uk^2mF<593^%VzO##pi0m)(#X_TV{G(ptS!nFXO*hQk#M>!Otnm zbJKLSV*VE>QYEs?rJr;45*6ip(4#T)WPSX?kozfr(Wqs|ZGru}X}@@r4|p%kopdgW z$mxD}cQTVBQ4w+SooLvJUJxa{lsf(_ZF?(Y9SQJHr`1T$=}_jWPG&VD6B6bRR4;ha}(4*-jw~!e&cfE;}iW z=xyNDvUbYQxlBjuR`q10QDN!q{RLP_in)8#;*5+d9wqRn_S@1Jddv@CWI&az2vaH z(d9^)ZYfQZQjA!J^0l}aaht~B8C5);pf@wH=A*!v_wG%f0Mm;x+=O=nD`h-)NT+|Q zIiSrR1@-SmkDfM@{!shY-B}GKF^i+2r8C_ylJKym(@CGS#z6et-b?lTfe$@FPz72_Ufw-L6zD6JX7 zM&B#DE^5z?pbn4Dc?9bqJ?8+`GIp>AO00B$M*3{_$+H>t*d1NlwDjq?r;rFmQg?`=-mhc86XxcoTrB&5ac{R-NVo(rr?hr6H2mZs zmq{wTOW*-3inQ|lUBh~L6qaP4OF~auJhaa9tJEGjsVcnZ%vPlIV2P{|5KeCKtxo{~ ztiOd^=_}&`@_cel?bvRb0#v8fHCifT`hL)lD1X5HESTkCCs^IcJ#k2X>ZrpY3J(m+ z21{?-aK#`#LL8zbN|ljx19bfLOLF*1$J*&JAwKPaBcZ?IVuSbH$@@%G0M6i<+Z(@L zbsTF2h4-D2_2ZV!@vuMg$JCxsxVbfgZ$fsp9l7zOGim7r&qb3+)`cco>Y>Et)dq-KMopF3aC%-p=c^-lI#Zj$7VBpW9k;uT9g{`Ejs|4Mjx)Jk z5@yn8g3~}x6ks%nu(6q{C5-l#;4R5Aip3B z3Cfm@`o}a6@DW%7mIeIGMDsEfey^_3d|wO5fhMzKEyTbaZT45Ho$wCY@|1y;di=jq zXP7Z7g=F$7>EaMzN#vz&6Rfs{MB@!`>#>@gXDoTJB1*c0|5&vAOGq~>c(GYhKR#bA zRU@?aLtY+^R=ePptyihH6J-W+^Y*}ro3r{ZAS^HRl$ndS$B&h3A$=5wO5+tbN{zoiHIMPx$g&n*X=OV1kgk+7oQFF#{67<95n{LM_8aP?V7%xXv{%(;? zpnioL7G#703}>6r^G(>@byz~pIv5B#T;xFME2DRJf>w*IrSEt(nYw0eH%?y&6x{eag_;Eje8?^Z# z2MuK`mQjYefi8DT0M1%U4@Gg;NO>b+#gT!}f zmeT2652Q)MPdqM0EYfVf6MH&l=&OAA&!$oO86=yK5kz}ZT{J3n(WoqA$RZNYOaBEI zpi!iSFtr3=xT=dCilieH|9R*!xCMzexKP|Oy*w#Z!WKK|iO&vcu*A*JPVFwUs=~U| z&@qNbUMA%#G420NJeZ**A2{HKFS*+sQHImQRo~ z@9P`SRB|)VnxEm2eng+6qD?eBije5!UY)}B1fIUD+Ja}0p_fA$+a`4i9Hh9kfy$s`>2^>}M>za9XEc>GCSnsp}QFSrUN)tO`*5 z!EC5L#HGzhNVyx&RN%8`Ypge4ILct(r!k~6%L_9|B4+%3=w6>;rI0#I?kXOmBpYxP z#s&nrMN7)6d9`AMfg<7fTZ$mA9PW*^r=eRu8U%RdmWN4N3@Iux3mi0Q4Jscs7Or6o zZ=Q+&L0%lA+JTANw;>}sdYk@hpS*$TtWD~LS+$w%g8h{#q%NBK8m9D}hwc4Wp|-&3 z)kbe_G3zbNc)%92t=Y)#ueE3Q9K`KSppQC2V6o1nNu)NLDdf`eZh}s9Zl{D)!+g|Z z?p3W~=(*ziEMxmCry36+olRdv?yuN2Edyt6U;lor&_47lQW;2R&)=)}Q@e>D!&-Vnuw37TeJPnwk)sEk}(cU)&WVEOlv2$DO zJUNig(=5IeF!zbo)B8DNp|0SLiKzaq2bxPxI2^_lQLnITd;FTPc_(kqfH@!P(CcCw z$nLK}J7FIDwQ2Jq#BGqDxp$p}OW0GN&11w@@oF~H!_%g~D2gAkIe`P~CI|a=upk>qNdOd8Dj*kz zrdZ!>Ou&t4B3VsvvBRUC$YcG|wEQIS@~uH9>fCt9*Yb^fEt}ps^?hA^H*c$n-`Oi> z7k=Gz;cv@TCdanFdc@kiRb2;}U;lODwm}>b2H5Yu$SuMBb9epmAE~QXzj+k;X{S%^ zw$jkmzwFTdOZG(QsSs;@EKym^>ir)t(#w;xNFi0eNi?xN^+D4iRk$I&<~A@PIpY}g zkg3*j%Os}_gc@6n-Qrh~1e>B7;=CfIU|(FYfgq^kL_&7wmP#&q%JB{wqXlvmr)usX zJ-K-=D+F3_DxEa`%Z{rvovaM&x0D1u{I%U`$=~med6JmVa%qE$6qcspcmAt4idzWM zU<9I=wV(bd7R@qHBGf$Rkw{7&yNFtDnsx%8<>FJ`n;h)}OI|CDOj1;El~t~bQ#?^m zxGTFXHg$pCCw|R2@Z${d#OlhU-MNWJ>~P+gfvmQgqL93cDhAsH3(v1&Z43_L2-0OU zCb$d2y+$Pd2jx@XAwTMm?`F|6JiwQ=Hx{Cjo_20pE?r*pPR3YQ>4g?m!rKML%c+l9H^BI|wpab8UOE>5BVexRAqzfx+o#iJ+WF=R zSqUomtQRE34fv!l|KLKs|8T6&sxujaJ%7nlCKf-?DlrkbV8jbvF)Kz*L9G|dw^^($ zn4t$uPx@V&2uy2^wQK23`g#>tFVa5UMISXBi*5o?_570Kzyz=FffW@zPOVxX#N%H`9EdYvOl( z)hzW6cd}2HvbIcGx;nNH)n$^r$F*Hrv|PS-b7dU*t+ttPuW$S~tvP9lNy-+6zzJt3 zY_Z_qc;b=YFh`_vvfHeO-(7doPY>C8oELD|hV~#`DR(MMvHD^#ab`MsA4&$eC2@s2 zG0dwemZ#5pMKxV&FG_5ko-c{l3@9q_yrU+2EREQ9H>;<^khV`Czm=$bTK5}c{MF}H zF6$H`T)O5lrAIgJB&2pLLf5`PyDMNmHdRohY~hae9Dm5w9na?uF=`W?i;fcopUsmL zmGDjxXA?*Da(b3;5)^7Uf+Jn3<-@Vy?77o4-iRE>I=XU8gD#xk`%hfgASp@bZilK7;&@} z+$EndtUULu>Z1E~i7$H)1bBFyBM=al0k+oEDWxn+jD zx`UC*ixX%cZ=Y+$&qxq4yM4O07DP{Hke7h#s!;>?&2R9dC8$b4jY3Z8G4YMS-U$lQcc6}0qi%D-I$Rvj=9+S+T!R59Vb%k&|7B1q6 zs-wLg+X#@O)Qu9d7p6X$W_orQ!q|;uk8<(Yr%9VDRgL^V z@OKC0%rFX1B&JS*KHW|*t}R&INIQ=&Ae^##)Fk>atI;p1-I+i)?5NOnR}EJ@^((`< zD8@0nJl6HpD>8oM$N5?k;dMJ9JLnolN?i+yrP<#HW5~X&gVW-HtAk=PmuX%G zLm5<0bz^3*;1g^5=o{sq?`$HHR%suFv7GAXqm6z&lzpE;sI4**>V>wX9qj=(i|;`7 z9hRY=t50e8F+k<`xZmQ?u9+~d%BQVM1c6?c{O!+{_+a4YCReY{ekI9h1 zkwvjKi99WITTnnROAql=s^->^_1)hWd@YA~T=uQ~-S8B3Xnq?#K0bUhmwHs%r2Oi9 zWBI8JG28g!dLnd25R(mMzyBs}qOF5H%hW|lbgUK-FJ+hrMwL%5uMh4a1rw_LEr8&R z2up3sTGkKMWY6$3lv+ojbSjYhU(vHVleXo3?tHGmiS0v~N#}EyP*lmnO?T zKydb7oiI1NV}g@krOK}IUQxrVdSbJ&mX76FeM6L3$D>Y&73~25ZUnTmT=gqg>`aUv zp3KC}984>SY*%C`_FkQ+XH9ohDNoUjR;=zAqk?j%v?jn3BcAtky7`pHZ|1x_ z*Vu=)Lq%VkXsN!D?W3b>sGj*D`cG~LVzvk+^bTWlqreV|PO-kyfsDCt`U!fKdvJaE z=0@2eZ11O0O-Igw&#>Q>>fsKOQB^{d21-G;3`I)X*4;gF^K`H8WsFqy=8~iw#N?(X zQ$-~WqH@5w7blF}w_R74_DxnBlOsS%QorqZe@cVe7+DfllUJD7Ty{MszMMSe>&#p7 zQK4U9OQ#k@v~6OarF!OtC_Le%3WfTVlN_?Kr~CV$8Vu?MWoFn4=1>;Sk9u}R?_`p9c;+b#Wu_Qx{JeyBxd%b&>8n(Ue3@vz_48iqT z#+xk?dE&W+u%{?T#!HI!yL@)y04(-w-!zz3J~jQGb^O!Wc69vIa1?c`>6>sKP3%(@ zX>H(cm@CdRCyNhVt%+=ep(c@BN8J)k0XJwdQ&iF@s#KtzRJ^AAbYzoH<+Sv>v8Y82 z>Kr3rx`Ya$NMsjUhHVG;t%A;TSf8{+WCrxU9F00RrUJ1Frd@5IX z^7Xnto@cK4(waueCqcsfo|C-l;92=ffB9QCtKGRs4jS*~yI0MIt}M4#{apS~TML3K zj3GpwD#x?gz@ts$oFy;s<)R`%rgf!Iz&z00?&Z}kQN z%a{306?59{Iu-QgkG{)Ny;hQhZ(xnIpO3NbO7-=g9PUQOd_teXUf3-kl`TSGHr9tX zoSMLC8jNM_sPC6P->TbL|NSpkcYDL;lAwn}6jI3gV?twlgpK?l_orN<$|4pcUY&n^ za`+H)ZXr@s(p;(_>jCRayG5WNX=Ax7WiM(<`E;h4J2+#6ZA^TKe0SSR8Fk_f_EprffdojNPUAc3yj8fZ7p>Vor8P=>k}q zV_mAhwD!UzZ$e9#gQLF42@^wqS|y#=8YUc2HO+2{5<#*37IGo6Zzpoeq`gvfLDiaY#P<@4?@SJ^G&5KrP3&E&MBJ+jEkHo z-wgyTA3vKWl=6DdCp>OThO!zFOq0$?dg0amoxMoCA5$xv{hF2CevzQY+@m)H4vVz% zUH(njN!ZxkwgBD6>dtH7JbggO6v$i8>ZYdH64gII5l zGXDwA)9tO2r}R23`lKf{SsQL6&@R4kqcgv0SOp3PXiO2TPSe~>`;_69kiNX0AeJ{X z3*Q zG>@m&W)X=97-U7Xn-kC>as==4xtEEiL!iEh3ycdDs-OY75Tpg2c`1{Rklf1{EkZJ27dP7>+K6Lq>&4a#aQ+8{F38O1F zNnz#SwCteUK1Ld&$sabu34%N8bI(IH$Yjk0{vA83kixpvGulwR9;kggVPrPJ>vFE4=hp4mxtT&dmIOH*GO`Xerz%51Tl zmVD?mW7A->U*NHgl@bc?-lfB6YeA9$ zhh=4)7>jChE7AnCG43!m-2ZdwcbFquYl+fDiETH9eV%b==TPs*8gVVxuw~%Dmgw%{ ztx23eR`#7x(YaN(I%`Q2R$C<=?8)j{qpDc#Gq^wSyufhu*60LmK7(7TD%n_cf)tHR zxXHdnS=e8e`!IX}^p*e6C$MWrn61eii*>{1d`U0mkj{DGG)t7#^15l3az0*QQ3VNk zyA2_oNe!g>OXf}T@|U#gV7W#71kAwP9HJduF`#QlNnBZGOWRbqA;^9mQD1{u+9hou zM^SQ2w|U3=3%Lb(s9s_!PWYuk_3EOS`jd|5^X0#`oGO@vDJ_wS1U>z{u8lJ$(ssE= zeCk^>1^C0LM0$%*lmRm(Il}K|z=Dnh>lGeXX$^$z1BesS5#f{X7N;^*Xh|1suA%%v7Tn;HW>LARr{(=pB))7x{*+ieZU$B5f2E(dPSWXRVyUHbGdF=_b zf)6odqrAND81Y($&12I`Ox@pYOGpjEVB$u*TN_uz^QI}DyOG(V(mYlHBc9bhLd1z8 zQMOiUI|e+m?m2*pcP;V{t{Z*tJF@iUF)}wQo8sriuuPRp$T$%V13|KJqTO6Sdgn$$ zeg1IrAYGvjM#pDL9{rzJImmX&#<@dKk+eW!4Q9S}PE6JTPfQp*IH-{7d(_47J4mKcK0Ow$=%8>RzSzE*atWham)g|odrxCwXyw7f+Y)w`rj+Tx^5?@9f_ks$ z4(-r>`clG{>*q1bb(hoM1ncY6HVbDdBQC_CrK7!m?`j6qvhp;Uft>N3-QfGx&Uf&X1faAR|s2KgH(r}4xe4fCe%w+lPx zFyX9)PIvBX?n+SiKoHY%$+xF#8;_8ZHW0+r9k6jrDBSquC{oVqMcIT*pZAySee}h< zZ1335AZnRpu;pvZ5fcG)a6NlQmC zv*_0{!hykv3zdHd?<8;Kxj6TAwwYkZSe3(F_n+%6+F=yU(wfe#PqerDxiHX;eCv*e z?XzgaZYiM4h#rzu+k?>z3<37rW}cKJ`?AYk$#6K=?sk^&?G_={%Nc9`oJe4+ts)Em!EpEPc+g%A=_8 zUDTxfMe&*dN8zlq`sKhO|8CWa^|GZ67h7d1c6vK!izL$NR za*Gk@>I5;lMRQAhot75;JfN75X(8KZMZ|4K5vdVAJY^Y7nY%c}n<^O!vA4$T(@)BI z^}xk$6&c7sccE&FenqFVoq)qqi8AKD;EVs5o@Na|4l>R4RGIlz`>ywsCK1_a1Q7ZK z*2ULK*aiqpmtE#qdQxb9fZEzG!;?Rk@5>r8)4JPZwMzc2@`2?b5wYyzeV@(D+;5eV zn*(pT*fdbTXL|S`*P*{72yi`~n2PPeq99D=b_TWb9Rv z1CLV0LSPY<%DNw;hPM85QAgif!8bPAE(Sx+FD?Ag)YV>7%cmr$ zzyLtFC(TNE&#EEbpwO4LA{j5G(K5~tnxzq^V>108O6LhNaIR%}-h5l%O9KeyAY3to zcKzV;FgDnzs>OU_n5lh!jc!Kd3`}WEB-Hr1kl`? zSyP|;Q@V(G7WiLcONgsidvG4`Hd5^z2;{xmW^T0a^rn@^Q@^u~Qr248ECqn@bP$$z zR)lz>`AJj%FlO%MW^$DbeFd4g88fadSo??}AFYPo$|~T!92d1oT7vmjA`M>*Z6n}F z!P!7|yC6Gx`AqTQ6VA|E2yG}|Ey8K_JA8B>D%|KX<uFrxZE?Fd3I(|eo1^=in?M;?E9T~EstPFlO2 zV%(_MT{ze98OGT}BGBhwON&eAiTfYG@Lwz``#GORJPl0q5W?CoXiqBvpCU_AworSd z=t8%fE~g%q&z?UuBr(QOi&|NTJ?kaqwWRu=31uRzt6{Zyd7AHR++dl4|L@JaPM;R$ zD0nCkYM26=mm_>7FIBBm-cuDlBUHm+V+TIwc-gEAQv{XKCsX2B<7BqnM8+%5+ z58g+D3bCFTvZkkQrpUM4YXiZEr1}|cHpU)(|BWw9_FtYa%~a<0m#UQZ-zxvjr!8UU z;z?dhhOpw08ohG+9N$j$_oi>YD^kmKo^!VKMU==wHcnFPV!x|)*H!DRipq{7$bj9qXEpK6-lWVzevxtxn6Vwey;re?7SkAEi{6 zYC=nhWe}IHol|#9s^72v{^!yAun)oG;5c*sd==1EF5Q3#?@8D?+j3lQ0RH99G=wNs z_hpgrUd!;jc56Z`X|>m1JsZH_ycT0$_KOi9dU)a@>^cvJ*3g%RyrN^X;R%x{Shf?WU{HQ+GNCItMr^ zi;leNVe>$2Qcpob9n6XQ7?VGKZ>))=A}A}uo`7CcQ%jJ}9-fe7<=pShid9+s=%Kg^ zgub4=jbymSYrz2Ein^EOSp(FaYG0~{f`#J+K6fdwfhtBkU>!$kLE36`lqpDPmkmUg z%ZX{ji4X|>L_BD^+3k15$q`RbltKHzwdAGNu_#K;{NL6We2Mv1;B#g4eiIkG%d~VZf_P zv4Qm=54cx>A#5`TVsm zw%A8m!68KqP+Z>|I`C(me_VxX8B|~+!@im_Z~HU}`#O2EjE5tFLPX_v&Ax> zH&t^#IfR_i%t)dP{R`V$t7PS9zj=_z@N5JPMAi zk?HPPK*+={f0271cAnw1O!xX4JT>j0jcn`O5MA!RobWsSfg-(<0jqqmY~Eudd@kz1 z82vllU0nSoxjo?r-8+X*2J4o83e$G9z8_<|5XsWP1yt^c@ID;CCxK~8>y3FF#i?U$ zW39b8hQ4hAx@=?+vg|URlcloWspX84$;IJ>Zg7_x4sgPsz}&Xk&<4X z^z9HJ5~}ae1FX$PooP$_rdInjJ~i!4$hAE&8&erlAk;i;jJWb%!*&NIiHk&q3S9S(=4wAgw$BSoo`1|qKZ?}NF04~Jo@6iz?7 z!&$M~<=Q5ICZ|({T6@zLFddk0X^n+%Y+)oS!j&bOpG08A5EwnF>29p5B_>cYe3lm7x8J_w3`mCbXNT+>sJU`ND4{XG> zwfEeHxorZ9&E(`s#7k+=lrRhxrM~h^K%agrTw@JbG3M!78z69@BvUsSy~5Z>6-(WL zE-9({0~ZOBv$1r1&5+Q(2%|nCvdMl1%$>LsM>;;YCITvb&C-PZ+G<)lE#113M*q%s zMb4}NY-`H+ajH6J^rm0?bSm=Egp0LhIIpXwvHjhFRiV4A-RaGeF1APBcs$`9_x6Lh z?sm@cb#jy!S(vJj+5iy&uSfGWP!3OL*lLSh78WeUcH_;bls%M*EJYZ%g#yA!UlTXB z%^WI!%<`!`mL{^~yyLFf*cNdW3lpStI1&JTf*R_&tcHG#Q4RPSjs&}@PJrB;+aaEv z4RzXofrlGyicuP2xn&bThQPw4XS^ zBSNv~hv@BnrX%oKTj;Ids-}+?JJAp-wi5E02nQgPnX&d2gg!#FbFJh?RRAw(GqmTO zx}kV6Cc#dt$@WQIw=!87*$C4E=aB+A{<)A;)MHI&HqQyiY2OznwqnU2uWkGK65)>! zsF$MJTJTN6oO4k|D`2iqI6(UzIKhxza)k&O1ucb<_d4r`rnCx1A_H({y2)Pa34pAs zw}au8)I@fA3X9?{H3J!a+lyhHCCdS5cuDFMY+N=1x&@npVr%MP%|q|8INp6RS=F3d z*3OlD;caH zyMn{zVFN!nn~qs~UWh*xUbqkCj4VOojfu98rIY?nNXwHNapUx0i?XVPJ;D(XRktXqhhCz2v_C+Y4weJR(KNlDrn@V#Oqm2- zh!|yBzsd4~78*3h9ye_Qjbaj14t|izYt==L_7>q9nuyJH(#cU}J%<}xnQ2onVJ(}J zd(>gI*N7@oRA&IzGz{&h*!Kb0M1pz=EXKAx^4im#LSWNE)4q06fYeU(I3qgGr zTweC8SrX+@w}SnA`3s@Yz9m+I7<@$@rYqg>pRXO9n5V6$yO-l|i3DsV3lOP@n>j)) zYdQq(N~BzOT7<8$s`J_cUP!xa$J&BF(dJ2|ozHSN65xxL>Mz3yq^Um>t9HWlmzax= zz1Y`aVjEB+zukVyg*t8>r@DY7erwt~CS9#WSP(12Tjg7j zfEH9>YXHNjKoFx(NA`qEWx1On4c%&MOk?MA_{}X18lazVg$P#wOp>IFPf?X{UAJ9vuR;mg8Nv>6Z$| zvbLmZz-#)}4Oe`*pZGBxUefN2P&3kb17SA3xs{_oe7&|2z~V&0T2x*;M|jC=M&?aO zy4(ql&2myi#tlT!u82eQN4l;MiPH$~^dDMc)bf+=dr~UAF{-f0AyQZ+GjT|UE zQ3W=VXVBT$mD}~5Ue+&0yq~qNRU!=;Ke5gbLaVKkrOnN7MO%E`I(>w2 zL^i@m_3U%n*nNz$1N1$L$etV2E2E!BHYcofec-DqSX572esW`bq6&ULEW3HId9?Jr zKgId*|JTmd|0S7aaa`JL*3#Xb%E^~(az4$dneL=mHoiO~->&)Y6ia&9LIe677S`@anABp?m|OuYVc`j!LlC&l%I zL3eL}ZgA)QPFHKh}x zY~sQNIEx1giLWwX7ZrW7s61kM@)r$SIJzPS`&);gDt%=7o(is^@xt7K5@4N5!c;}V zxuMF*OJEX1w#E)-_|^qjRoC9SsZN%mCXG~%smzRdj>->j^C4B^a%Z1tG~Q&h7+29lrpYMI;me z@CexYmM;zYc`x0*+fu{#9YlKvq>IIEAp*Z`m&FK}u7XT>x(?_{c}ergW2-9581|F4 zAmRw*I*GHYNaS;$yi){fqM`nAwh~)ql~hKwsJ^1W`&n zlMuz5{mjmn~^|8U*9${F>O&kM8Hjn^I2+{?`({pth1SH$3qbih+44QZ2RhYpdJ z4q2aqmMwjQuCx| zN|-i0PIWzPb@A0Ha!MkiqoPo7{X@vq!>j-=9uocm&|lt|_vE9e#~6=+mYoI+ruZw? zHRZb7e~gM`P(%%4{YF!HxUA2F$*t7=K;WA|FbR_`eU^QK3Gr}oro(t8l-g3Y&HUb4 zpD%3aEiGurCQAYrN&M4uU$Kb!6%l7uONB0Rs7cWd3S4)Jvw?d6>SF>i$Pbd=d2C3X3cut zguLh1sYC7g+yMk|X(M0}xVuA;wMdDmzGSF?Eze+@`@aA%sVX_UTrPbKpYzu%AG z{clrlP0jRtGg^umBNyenf>7F%o{y^n`~NY5ZQsX{jvK`V>Ei4-UCF}ygj}P^&b}qs zWuAJzJ)J=jKf?;amv-UuExta&Bc$MDmAb4|_fFLU4J0{cpg-)v)1&jlZ5uX0Np(Xu zzYeVNw--C;#}b!z{nwdG*o&%Jwjx!#7A>)z>AAzlWF0l(iFndwWv^fHMy3WW=Kugy z9@s{3dx<<2gjf`)12kE1t#i8(=i(nD;1rF5G-Ptpx?Bg{8b}23jFl*lj#@i7Jve7+=cXIbSH3>;AH_*misQ}Z$Pmwd_>7@>X zT4ydXYNQSd4+L3M=saOK9u~3G-hWEKu}K(Do}9FrPf!PH;$Wn`9#NCR4cbd)9uI=6 zk6@9etj@GTH#$K-tDeKtPvwU@@_<92-<&Kc*SW&42!%$HjvB)ZOOL7H*nA6~(V|R- zBq$Z8c60*IR5hJ|(OAO>$oupKz5$wj@YBPo21P5R8U?N>j@!GAuRjiO9a602D$8Ja zNO2i-L)6#IL&sm&aL)1}>_j^8X$bQ;ugpUR7xv#&ACawS3{MQJj&O0 zO&|pHIt&DbnTH~{ngkC#ff`%2 z_v99f^l~$gSHab-JkT`l2IZ+dP(M0zXJ%IA1Gy2`7r1!!Dr^Dl2MS%dmXweZQi|Xw zXT>)0>!Y%Hh<3P>qjkf)hQ>H{gWX`gOVcj`b}5PMmJCuK(|*MO`3JX@D|W30KE2#F zlS@Dv>GZq;EI7=Vh*0zj9WwbughVvv`g#a`KV~-HXgJ~RBP--;{joU>A#dF!}SWcGNKHz@*l;uf_$^( zOE@;)P$5_w$jkZNn~T@`BqAOFF%J3#c!-=8TsA%;kIe4(9HZGtB>vUYwQgR=^!~c3 z6k!>=j~0djAv3soTZ%%wt|}2oG{H#>*_MSxWS%kN=It2}!dK`$yhfF%ls9u&sS&u<;-Qb@H1c~Z& zc+c_oYYnYE3MdV2Sw^_YhtCHOeDJ^r5B%;tu&_8YbOFuEziUyhbpPGW@56-tp9hXF Yv1vQEo^Y!WZD4VK4-5^c-5&k@Zyk*ndH?_b literal 0 HcmV?d00001 diff --git a/assets/images/logo/logo_ios.png b/assets/images/logo/logo_ios.png new file mode 100644 index 0000000000000000000000000000000000000000..a9992b4a52882406e5fdd04907e783213bb82c39 GIT binary patch literal 13158 zcmeHN_g7O*w+0cBuJm4BdJ$2YbWn;xX$qnsO+<=Pq(yoYkS@JL2rbeP5JD9YPHNUe&*TF?AbFp(MATkv{W2aBqSuXp!-^nNJuW% zUVN^S0WC=4Jv{J_)bo+y?G8=G_W!RuXEI1$Qk%jfmClZK+g?0R=E^KHqaqp0b#o@bu;y-*M%>E|g z@%V2O1k?^bu^Qh#iuKOHWj5kVI&k47#G_NJXBP1cOFTIv{NBcYtRajb@nxO(<{?~B z8)0J?>ywL*ufnC*W8dWw2?YF)86xI@h{NOFml9@I@!zNMJwNePJ%r^AeDybMND=Y$ zoUpn@7@o(&z7m=S@u{D2&{lk0C1Gcucz8mXULhVF<12fxeg(LkCR|}FVSEW++lP;> zAfR^%E1USfDI%VL^~xm-%@L66xJVdr{|J}YjDxgd{UP|SaeV!Ed`tx{t)8%mB5eP` zw~Z2}mI>pF#N#t;U?D!G4qwqt7@WnWea1Hq;5){!!9}>-CLE-NF!2ljxgQ@>P8gWQ zH~t_VoZ#}|xPlh^R|LM{2R^zSpInPC?t8^a zStX1j@%_{I_)5aaJk~3l@Oz7Z!r;P6@NFab(oS4)I{~>ym|w$xo5a`j;ioK$&RiM(D;hg-*_kDlavUihqg=3@AJ5e2f@*^GFmSS?+j172&ay#i{`(qL4;9s{ zJDqbs`X6gl*UG$S`d}S4VoVdl^tMLOc}{U zw=XNb&lFe`*CfT%k5{!9u)GqKQ_{xjN9w+6z zmbq9(5;AvS??tRb8ntC7D-~kfzA9Y{7vN;~w*LL7K4zDQcx3 zhihU`n#!x)&-4EQ9;yDY7zmhXyZzw>R_0o(xbst|DX(f8C{7;41R1pCZb*KetOb|< z;9;38s*$0r<|qX8>wu$6&dO=f9wJw&exC~(KOr~fs7QHF+A>e8DFUi5l|jA?v`d!O z0Jn-c3%yQ$|BO=$bYRLpwm=upF*OV7uB3+IqyYP^5wDfp3>bu4wpY>xjYFhZt2S_f z$)a1~qMXTEq%FuQO$qo7P-?tps+M&c>B7!2Bn6oysQ0eNE4yYO1jACbkt;2)om$5j zCJiqAaLEf!SGB>#s0Ej}u)V-7S1J2iNrBv$@bQ8is9av~Lk4@#Yis!zzM6y_g)CCE z6y-i7uUFiL3~Iktx_O^rZIMrJwL{SMukki@Q0fok9zQ-18>Cg0tugrrs2jIVr%{4<7F>n2VkAA}E4VDZ)hK&p_QI z3paq_Pm@JYv}v~%FWgOK$>j}}QP>*jUnfU0^ZC%>*5D(-i@r?X?KU&rqgC zvO5yzroZBD7hE@@Mfnup;=T_&nSMpU(qcvCQu%Vn$To+{$CZBj8+1D6&sOeTDz`P( zJs&4!R+rks*%V6*2Iy%pt1Kja@wzt3G<>*k17ZH6elNp<@J6>~ToW+gEXlDkew&x+ zx{F?kRhv0t$Q&*i?@&Lc#^%E1Yf-nH(Ix_+n)pr)z`ZDcZ}=K#TdYHnoaTig8&l^ zR|M%*ZetwPL`rxPOWG+c1X+!_FboVSwkfv#Yi5Ic<9<@N8H6CmhMqLGSCizcq2zV~ zn&u0vV|D!6hoZ=TH$oZlq9&(Q6Jm&CRL(%aQw^)v%HtpZ>8;TS6kyvJW+-6(_)I8B z=**C$F%f1 zEs~%P4Q61}t`KDkhH3(R^<3y*TeE3`dS2w4^%1&$%4{2W!Lv(gOzl_|V5tKRh%9jp zs=%W{+Pi8g$$uSq#jjxKlLcu^j1MvO{(3Q@8`T&b8Yc&Y>cvzC6Yqdh$^W)?@Wvg2{)>2gX@CcAbO<2$>2qni?2@o`wN6RVTu~)Hb zmxhy%`lYnPLD+YaU2Em9ajVl;;Tnsin^qnBF zddTof=h|99gl>z{e@tJ{g8+5sa%nv?c~|Tf{=ZYM6f}n3&;W$2dh1^l1RmKCR)8M< z51(82j!~Td^0ToDA&moadLxk2XP^RjVK&jfpsiv!zD_V1mjXH`RG{x?=v3YO8>S#; zxe#Aa_d_aZfT3(u!VHTc^0h{9g#(u<*VXYR3>CULczwkDq zA*7N93~QNu1Kr&M3q1d;oCm;Gqx3g=GGU>AqqorxA$@3_|Ifg8m~A`2ohjlz=z_Zo zcW+}vmsueE+Pc}iYdII<(_U^J6#{i@S%joB@i@8sD+}%EPU{rGfqBi;6Y9hUp?}$c zZY{A2M(;QA4LC*UD&72VE4UyF1XW?xn#F4(lBOsGN@np_O~!RK$ua>GQJSfJ2im%G zZ1JftFSI@ny;3XuH+)~90{XH0mg)akLz43oIlcnhcP@+884RsqCvy2eMvUL#v1@<9 z#aM?b3IDIdV3BAB7g7$hcg6bu;WF(oXpF@50fYVhYxeJ!C5#mQ3nLjq`aDBWubJHO zB_hdy_R4?Pf`~rZsigDMT|nN8j>Uo(i_TBPnFJ^{qQ8)|n|VQg{3{}+4lFWL@L>bF zquFv29lg%E^@Wt39L7ipnSui%nyDgGiHG;4$B8`mlckNc`zdH`UMGtpSRf!E>BzEu zs#64YYcT86Y!ZP=1dvy2858$LA#Fu{qnGuuV<%g`D`|_fPy9sw( zndlmH^2bo}TdgZ>Hir2R$ zO*x)4!GRp(VOCyen@8n3DPy_hyPMC9IvB$r1qfciHzFce>@3sE2aDxicxKmNL&!LJ zCzh8%(DxQ0RuXDc8VxgY83b>KWi+kC|%6)78{ zRJ`*ZP;5b)pefnx+ud3_B}_8MGaY31luLmI`V7Kzm%Kb=!Y#dn%Q25)8o`Q7q+7Sixn^Qfp8D%=?1!Ii$Z2Ku+a_E)=d*0yiZk8cdq&9Ry`3j(@%R zoYQ16^RHTlciwKqO09ef@#aiXQM9><82Cd8qN}EEBFS4szAJJCO*|QB=xwk5I8zSW zE)}Bm^uD$>B;0RNFJx>cHu-SLUh~mG5%j^q4*=bvcRiVbqLlGtF;n8oB#4K1uc%RT zxXO@0Xzr41!^U;pyxj7NROjyGGY}8oUOT#e(ia3pUejVnbY}*~O?@=&89^AFhntL0 zZ@K{GJRts8U(|`)Kg(SuxL)?ng7W#@zn??q^7RLbtxV`ylnIJub-+U%ytXiL8?U-*sdxu8Nem(-xt72Nakt3MM z;5SB9!&(^^*~*YP-rO zJb{O?^BAt%1l)X6km3RKx_xl7%IKej}TaSs$>_Na#X%3lE6!UmNZS{|Xh6N?y*G+0P9zgX{*ip)7pt5U z5*4;NmvxaLVN!0~c4mQp^yaxMh!XzPg+laNlSf-j7r7~;?87*Jy)M1WiFEZ%_3x^b zu#*0wIi=fw!JdJjUPCpbnX`*W9}*{4WTZ`o_Z{PZ4+3j>zJOIEdK@I6+jKPRu9K`_ zhL$_IoaXHhh=le7uyt+J+DA3kZTZEm(T=sLQ$#e1{!J@~emsF{o9k27^}?$8%Kor| z*02_ibV0qSFX|4`Zm%mi>52Qc7-XRjhGt-M(iqtJEkc|FjQ1Uynwsedo6{a;blwN< zCQoWEGGK6rFevq5X+m&$NkXIiaDBvLop5=%ENVpCB1F6e4~N0&@g->qja?^C$8urg ziw`ek?*$we+4b2wYaKJ-f|1vXT;e_I{=iu#sns< zETd7tmAj5-?B__s!93}S-p+96IiMJQN4BylXj{Ir@>buwvVVQufX#VjACl=KGE3lc zC2hH-ow~`^3h&HHX#8<&*Xa9aSZ2NCNn4JxB`gYk`#3RK%U4!>7GsehtDrOx!H0@*xD%&jg|wePKqYn3Q#j$`Thj$P<_5 zpsjm1U$r-S&2dJgRGn!e#whCzC^sE}9Un+Ow};tVw$`UTtA=v}?Uz+t#NQh=SUH*{ ziw3*Ypc>pzV<`zE`-NNOj=^W6@+w&Z>aaBQ%O#Oytt{rMlg;;LyR&|tsGC8}*nH@j z>f9eB#C3$e-0@MiG-5wG0luG0p%ftX=lc%%<1M1017rBPq5=%M;52~!?!&2nm+@5s z(Nv`KXLg`@*SG42pC`FmX4+seY7ZI9&Nw;r8kZ3dl$y-9vS`Wc&jb4(h52Q3=Dpv4 zo_%MxasEZ}Jp6RsAdr@5-Zd}wq;0D3J)~OavUVyOogs#}E~7}(xwkPAK-KzZ@&%>K zTUi#a32IH+5u(QBXI8TQAV^-cD|tVz^gUBM5RWmrEXi`vIBFQJ}Nf(AjHrAmUy8c&^ zkvQ-EhJ++MV0HFQIcY|@BZf-?iE2jMk&_PwV(2Y@twMqp@3Xzu^UU}MRd=}Y`N^v) zsYAIbx(eOp5NLh*`D0g`XPyCJ36EOd?h5AaFGey^5p6(-Lh3e_NyBkk*T+o_3kFKI zYb9bLcy+29JI&Qwn6!wOrQ39ysQ1OoI)TZMf>gJYZ;#SDmn_l0A~OcA(k|FOGEx&M zp`AW4?#la`m3O-%j?6LWvEU)wNqKiD7i+5Fen}iX{ev+>rC$RtU%xtegpL~5ayZvu zjY+3yPgYr(sM-3HJV|_V{-9fJ{u!TpHg!`WCpV+XsF+Xy1+Ienk!hS+{ZChpUb_TPqMmp zmBBx%z*-_YFTcp|bi;la_VSC|VWmFa-GRKbLTk`HE|A>LnOyP>Tx_$~bqjDBm2-2o zs(rUUe*I?(aI=%0d5B(#qS8X<-sba4N(`H0Pg3ssXWgo8vt|qI!l59d9XvP9EoOMa z`A(j$d8(?s)_Rn@F~tk(xv}1Hj5HGoYLy4&%nj~!3J?A1xyeTiWz`9(C zb0fDvx$8?nP{T>F5W#qms3KxL@Z>57UN+?#$$y)Iln=L{4}RH*{?b7Uk<^c0wqQ z?V)Q|$gS#-1!UL*R}&>x)QEJR7_p_QUAweN@Vxko6y4Fo?E7PuYO&&Ft*-gzo(E$- z9q*Ukf|DhwDtpiGfcVhzh&syY-v-i+*bYa##vWCH$HikOPuUz>sXJ}dp<9<&fxh-< zoEE{u*ujB zG4F(UDu-tnnn4+Nody}F&UL{%Tlgm1RK|JwOO_5MUX@*Ia2d&;U-O^PIR_Qtd!2Nn zG=0t=Ud7JwFd4K1M~%`rw`H|BTNmD=MONP5W_^~`M&Q`67WfTtn0@Oe7{RWLZE^5u zHD0Yx=AI?Nnlnrcd&3zemtHT)FVyikfvuNu?ipjbuCslVl$IOQ8ciW&X$`hc6R1re<|i%eGT8aZ2!0J^+gEU6Y)t3qDC(UZH0=5f1XU(XK?*SDnj{8f_r1S2retD4`X0O;#^p#3wPluUjMZ$nIoMDG3`F3Z{&I9+syB1(W0oC(X0x~ z_V*B(@g0n*`JPRvnCj7z(Bp^_%DL_pn-N}~B`pt*%u}d3{qrL+aLgk$SljNV{v9DS zU4f2$%5Ue2>@4nr+LIJdbGpyM=k0djd6cVW)0wwP68-NH=_ofVvANoFi(s{7hM4?- zLw6$toYWco)c}rm<4niGAU%pO2Qm*fB^;7v&I#{T#;JY1v zKe6EVs@iK<%*R)49zYeUzWA}veRTYF1VWKn+L)Wjwfl2-Z}!B>RWf%vyWpmaLRm%f z=)6KhDK#f^vz2=S5@>2x&k^ZUu856Tjuoe96xk8$jpXe$I|D26cX=c5D%wsljuTA7 z&(L!Vb|!tpf=R4#U8Eee@gTW}6LJ%z!VPPG9S&trQSPc-6rZZ=aCF++n?WQpj9zix zje|G}A095R6!|bYKh+&_wco4j3AZ&X&8M5Ne79=i&uTuou>FE{wg5T)3!$y*$pDo9 zZXPDFoSqJu(;K~*@w0}sItAULFP>Lryh_l_(CLhuLB=p!7bko4-a|}N2$g_-WL`XWtr12s+lM#uJ}dN^Zcm?yPtB}>O!C3Utx zsSKr;8js5$j!zB`k5L6kbBOKm8^4JhD1WDzt+$1S6km!_xb-HyMkJ9-)% zEUn(lh?TncP8=M@{MIe>2;-m4b+B)vViwrNx11n9t&bONu|+Gh$|=6|x_fjug;9Br z@wPUJxRFPkkH}x*23HEZ4bXuZ)t-%*K)$5wDsXk?D)*G1-b2<3%PYKC66<;wl`g{w zt5FVv!d1axrhoi!F>>ucMn}!AthQT>JcM+(Qpdb9#$1+I{S910@i@DFvj|(R8nFDcYa!Z2Td!oPM5n5t3nqqa(ak7 z<))9Mt$-_=7MZpN?S*tz)0g)FfjZLG{iSgBi0OuRaB*d{h6`Q_mc8mJ@|54dJ5nyt znX#vxhh}QqiWeO;ec7~L8es6Yy~9jhHk{4nrS#PCxbP)!Z;(lt+1g0$T{>#cDXe~+ zyaL;6muEji=BN@`I{yJjGJ80^8yI>o>?28k+0GXp(e+AuzToCyo^6^6gIr~4wF&Li z2>neL4kL>P;Z$#2Z@tWj-4(^%V=N*k*0{^?Z`-{!3LOrR_<5wm%_CR&xRiyW0#3X;xuh615;arHeam) zs1vL2DS4U`OD8AKhTgl4B9)Vqf#nGAK0*5|^UKJ@B*+S}IX=qz8Ir9e0n2#{tc-Mi zQ@WA-j((deU!&)3JH?F zJVpi#P6m0x?<|)1w-3(_LX?zL%kz>h{K6T- zDCC8C1XN1b5BE`|-b~y>6Q_m%N$aP-LXT=Fqnf4t(4CmCDv-$YFzWpPCHC!O*!+8u z=W&*talhJ(?}V^x{^3do{6dB)TZF9)kIsMIOb9rxGwY2i0hjq|dv zPK;JVf~c%J7i9?SDs{{hTX%>t=cabX{lb@y4y+E<1Lg}T6#1PMd3k;(_D9feWhdxg z-b5|HLSCUpYMn8$`7>)2gn9RR-=d|1NHR`#(~m`DS13=ct^9wfTe8Ym-r&xqoG`HV z3Lne@sLw@zjU?k`;|R(=jJ`%Eb^c^+3qU(k)Wyj87NX8I!*eWAaCI31&8dh4GPVcQ z&YZqE&na-GhGR=fi4m5U7{;LV3ww9ePh?Z+=P?mnUv#sxPfviB zl%i_S9|WSSJ+dZp{*;D-855tAq1-bNP4~gqtuxr$6DGg}*KFS+UJwzDAYZ4J?tZ0Z6z$ql3S(DKAeEv zTZ!R)ufTCBitDqCs9=G~mDuX@mOmN9Gj*z?iaeJ1yQGK>GHM_LDfW{aZpT^Urh_+c zPSkb`$O2xXHHC(U@@&>}*;MMRpXYl1jJb<yA724=Zb>MZ^7P4rC9 z#>VcT6-&Mnuin(zTN~g*SXwd?QR;zyesw`|JuBBme;>gzuMp9b2Ub)(Go=N@K?K5J zxF8_=t&))lS(MN zqFeggRa5=dj6@J3T!5~rS!#$&S^5nXeT?ZgnljLz6wh^4zKadZ7JtPD@xLz(;G9Su ze10XGuG#S}kqjQs0YM(t9SZ65Ax98bM*421D}!|W*vb3pnP0(eY9hOj$#U|3gs%Gn;!XJ7rX8G$R+a%{8^054vO|a2fuX7Xk4m^ z3{3ZDFtUCfe(rH;U=jMdVOkSn{)8jX5oUZXvv}KxOw0u7K4mkcW zd)O)OI7h5n6k3uY&d7oX@@ym8W_3DSi0bhAC@E=S(S~7wzK_%vyJoCUgUBi0P>H)d z2G-ZhM-@Be!5IhI{1LCi&HW|8VUu+&lR=GprN~eBGVaKU%Fh=Bv+OXbe>_ni%~SM> z^)K@97Y26aKRyq3oKg8m-+L#ST+GNmfZ*PJti^^|(_9=GrMB5l)o7lLY1_}ctu0H1 zrynO;AnOSL<)*nmy;{xRPT`QCdl`!2uomFFTKS_|!Qi{nnIKM!-$Jr61(0$!M9c94Qf><@)fft|pACxH|stuI;dhZy;)ENuq z1LqOFQ@cMta&Hpj?--@~hn=?qb-HP7z*B_J(ZH}8&0Svtm&vaIK<8cX-j>M(5q@(RZ6#Wkh=@JCdS<^;~3q=oelU+)2eO!EU{H+X^^?bN#-Z0!>CM*qoyy?!F^O)hrhh9( z?3)2!32^e5O!vHv8jwg0ej134)xNnFW$=v0C0I=W9LXM^Z49w)PgZ__=AR}%dgqV1 zETdGZ(dpcld(qScG`$9ni?cJ<5bI&~G-Z7>Kaq}VAE4otjEh4OYvpT?GRpSf`w%|s zok@icpAgS%)t@RWfb+*5j3nsok0IIDZ@o573R+v&LBl>7gBDYjdsL_bmlxj+P_uYV zM*~x_^{NYMY&Yxt75<#%vbJvkfa)FmdsyTyP8bXw zdtFT# z&IPiWffk{r$y0<{Rm@!A;z+=K@T_jvl8psR=rZ?TC9Tc}`}C(GDeu*ATsJ|P0e3H! zOJ9CwJDJ6A@UD-r`At!4`WgWH!7M?h=#$T+IILu|^{r*f&OpqS z@1jZ%>}@fZAIQF5ZGJUxY8QF4mal-dhzc(Wj1Un#o78|*fmC# z`9*&&@B*jQh))^0!4LlYv^(Cyhayu*tYWE-&XDY1oTg+lZa$qmM|~}*tS6z~&tY&0 zCg@^lEP&UM`8%;+($=W4ExlRajmT5N9^t=4TFcR^1pPqQ6x^x8pOhC=ZpYB~83ivJ z8@U08(b~|YlSSF{IYX(L#r0`ZY~~&LlZg;sE|&DIAN@}-*me4R+{pLIlVR*3+9hMx z7ai+KAbe~787boWSbg5KC*vX7pg+}geE~`=LK3yU8a`Qby~?c6#!6=y(UOUE#-tlL z_4ol72e8;L>A~RElY$}qDB1Q70;ylvaz0v+hP9?RjiB&siWr!4x5ET;+I$rI>8TJI zW3HljqE1_zm`cz0K2rmAi_1n3&Q0uX4VW`qso=#z(ue~kUv)d z`Q$V>5MyOb>K4b7hFu~RUCA0Z=&Xo+i+X~|y!(qJe{ezi^IYH-0!}0i5kA~2q&0LQ zsGR(y$iBU&h3QD@#}q192var6fORm7NnNTU!y|ZP=o6%>02qs|z5c*z5I+?9MBg%_ zHNtD`*s|h{d!$m1hb=~aoV~^>cz6>z%He3XbdKq~hsv)Zu8A6>=_;y+7K{dDw-KFI zX;@0KjR1FlaK}s9&b8wVo%Nt*4d= 2.7.5) - url_launcher_ios (0.0.1): - Flutter + - webview_cookie_manager (0.0.1): + - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) @@ -43,6 +47,8 @@ DEPENDENCIES: - share_plus (from `.symlinks/plugins/share_plus/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - webview_cookie_manager (from `.symlinks/plugins/webview_cookie_manager/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) SPEC REPOS: trunk: @@ -71,6 +77,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + webview_cookie_manager: + :path: ".symlinks/plugins/webview_cookie_manager/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e @@ -86,6 +96,8 @@ SPEC CHECKSUMS: share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7 + webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index dc9ada4725e9b0ddb1deab583e5b5102493aa332..7f19eb8f97a12567b440c052e50e06bc15163d46 100644 GIT binary patch literal 25069 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 literal 10932 zcmeHN2~<R zh`|8`A_PQ1nSu(UMFx?8j8PC!!VDphaL#`F42fd#7Vlc`zIE4n%Y~eiz4y1j|NDpi z?<@|pSJ-HM`qifhf@m%MamgwK83`XpBA<+azdF#2QsT{X@z0A9Bq>~TVErigKH1~P zRX-!h-f0NJ4Mh++{D}J+K>~~rq}d%o%+4dogzXp7RxX4C>Km5XEI|PAFDmo;DFm6G zzjVoB`@qW98Yl0Kvc-9w09^PrsobmG*Eju^=3f?0o-t$U)TL1B3;sZ^!++3&bGZ!o-*6w?;oOhf z=A+Qb$scV5!RbG+&2S}BQ6YH!FKb0``VVX~T$dzzeSZ$&9=X$3)_7Z{SspSYJ!lGE z7yig_41zpQ)%5dr4ff0rh$@ky3-JLRk&DK)NEIHecf9c*?Z1bUB4%pZjQ7hD!A0r-@NF(^WKdr(LXj|=UE7?gBYGgGQV zidf2`ZT@pzXf7}!NH4q(0IMcxsUGDih(0{kRSez&z?CFA0RVXsVFw3^u=^KMtt95q z43q$b*6#uQDLoiCAF_{RFc{!H^moH_cmll#Fc^KXi{9GDl{>%+3qyfOE5;Zq|6#Hb zp^#1G+z^AXfRKaa9HK;%b3Ux~U@q?xg<2DXP%6k!3E)PA<#4$ui8eDy5|9hA5&{?v z(-;*1%(1~-NTQ`Is1_MGdQ{+i*ccd96ab$R$T3=% zw_KuNF@vI!A>>Y_2pl9L{9h1-C6H8<)J4gKI6{WzGBi<@u3P6hNsXG=bRq5c+z;Gc3VUCe;LIIFDmQAGy+=mRyF++u=drBWV8-^>0yE9N&*05XHZpPlE zxu@?8(ZNy7rm?|<+UNe0Vs6&o?l`Pt>P&WaL~M&#Eh%`rg@Mbb)J&@DA-wheQ>hRV z<(XhigZAT z>=M;URcdCaiO3d^?H<^EiEMDV+7HsTiOhoaMX%P65E<(5xMPJKxf!0u>U~uVqnPN7T!X!o@_gs3Ct1 zlZ_$5QXP4{Aj645wG_SNT&6m|O6~Tsl$q?nK*)(`{J4b=(yb^nOATtF1_aS978$x3 zx>Q@s4i3~IT*+l{@dx~Hst21fR*+5}S1@cf>&8*uLw-0^zK(+OpW?cS-YG1QBZ5q! zgTAgivzoF#`cSz&HL>Ti!!v#?36I1*l^mkrx7Y|K6L#n!-~5=d3;K<;Zqi|gpNUn_ z_^GaQDEQ*jfzh;`j&KXb66fWEk1K7vxQIMQ_#Wu_%3 z4Oeb7FJ`8I>Px;^S?)}2+4D_83gHEq>8qSQY0PVP?o)zAv3K~;R$fnwTmI-=ZLK`= zTm+0h*e+Yfr(IlH3i7gUclNH^!MU>id$Jw>O?2i0Cila#v|twub21@e{S2v}8Z13( zNDrTXZVgris|qYm<0NU(tAPouG!QF4ZNpZPkX~{tVf8xY690JqY1NVdiTtW+NqyRP zZ&;T0ikb8V{wxmFhlLTQ&?OP7 z;(z*<+?J2~z*6asSe7h`$8~Se(@t(#%?BGLVs$p``;CyvcT?7Y!{tIPva$LxCQ&4W z6v#F*);|RXvI%qnoOY&i4S*EL&h%hP3O zLsrFZhv&Hu5tF$Lx!8(hs&?!Kx5&L(fdu}UI5d*wn~A`nPUhG&Rv z2#ixiJdhSF-K2tpVL=)5UkXRuPAFrEW}7mW=uAmtVQ&pGE-&az6@#-(Te^n*lrH^m@X-ftVcwO_#7{WI)5v(?>uC9GG{lcGXYJ~Q8q zbMFl7;t+kV;|;KkBW2!P_o%Czhw&Q(nXlxK9ak&6r5t_KH8#1Mr-*0}2h8R9XNkr zto5-b7P_auqTJb(TJlmJ9xreA=6d=d)CVbYP-r4$hDn5|TIhB>SReMfh&OVLkMk-T zYf%$taLF0OqYF?V{+6Xkn>iX@TuqQ?&cN6UjC9YF&%q{Ut3zv{U2)~$>-3;Dp)*(? zg*$mu8^i=-e#acaj*T$pNowo{xiGEk$%DusaQiS!KjJH96XZ-hXv+jk%ard#fu=@Q z$AM)YWvE^{%tDfK%nD49=PI|wYu}lYVbB#a7wtN^Nml@CE@{Gv7+jo{_V?I*jkdLD zJE|jfdrmVbkfS>rN*+`#l%ZUi5_bMS<>=MBDNlpiSb_tAF|Zy`K7kcp@|d?yaTmB^ zo?(vg;B$vxS|SszusORgDg-*Uitzdi{dUV+glA~R8V(?`3GZIl^egW{a919!j#>f` znL1o_^-b`}xnU0+~KIFLQ)$Q6#ym%)(GYC`^XM*{g zv3AM5$+TtDRs%`2TyR^$(hqE7Y1b&`Jd6dS6B#hDVbJlUXcG3y*439D8MrK!2D~6gn>UD4Imctb z+IvAt0iaW73Iq$K?4}H`7wq6YkTMm`tcktXgK0lKPmh=>h+l}Y+pDtvHnG>uqBA)l zAH6BV4F}v$(o$8Gfo*PB>IuaY1*^*`OTx4|hM8jZ?B6HY;F6p4{`OcZZ(us-RVwDx zUzJrCQlp@mz1ZFiSZ*$yX3c_#h9J;yBE$2g%xjmGF4ca z&yL`nGVs!Zxsh^j6i%$a*I3ZD2SoNT`{D%mU=LKaEwbN(_J5%i-6Va?@*>=3(dQy` zOv%$_9lcy9+(t>qohkuU4r_P=R^6ME+wFu&LA9tw9RA?azGhjrVJKy&8=*qZT5Dr8g--d+S8zAyJ$1HlW3Olryt`yE zFIph~Z6oF&o64rw{>lgZISC6p^CBer9C5G6yq%?8tC+)7*d+ib^?fU!JRFxynRLEZ zj;?PwtS}Ao#9whV@KEmwQgM0TVP{hs>dg(1*DiMUOKHdQGIqa0`yZnHk9mtbPfoLx zo;^V6pKUJ!5#n`w2D&381#5#_t}AlTGEgDz$^;u;-vxDN?^#5!zN9ngytY@oTv!nc zp1Xn8uR$1Z;7vY`-<*?DfPHB;x|GUi_fI9@I9SVRv1)qETbNU_8{5U|(>Du84qP#7 z*l9Y$SgA&wGbj>R1YeT9vYjZuC@|{rajTL0f%N@>3$DFU=`lSPl=Iv;EjuGjBa$Gw zHD-;%YOE@<-!7-Mn`0WuO3oWuL6tB2cpPw~Nvuj|KM@))ixuDK`9;jGMe2d)7gHin zS<>k@!x;!TJEc#HdL#RF(`|4W+H88d4V%zlh(7#{q2d0OQX9*FW^`^_<3r$kabWAB z$9BONo5}*(%kx zOXi-yM_cmB3>inPpI~)duvZykJ@^^aWzQ=eQ&STUa}2uT@lV&WoRzkUoE`rR0)`=l zFT%f|LA9fCw>`enm$p7W^E@U7RNBtsh{_-7vVz3DtB*y#*~(L9+x9*wn8VjWw|Q~q zKFsj1Yl>;}%MG3=PY`$g$_mnyhuV&~O~u~)968$0b2!Jkd;2MtAP#ZDYw9hmK_+M$ zb3pxyYC&|CuAbtiG8HZjj?MZJBFbt`ryf+c1dXFuC z0*ZQhBzNBd*}s6K_G}(|Z_9NDV162#y%WSNe|FTDDhx)K!c(mMJh@h87@8(^YdK$&d*^WQe8Z53 z(|@MRJ$Lk-&ii74MPIs80WsOFZ(NX23oR-?As+*aq6b?~62@fSVmM-_*cb1RzZ)`5$agEiL`-E9s7{GM2?(KNPgK1(+c*|-FKoy}X(D_b#etO|YR z(BGZ)0Ntfv-7R4GHoXp?l5g#*={S1{u-QzxCGng*oWr~@X-5f~RA14b8~B+pLKvr4 zfgL|7I>jlak9>D4=(i(cqYf7#318!OSR=^`xxvI!bBlS??`xxWeg?+|>MxaIdH1U~#1tHu zB{QMR?EGRmQ_l4p6YXJ{o(hh-7Tdm>TAX380TZZZyVkqHNzjUn*_|cb?T? zt;d2s-?B#Mc>T-gvBmQZx(y_cfkXZO~{N zT6rP7SD6g~n9QJ)8F*8uHxTLCAZ{l1Y&?6v)BOJZ)=R-pY=Y=&1}jE7fQ>USS}xP#exo57uND0i*rEk@$;nLvRB@u~s^dwRf?G?_enN@$t* zbL%JO=rV(3Ju8#GqUpeE3l_Wu1lN9Y{D4uaUe`g>zlj$1ER$6S6@{m1!~V|bYkhZA z%CvrDRTkHuajMU8;&RZ&itnC~iYLW4DVkP<$}>#&(`UO>!n)Po;Mt(SY8Yb`AS9lt znbX^i?Oe9r_o=?})IHKHoQGKXsps_SE{hwrg?6dMI|^+$CeC&z@*LuF+P`7LfZ*yr+KN8B4{Nzv<`A(wyR@!|gw{zB6Ha ziwPAYh)oJ(nlqSknu(8g9N&1hu0$vFK$W#mp%>X~AU1ay+EKWcFdif{% z#4!4aoVVJ;ULmkQf!ke2}3hqxLK>eq|-d7Ly7-J9zMpT`?dxo6HdfJA|t)?qPEVBDv z{y_b?4^|YA4%WW0VZd8C(ZgQzRI5(I^)=Ub`Y#MHc@nv0w-DaJAqsbEHDWG8Ia6ju zo-iyr*sq((gEwCC&^TYBWt4_@|81?=B-?#P6NMff(*^re zYqvDuO`K@`mjm_Jd;mW_tP`3$cS?R$jR1ZN09$YO%_iBqh5ftzSpMQQtxKFU=FYmP zeY^jph+g<4>YO;U^O>-NFLn~-RqlHvnZl2yd2A{Yc1G@Ga$d+Q&(f^tnPf+Z7serIU};17+2DU_f4Z z@GaPFut27d?!YiD+QP@)T=77cR9~MK@bd~pY%X(h%L={{OIb8IQmf-!xmZkm8A0Ga zQSWONI17_ru5wpHg3jI@i9D+_Y|pCqVuHJNdHUauTD=R$JcD2K_liQisqG$(sm=k9;L* z!L?*4B~ql7uioSX$zWJ?;q-SWXRFhz2Jt4%fOHA=Bwf|RzhwqdXGr78y$J)LR7&3T zE1WWz*>GPWKZ0%|@%6=fyx)5rzUpI;bCj>3RKzNG_1w$fIFCZ&UR0(7S?g}`&Pg$M zf`SLsz8wK82Vyj7;RyKmY{a8G{2BHG%w!^T|Njr!h9TO2LaP^_f22Q1=l$QiU84ao zHe_#{S6;qrC6w~7{y(hs-?-j?lbOfgH^E=XcSgnwW*eEz{_Z<_0R}P4oY^pMZ!_ zh?h}>pES+?KYsnU^%SGF{J)z|n0+ls)BJDOIYvFj|Igmybutdk3|#Dtx{CiFzh>m) z`n%;M10y4offA#%2vM3Dg?Sm?eP&=`{{Q(qBReajf%3moH-DLc;u!j|X%-du|LOy? zrzw#A|Lc!`+fOq&=>0o!gP9o33}ONdXKyhu$b$^x=3w$R`+w*P1GZF*O*0P%(7_0Y zaI*is^@PdEfGEw3jEoGw{`|Z31jPCG|Np(`OfH5@nsUV1%)kKjBruVH<(Zl6bQyU# ru`0u^nNgSzh#s&?5!Os37^0d1g?dgS|KcpA00000NkvXXu0mjfnkkai delta 279 zcmV+y0qFj<0;d9y8Gi!+006pI?LPnj0Blf9R7L;)|5U~J`u_j-{Qm)0oAmqtj@kOz z^8J|I`-|B6ht~R5kG+%I`zf~eztraM`u^bc{`dO)zUlmg)%x%C`E}6wSI77~z4s`y z^XT{f(eM4n?EUff`e@AgO~UxV*5*r_%Uhbj5N)LaQj!wdIe!-b004GLL_t&-)18pX z4udcZ1u-#g(~z+5JN*AY5?>Gw7hsN~k)CYt4dQDFxbs5*_&e@Hj)wtt(&JE<3Eq*D z;_gQLvqXoKv=I*gWqM9C(Tvu0>=?hTbOp9!6k6AF;>f6|S5%jGEE}TA9h)e`Yuiu8 d7)l?o1NFcJg%EAfM$P~L002ovPDHLkV1g=?nv(zk diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 797d452e458972bab9d994556c8305db4c827017..bf7de64d7411ade8d15982127eb1b7d75fa6aab7 100644 GIT binary patch delta 646 zcmV;10(t$G1C#}j8Gix*000A=FFF7K0$WK$K~#7F?bOX{6G0RJ@b_jniD|wYvoU@| z5^06Dwo0WGKdMNi=%IoK@t}fw6~wCtPyPksKhR4NZ$-g_qD9f7g<`E1KiVilV@y+% zG;Oy{v-6y7K-A{2DVz4P@373y!2Eb`-i%B{uo)q5ifX|YY=6NPyg@LPR%mEkd@H}~ z)IpS_4S~O;`0xaFHtFM66VSC66n}kTT3(^CB+V2+;}~gVolWJhY0BiW?(oPb{_Gv@ zRxlpn_l9tPTqiU6b*n~U@HAl1BV9^}Vfj`+Wak7-KXY}5G34Wa@(cO(nhRKzVJr!V z=yJgJAUYl9!+#L+!Tb`bC3M;=MaH9T_&%(+7y+MGVYYy6G1k|OK9`v=+GGg0VI~hz zMKAKVG&b*GM_R${1aE`LndTPxqY>@SpmhBVdJWk|uaoYHLrr4EUcDhTuJwQbj zJxu3lPw`m0 z=zqu+jcjPV)PBeoh;wAD%Icm7lsTkxkY9kXBaM7? zQrwn*X4MjX&%lBzT|Q}Ctr0%d!osCwD0y}J&I8f8>=D_BL g1zWHMTkyK$Hx9ek+4*g@%m4rY07*qoM6N<$g6KFri2wiq delta 390 zcmV;10eSwE1(pMl8Gi!+006rnNM8T|0E$pdR7L;)|5U~J0au$Tw)XJ){%+3s=lA~6 z@BMVp`S<<*VaoaP`~U3u{%g(ou*=|m)B4`@{`33)?ezIj#Q6OF|6IuUF}e2O>+>eB z?J{?+FLkYu+4_Uk`r_>LHF~flZm0oBf#vr8%vJ>#p~!KNvqGG3)|f1T_)ydeh8$vDceZ>oNbH^|*hJ*t?Yc*1`WB&W>VYVEzu) zq#7;;VjO)t*nbgf(!`OXJBr45rP>>AQr$6c7slJWvbpNW@KTwna6d?PP>hvXCcp=4 zF;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f>&$TNdN!< diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 6ed2d933e1120817fe9182483a228007b18ab6ae..59305c75b774a9823537d910e06b75baa494c753 100644 GIT binary patch delta 1033 zcmV+k1or#F1Ct1l8Gix*006a~P9*>U1KLSMK~#7F?U`9m6hRQjt9n*e&fNu8KqM+r z5RJD%6pwht1Fxv@LF1dp2k^xYVd8^{FD5?vqL`3q#Kgpi1_+==#EpuIpt1-8g10bT zR>NX23QMNfUXAn5X6Whdp80icRdvnaAtLzKf)tS_CXy93kl=yz)JRXbhn|JuuZBj1Sq`D8 zyI-Ea@l#LjBY(JZ2HDa+aW3K&L7(dB?8Cnf z+Mld~Z^U=e5|EL~psGE^ue!Sapj$hHMvlLrk4BLxuW0E|i7z7!EJ1I2V?_8HIS)id z+4O>l)B;#+;D@dB@e8IT!t^X^XdY~6R{iWq$zBNh#eb}+xNJ7^lHv?KU_=8$EE;(_sC<`$42 zX-U?OlF?(;%eS^pS=m5UO)}go4sX%n*0HprQ~@_9Pc=AKc!aF1mo*`EY6;~hIq9Ns z8qCaAE`KyVOB~%Muhh|v`+T%^t%w8kh^6}?Ye@8p1MB6@2QX-JH+&sXUp;KKuis;I zYfq{A1rSA9v3%W4x0|SRE@t{nLnEiWKnjf7vBkoC{`@#qF7dXX?mm@OjrN(#V*eUZ zTsX{apSmP#A6eU%n}$~0K|gi$jQQQmgk%^oxbJ88$SJToXSuYwi3yTY(vYsBaKtV1HZAakrpAfp1h9T$)4Ngmf z?0NULv3`w1sCNb&vhg{OGnD4zKKpq@X)}Z zJa-LzNn&SQHkTo(kZNqKjKjDe+dGx#Vk-jZA%{VM(u`ai*^ zuc|#W+d8-olf0UDgcv|)`ozA~>SYbr&A-Y!iT7<)>lNj3l^1zj&{hoIFo_jK{L#P8 z#Pz?v%QG(*+LT;qQ*xnA$%Qs07uu9uXj5{bP3b#kvtQmdt_|w|0000GM-ShrilfUZt{^9lhT*&z4_x{-O{Rv#2V9EI}xb^~1iQe@7)8g(7UZ4B@ z|4zgB>+<*9=;^^)>d)H7pzGjuM>Jnezy3`@G2r z?{~a!Fj;`+8Gq^x2Jl;?IEV8)=fG217*|@)CCYgFze-x?IFODUIA>nWKpE+bn~n7; z-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGrXPIdeRE&b2Thd#{MtDK$ zpx*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{HY|nMnXd&JOovdH8X7A@L81gR6s+1Hs9mi8Mk)5G14(gwmo~BBA@ntMuR_S@F`|cCv>z zFPmTH?an+4GeZf&QVP^`P4|B;-=)RsHpm`#JG7C`;<)_!#((hyIBXOO>I)lkf)KF? zu0FerbskPV@nsS{PDsCir?Lnx*sV0$&8OFJl?1O#eiJN;i(VI`GU#)O-Bapn)|WQL z`VJsqqn~%TLPST+@^c^dQ>HR9x`6;rbqlXt)Dz zA0e9)yJw&?jeqq}PH^M^``Q)j6?-wcpe8@a=QG6Nxmb+Qcu1dHAvHIDMpf4%AV!uQ6aZlu%Skq8Zbuxb|ukYtxfhDzoX(Rhv}N`*Z2U$ W(Tq&2PPk400000qVZqE6)=lqo0`vF#&*75!I`TIh@_d&k*HoEtQyV-iD z%Xz2D9EQRbeYh5Nr~y=#0ZD;^+vz0$004MNL_t(2&&|%+4u6C&2tZM$Wf&dzefR%A z(^3-?6X>hnCz2Ba@RH&`m!pgy?n@#@AuLYB&}Q)FGY`?vcft0!vht0Z@M&ZeNCWXh75gzRTXR8EE3oN&6 Q00000NkvXXt^-0~f*9e2umAu6 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index fe730945a01f64a61e2235dbe3f45b08f7729182..4eb2bd8da04689383b9ffdbe54211ee81d936d2d 100644 GIT binary patch delta 1019 zcmV(76j2n%&$+XvuG_lm?rOSb zQfgKNMZd8kh?1zNjCu%ye!SI7^k4K6y%s@lK}Gb73?k?klu8z4zb$Yr%3Z@EhC$q-SAB$l)XC_u}-R=Cor;7uURepYA>xc?9QH7<<=iPCJ&gi{aB3 z$j&kjY)VDurTY|#>CQQ}EaH+hAshkz6bN4v6z|890xX;gAwRr%&!cFF$puZxBCdJv z4mH2{8J@g`D}Uw~4RsjskxB5e7lOV?BK$>Gvm1?rFHdY=nF_fEAXx(#%vYn482(kb z=+CFOQLHIPE(Yd%@QU7iPy;DS{xSB*DsC{J*t}wW!1c-@*TBQq^fdu_IgmSr{x;lH z7S*4g#D!!Ha6Mpo07C(K^m_1Zw>n}Vx^tk|hczLrDt|+vd;eL@ZXn6WS)5&-43qR- zJ&Y?F>dY%m)IXq3kf!pr=bd!xG5EbaB&tJy02hk9Y+)4rVX=XsJ1q4}j6Lf{o=bfL zoNx2SL-Tke>{un1)MOeLU4|l#4Am%RXK_d^FXg+BDrVF5g*m4jh(AyV+-~6t3XP%Su}lvi}$!>#g^q*Qi$$s&bmBu zR-V6W9NLD68O4QQ0kn0n8>rMW{mf~ugPVs3)tka-+-!b9ciQB!3&#F>oLP#VT(Nq- zd9@ijB48=O8rco{)Vs^>IF$KE8Rg^!ea6P6w12o-o@kV(t{aCN(C6WjfUC{sv#^nA z+~OZzHX81T(+Z4z_44R>-uaB(Yq7withzI5ZBd z$`WfU>ScGRyY0b=aQP-v4RL9a*t|?sm04a;w|3Q6R}98&TqrSUp1I9C2k(3s8qf76 zlwNk(L|sDNK%?53l>7|Gk920sOMV;@8+4ayvVv}Qg}~YhYHgRT?c<%tUFFt#Q9E5{ zt(0^h+{Lan)K&=tiP7d3Fx8EvMd)>FuJUtA&FeACg$rg)3Q9Wq&)$(`SPrh8a&YaG pgKMW8Ts!69+9?OuPW@xJ-vI>%PlX1I5sv@>002ovPDHLkV1jS{1ET-{ delta 447 zcmV;w0YLtS2+jkL8Gi!+007oyx*7lg0G3cpR7L;)|5U~J0au$Tw)URh`@-w}Xw3Np zS)Ix4{k7)&ujKrh-TO(x_}20L&+q+}+xr1ilg8}*yXgGl_5RcF{f*iBEV%Z~-t4>5 ziGV;=={^- z?sLQGb)?A{hr$_!z8HbH7kH=vM0x-*R~t>;jsO4v^GQTOR7l6|(&r9>FcgO2dg?%> z;=sK?5%;?Pn^T7LL?Y$@5u?06NuIR*0?Yf$Hf5Afk+lM<^ch*jvO$sU*m9J?JI7eI zGFV6+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9R%3*Q+)t%S!MU_`id^@& zY{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&p6kME1_Z%?`+u)^el0!1<0sd p?Eyu!OMLDifi)An*I;?S-wj=m4RYIt!kPd8002ovPDHLkV1mn-^Zx(< diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 321773cd857a8a0f0c9c7d3dc3f5ff4fb298dc10..8cfd292f387587e0fe55084943d678c2bcc664f3 100644 GIT binary patch literal 1708 zcmb7Fdpy&N0-vGghw>Oji|v@l(kRC>kJiwbkcByjPUvRd6DlO)9FN8paaiRwEsvH* zhT|EgvAhmqI2_E`l~;3*>lKds*Zue2Kfd4Z=kxvh`$)U)gp&nS0T2izYj0;mI8x$& z00AHIL4}h!2y`sc-sXzyeX-SosW7M;>|;WE{>%h=H9D3F1#;khzTIMm`gUH&w{X>&$=$gokKm=E_@XpU$!&1oZ)8xs~L#v zKi{LxJv)SfM?x~YvT0h=Ow4De0r<}v31`q~uuZMJnS!;s&QP69<_K z_xI+T{RKsSq~1o_eiYezw1n)vXXb+4w_n7#o)}ePWbs73oE0v+Luk`$Mg1hG!6%}r zqUayDyGwU77>;0jKmTxK?&a|p5J-KBW^x`hId6$4R(!!a1i@hUoaKq2( zOuR~JGP_^><5I&;;Yn-x;RiUM14{T%osz1FvpOfqLbJ{-K-zvmn<)pD?NUK+gr8M% z=8EZk_wq_Qe5Xlen0DEHubS)+issQLEZm=pjuvAp#8Ub!UgQ?bqt)DhnsglD>V@Rhulk-AS@2>1R-_G zQtg5&7Kq~%AA!1cgW-0bhk(Q3qiJYjj}g;~qsmpkdp%jzsTv*5vy;eE^3myu6OS~0EB!HM#C0bWE_ z>CZj2c*T6;n|H6t^hjBva&C)XOySt0hC7Wq9H^si-6B(tR7b;dy7ocR% z+@#N2mV5WuMN5<_>iRg6$;ijsOQ+IvENWW^HVvgnAqaa!4Ty&N`p#!q6Bb=t|HQX1 zgVNwb4tlsmAW1vzo$r$OmY4LYXwqJ*oiR&Ik=!y%;1B|(b0)Y9zY)1~%%s*L**5cD zYu;=QR5h$E)hlQW{oA>jw`Cv|`kzMiE1HwPmeWCNRfQ(*po?cvK7z6#OuUO}v*DE} zTH_FEaH^(=4ciKKF=#MEu_a>*flhf9h}cX_&C|X_0k-RRdA=p2E<&y9YyNs`=h@`j zsMpuqe>y7m@4br2|=<_Wb|z`~RBV`-<24{r>;E==`tb{CU#(0alua*7{P! z_>|iF0Z@&o;`@Zw`ed2Hv*!Fwin#$(m7w4Ij@kM+yZ0`*_J0?7s{u=e0YGxN=lnXn z_j;$xb)?A|hr(Z#!1DV3H@o+7qQ_N_ycmMI0acg)Gg|cf|J(EaqTu_A!rvTerUFQQ z05n|zFjFP9FmM0>0mMl}K~z}7?bK^if#bc3@hBPX@I$58-z}(ZZE!t-aOGpjNkbau@>yEzH(5Yj4kZ ziMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_stABAHe$v|ToifVv60B@podBTcIqVcr1w`hG7HeY|fvLid#^Ok4NAXIXSt1 Zxpx7IC@PekH?;r&002ovPDHLkV1m*pYe@hA diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 797d452e458972bab9d994556c8305db4c827017..bf7de64d7411ade8d15982127eb1b7d75fa6aab7 100644 GIT binary patch delta 646 zcmV;10(t$G1C#}j8Gix*000A=FFF7K0$WK$K~#7F?bOX{6G0RJ@b_jniD|wYvoU@| z5^06Dwo0WGKdMNi=%IoK@t}fw6~wCtPyPksKhR4NZ$-g_qD9f7g<`E1KiVilV@y+% zG;Oy{v-6y7K-A{2DVz4P@373y!2Eb`-i%B{uo)q5ifX|YY=6NPyg@LPR%mEkd@H}~ z)IpS_4S~O;`0xaFHtFM66VSC66n}kTT3(^CB+V2+;}~gVolWJhY0BiW?(oPb{_Gv@ zRxlpn_l9tPTqiU6b*n~U@HAl1BV9^}Vfj`+Wak7-KXY}5G34Wa@(cO(nhRKzVJr!V z=yJgJAUYl9!+#L+!Tb`bC3M;=MaH9T_&%(+7y+MGVYYy6G1k|OK9`v=+GGg0VI~hz zMKAKVG&b*GM_R${1aE`LndTPxqY>@SpmhBVdJWk|uaoYHLrr4EUcDhTuJwQbj zJxu3lPw`m0 z=zqu+jcjPV)PBeoh;wAD%Icm7lsTkxkY9kXBaM7? zQrwn*X4MjX&%lBzT|Q}Ctr0%d!osCwD0y}J&I8f8>=D_BL g1zWHMTkyK$Hx9ek+4*g@%m4rY07*qoM6N<$g6KFri2wiq delta 390 zcmV;10eSwE1(pMl8Gi!+006rnNM8T|0E$pdR7L;)|5U~J0au$Tw)XJ){%+3s=lA~6 z@BMVp`S<<*VaoaP`~U3u{%g(ou*=|m)B4`@{`33)?ezIj#Q6OF|6IuUF}e2O>+>eB z?J{?+FLkYu+4_Uk`r_>LHF~flZm0oBf#vr8%vJ>#p~!KNvqGG3)|f1T_)ydeh8$vDceZ>oNbH^|*hJ*t?Yc*1`WB&W>VYVEzu) zq#7;;VjO)t*nbgf(!`OXJBr45rP>>AQr$6c7slJWvbpNW@KTwna6d?PP>hvXCcp=4 zF;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f>&$TNdN!< diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 502f463a9bc882b461c96aadf492d1729e49e725..fd0c7eab935a9eea9b29b8671c98df2603fe9485 100644 GIT binary patch delta 1477 zcmV;$1v>i51k4MN8Gix*0004VW%>XB1(QicK~#7F?OWY%6jcU#i%bls1JrH(O?u6MJPl>Qfmus(^6QL z((W$Zex>d1+~Ya3V?RlEx84h8m|vRRowIXy?(d#+e)rrvQ-2>KiU$e4``v+i4Ue$f z;SqK_Ji=~=N7(J~2)i8~VYkC0>~?sB-42hi+u;#*J2VnTiDGJIsU7kI0`y%r19TC_ z!MOGDNij09^egj&H=!tB({(dK7h!q2{jO{o!wKO&!op|L3+!q%4(yz@S)ZH~14B48 z0*|cLb={2p(tq^35Gvz48h6H^ATVdF3ZhL|(?zp^O2Qb4$!{*;#1tP)PP3)&R_f?+ z;%UC#N{3KskAiCmTc4j6k=O!L(h}h)#>WJr01%;4vf)rd;YD+W(vY@&3(vMdO@(7g zrX-ER1Bi z#70F`P~oMshf)ud1A8NjHHpG_Vv52T>eqm2h>8*-&qD12!cbYdRFhb!uHDArk-2l| zg!`AqNDCA_)VUQsCcY)gc|Ir zh#A_-3=b8X@4brcePTN0w6ZK5XH}z`g!TMPhjrnK@#0f7G&;!l@lc`^H0DOA_e3o|wiqorPce+kcZ z-sO3rk^*?N&e**L);eQBVQ(JVW6_VC?UY})L1V4jd{*sYTa#xeA;kg5sW&&KmR-7t3h2AABLj}yQg8tgM_;0<|5t72F8|J++2fK!t&P+^T>YZr+)+& zEed<5!^7z%#7dS2@bp$9Oyvi@wwKGYfUQ-@RHk-0K#zx)OvPCOHr9w$ele94RAx4; z!LdoU<_9vh%K;iZoEB=*t?8uWqU_s}P*L7q4daa=Ai^C3Y+P57v0?Vm;Nh}Dk>}%? ziPE?xlymddK|zFvN3nkpf4@ow*nhE6Ved3}IK70p8RK1!+zCTrKG$kJVe3+_kjSOw z=5-3Yr)6K7XC@}Rk~F47;J28q%ypH<8?Cbyn-b_D99sGQGxpu$w}ODNuL<@yDc{`G zbLU~DLM^+li~d30_Ic+eNBmSnQ%KuOh-d;S35{$6Qg&REpZ(0upV*{{t$)zv;q~?sB-42hi+u;#*J3PW}hez1$@Cdsd f9$~lRfg}76k~x3(5qL8Gi!+006nq0-pc?0H{z*R7L;)|5U~JDYo_jSDXF*|5nEMy6F5^ z$M}8I`uzU?*Yf=uXr;5|{0m;6_Wb|A>ik^D_|)+I$?g3CSDK^3+eX0mD!2CP`2NN0 z{dLg!a?km&%iyTt`yiax0acdp`~T(l{$a`ZF1YpsRg(cvjDG_-U$Er-fz#Bw>2W$eUI#iU z)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G!hkE!s;%oku3;IwG3U^2k zw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn_j%}l|2+O?a>_7qq7W zmx(qtA2nV^tZlLpy_#$U%ZNx5;$`0L&dZ!@e7rFXPGAOup%q`|03hpdtXsPP0000< KMNUMnLSTYJMKx3a diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 0ec303439225b78712f49115768196d8d76f6790..c2d7c25262950c5fc0fc50b3237df3981c932391 100644 GIT binary patch literal 2335 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..e1f6fde249931500a47dbc81b4ff467431caeec1 GIT binary patch literal 858 zcmV-g1Eu_lP)S!Wux?O>vG}V_F))IKS!9dJ z5cH2geH9?a84GNwU`^339F>TDUVf`t%*^4|Ylac4FlYWGv=bl7IsI2Nv1l{QwBTN(=DXd6=1#vx^bo3yPj${-_Pwd$q=6 z1~OE=K#+%ehL>%R9cJhC<_`JGG^9+#94F+tSiT$fm5ADM{mwJ}?sM((300q|d&`|B zUIzsi$dFPGt9Oa6&+@6MbC{lmg$PxAR#(X?iZI8aHP-1j9*ehqv@Ol{u^o~9l9D}8H4s#1iCm6w!;k6bPX4~OGbStEW!&>Py9uqdr01YF4d#rRm_;TWWxqufI7^oK(q7W(-`hAVW$paN|Ndk zhP*`$+p^TKSHYqsy{VOMJM8o!Oina#f+25FC-G$Pmz|<}c*O#h(?wnQr)5H9+O>=I z#+VFZk+g=xy!C@D_bYf2QWG&}JKgcIB-_~C;h(T%zdKiJ_|!G^f4vi9EfY&cCYFj! kEESnpDl)NDv`MkP1O7Y@V=D)$ivR!s07*qoM6N<$f}t>)zyJUM literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..48ce80ad48e37e180358d3e6db055f2ee2914d72 GIT binary patch literal 1925 zcmb`IhdUbx1BZi(5v10cEv0JKDn%n4MdHM$ty(+s)Cg)+IV)+*pp9~|PDPEjQH0n< z)GAu52sKlepyFKZtMT>wAMSbH-~0O$-V6sjm>|D2KL7v_w1!(a{``RdkC*eO&yzX1 z0016@wS}2;EC;(}F3j0QvS-Yw1+=I-?$~Y(3Y}B`?06A>(Loz?wHJG}h*nR$9S%X&&(TxJg5BfNwnu?z$doQBaQnftU7Nh6 zrMAd;=HR2Q-Gsv>0uW#!D)#^cO&;XCZo~N>GEGV5T4=L3cGB^#?6=u?jm%6FGyM%& zu6lNCKfVN+WW%6uIl617Di&N0o18+mO=LvJ38ti5h%P~>#4=eQ=h}&lC1J&>%;m0&q+6>yyt?4Zn@70uohfgA$C9L7^1DP;eiu_G;^-!|@V776&apuLw{BaHH+pJ3HRGK00NA<68(vVp=r`baw&b^_*YTKTjc&i*6d^ zZX1wVP%Cd_laAtl8_jUTMfUlW2&Glk~h*LbMO*~rT9kMP4AXrEeV+~LGxy}3LhPP8S=8S zjw^CMs^?Jiw3{uM)URv|a>knosCW@UsJe=}p4sYbaE|Jb_b1pdpaY##u)l@tZ9y8J zez2*lAaSKSf%p<0uT>J0VOjs+(wD~m#8QxiYfMdszis@frqpFt_q`iJ`bMW7&iBSd zrnzE16VaWCMZ(9~dN&qh|K=?TOFg?HqDjV3d_F5~#_skCX<~?!Lg*VJYwS{U*XilqAkan{c;)F!;v9AL{O*enOeY*Fv!H+~Va{P|~u<#_z-G zn%VoUb8k9Sv&g`ZFzER|gjRgS@y&$9#)`7rE&<#I7W>*xGr#}cYwY6U=bYb@0!-X2 zg4?^#dWC#_E)9DG6AHcyv-A3;aB2F(8JLpE45|Ja7as^@Dtb8$cN;6IkwLYc6r%pj z-8#QlcbtF*@$eRzYgLy1Ymz}#zH5YtUY1X^_}WK_Bb2gE*HH#2D#6lld20TJ2EJoE z#ls-mF^e$NYY8=#`BHjSYpYy~-eF13&GSkeK*}!pmUffqw-`(Ej~9@_H$r{G*8g0z z8|gg=1Kn%498g{zyE%%~Q@p<=2-_Za|IG>_@fwX=#iR#!&gMe-GwR%P`CMhT<^&lz zThGBo9qY|hzpz$6;`b^)awJKOk{QB|5}~Af43jjBU4kWakhl)CMslfzZ$P0>PTh5T zCPM5-3W+mhq`#jpFR3;jPNF^(i8dNpfPV-zYmcjJ-MgjB;V-JERqVaN%YM{c>MHJG zxIZb#$eOTI23%KdYF9i|c`LI6%c6f88<18w&KtwN9_j^hv+R}~g3utQr-bn1UDNQt zn&-?Z*fGNdF55-y6~>a=@}I$Cak;U zq=x}p)x)>8z)bJrAEP zsEWhq{>r{lNo=k+kdTx?4;yBz1B{;kcnsS!LM_j0)~|H_{a=}ko@@2n6kZu45Q9`;@I z)4f*Tz;Zq14H^qo@1i#3L1&>5&gbl$qtX2SCd$!7nx@H~H_rM6^1oz9jwwguS^4JB R_cN~mYfC$ex8^>W{{cNDm$m=^ literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..0b3f7b667b1583494bfc1839e4bbf4c4d6388fd8 GIT binary patch literal 1020 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index e9f5fea27c705180eb716271f41b582e76dcbd90..41f9638c51a869224afb8dc0f5733f403ec5a54b 100644 GIT binary patch literal 3677 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 d7m)tFF42f6#P{{vV1g?bZMQ delta 1668 zcmV-~27CG49EuH)8Gi!+000UT_5c6?0S-`1R7L;)|5U~JDYo_jSDRJE`2GI>`u+b> z#Q0do`1}6<{Qdq#!1wR$2T#*AweE>Ub09v4>;QIg_I^_2LtK$20(D{zn_^HL*3Rj70 z%=tLH_b#{gK7W9-03t&#zyHMQ{FK}Jd(rva=I|w|=9#+Ihp*3ip1$;$>j3}&1vg1V zK~#9!?b~^C5-}JC@Pyrv-6dSEqJqT}#j9#dJ@GzT@B8}xU&J@bBI>f6w6en+CeI)3 z^kC*U?}X%OD8$Fd$H&LV$H&LV$H&LV#|K5~mLYf|Vt-;AMv#QX1a!Ta~6|O(zp+Uvg&Aa=+vBNz0Rs{AlWy-99x<(ohfpEcFpW=7o}_1 z>s&Ou*hMLxE-GxhC`Z*r>&|vj>R7LXbI`f|486`~uft__uGhI}_Fc5H63j7aDDIx{dZl^-u)&qKP!qC^RMF(PhHK^33eOuhHu{hoSl0 zKYv6olX!V%A;_nLc2Q<$rqPnk@(F#u5rszb!OdKo$uh%0J)j}CG3VDtWHIM%xMVXV zmTF#h81iB>r55Is`L$8KI@d+*%{=Nx+FXJ98L0PjFIu;rGnnfYn1R5Qnp<{Jq0M1v zX=X&F8g4GYHsMFm8dDG!y@wy0LzrDkP5n}RZ}&a^{lJ!qV}DSMg`_~iho-+ zYhFY`V=ZZN~BQ&RAHmG&4 z!(on%X00A@4(8Rri!ZBBU(}gmP=BAPwO^0~hnWE5<&o5gK6CEuqlcu2V{xeEaUGt9 zX7jznS5T?%9I4$fnuB2<)EHiTmPxeQU>*)T8~uk^)KEOM+F)+AI>Y`eP$PIFuu==9 zE-`OPbnDbc|0)^xP^m`+=GW8BO)yJ!f5Qc}G(Wj}SEB>1?)30sXn)??nxVBC z)wA(BsB`AW54N{|qmikJR*%x0c`{LGsSfa|NK61pYH(r-UQ4_JXd!Rsz)=kL{GMc5{h13 z8)fF5CzHEDM>+FqY)$pdM}M_8rrW{O4m<%Dt1&gzy8K(_+x-vIN$cs;K#LctaW&OA zAuk_42tYgpa$&Njilse`1^L+zfE<)2YpPh<)0mJ;*IFF|TA%1xX3fZ$kxPfoYE=Ci z)BrMgp=;8Y9L43*j@*RFlXvO-jQ`tkm#McyC%N^n#@P}`4hjO2}V z1RP0E%rxTfpJbnekUwBp-VB(r604xuJ$!t8e0+R-e0+R-e0+R-^7#e&>dm?Lo++vT O0000nfn_@dFI#zY^C@!2Q;0bhJH@ul&_ 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 8953cba09064923c5daf2d37e7c3c836ccdd794b..371d4763364e312db69d171ce862a5dfc3f3e6bd 100644 GIT binary patch literal 3015 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 0467bf12aa4d28f374bb26596605a46dcbb3e7c8..04d363004c02cdbaecac4a609426084ff5081a47 100644 GIT binary patch literal 3334 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 TjxGJ$Z{Qds= z{r_0T`1}6fwc-8!#-TGX}_?g)CZq4{k!uZ_g@DrQdoW0kI zu+W69&uN^)W`CK&06mMNcYMVF00dG=L_t(|+U?wHQxh>12H+Dm+1+fh+IF>G0SjJM zkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJTkdTm&kdTm&kdTm&kdP`e zsgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>VI$fQI%^ugM`#6By?GeadWcu z0gy9!D`m!H>Bd!JW(@avE8`|5XX(0PN}!8K>`dkavs;rHL+wy96QGNT=S@#7%xtlm zIW!++@*2zm-Py#Zr`DzqsLm!b{iskFNULSqE9A>SqHem>o31A%XL>S_5?=;V_i_y+ z(xxXhnt#r-l1Y8_*h`r?8Tr|)(RAiO)4jQR`13X0mx07C&p@KBP_2s``KEhv^|*8c z$$_T(v6^1Ig=#R}sE{vjA?ErGDZGUsyoJuWdJMc7Nb1^KF)-u<7q zPy$=;)0>vuWuK2hQhswLf!9yg`88u&eBbR8uhod?Nw09AXH}-#qOLLxeT2%C;R)QQ$Za#qp~cM&YVmS4i-*Fpd!cC zBXc?(4wcg>sHmXGd^VdE<5QX{Kyz$;$sCPl(_*-P2Iw?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF34$0Z;QO!J zOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUCUoZo%k(yku QW&i*H07*qoM6N<$f=}$s*8l(j diff --git a/lib/http/api.dart b/lib/http/api.dart index 068d7d24..62e7047c 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -5,18 +5,22 @@ class Api { static const String hotList = '/x/web-interface/popular'; // 视频详情 // 竖屏 https://api.bilibili.com/x/web-interface/view?aid=527403921 + // https://api.bilibili.com/x/web-interface/view/detail 获取视频超详细信息(web端) static const String videoIntro = '/x/web-interface/view'; // 视频详情页 相关视频 static const String relatedList = '/x/web-interface/archive/related'; - // 用户(被)关注数、投稿数 - // https://api.bilibili.com/x/relation/stat?vmid=697166795 - static const String userStat = '/x/relation/stat'; - // 评论列表 static const String replyList = '/x/v2/reply'; // 楼中楼 static const String replyReplyList = '/x/v2/reply/reply'; + + // 用户(被)关注数、投稿数 + // https://api.bilibili.com/x/relation/stat?vmid=697166795 + static const String userStat = '/x/relation/stat'; + + // 获取用户信息 + static const String userInfo = '/x/web-interface/nav'; } diff --git a/lib/http/user.dart b/lib/http/user.dart index ce3d4c43..f7f19a55 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -10,4 +10,13 @@ class UserHttp { return {'status': false}; } } + + static Future userInfo() async { + var res = await Request().get(Api.userInfo); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false}; + } + } } diff --git a/lib/main.dart b/lib/main.dart index 08059adf..f572a7a7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:dynamic_color/dynamic_color.dart'; @@ -23,18 +24,24 @@ class MyApp extends StatelessWidget { return GetMaterialApp( title: 'PiLiPaLa', theme: ThemeData( - colorScheme: lightDynamic ?? - ColorScheme.fromSeed( - seedColor: Colors.green, brightness: Brightness.light), - useMaterial3: true), + colorScheme: lightDynamic ?? + ColorScheme.fromSeed( + seedColor: Colors.green, + brightness: Brightness.light, + ), + useMaterial3: true, + ), darkTheme: ThemeData( - colorScheme: darkDynamic ?? - ColorScheme.fromSeed( - seedColor: Colors.green, brightness: Brightness.dark), - useMaterial3: true), + colorScheme: darkDynamic ?? + ColorScheme.fromSeed( + seedColor: Colors.green, + brightness: Brightness.dark, + ), + useMaterial3: true, + ), getPages: Routes.getPages, home: const MainApp(), - // home: const Scaffold(), + builder: FlutterSmartDialog.init(), ); }), ); diff --git a/lib/models/user/info.dart b/lib/models/user/info.dart new file mode 100644 index 00000000..9fa34bb7 --- /dev/null +++ b/lib/models/user/info.dart @@ -0,0 +1,7 @@ +class UserInfoData { + UserInfoData({ + this.isLogin, + }); + + bool? isLogin; +} diff --git a/lib/pages/home/widgets/app_bar.dart b/lib/pages/home/widgets/app_bar.dart index eb62ece2..cd070d1d 100644 --- a/lib/pages/home/widgets/app_bar.dart +++ b/lib/pages/home/widgets/app_bar.dart @@ -30,7 +30,7 @@ class HomeAppBar extends StatelessWidget { title: const Text( 'PiLiPaLa', style: TextStyle( - fontSize: 19, + fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 1, fontFamily: 'ArchivoNarrow', diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index e69de29b..9e867c78 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -0,0 +1,14 @@ +import 'package:get/get.dart'; +import 'package:pilipala/http/user.dart'; + +class MineController extends GetxController { + @override + void onInit() { + super.onInit(); + // queryUserInfo(); + } + + Future queryUserInfo() async { + var res = await UserHttp.userInfo(); + } +} diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index c2ae6532..10c7ab36 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; +import 'controller.dart'; class MinePage extends StatefulWidget { const MinePage({super.key}); @@ -10,6 +12,8 @@ class MinePage extends StatefulWidget { } class _MinePageState extends State { + final MineController _mineController = Get.put(MineController()); + @override Widget build(BuildContext context) { return Scaffold( @@ -30,136 +34,161 @@ class _MinePageState extends State { const SizedBox(width: 10), ], ), - body: Padding( - padding: const EdgeInsets.only(left: 12, right: 12), + body: RefreshIndicator( + onRefresh: () async { + await Future.delayed(Duration(seconds: 2)); + }, child: Column( children: [ - Row( - children: [ - const SizedBox(width: 20), - ClipOval( - child: Container( - width: 75, - height: 75, - color: Theme.of(context).colorScheme.onInverseSurface, - child: Center( - child: Image.asset('assets/images/loading.png'), + InkWell( + onTap: () { + Get.toNamed( + '/webview', + parameters: { + 'url': + 'https://passport.bilibili.com/h5-app/passport/login', + 'type': 'login', + 'pageTitle': '登录bilibili', + }, + ); + }, + child: Padding( + padding: const EdgeInsets.only(top: 10, bottom: 10), + child: Row( + children: [ + const SizedBox(width: 20), + ClipOval( + child: Container( + width: 75, + height: 75, + color: Theme.of(context).colorScheme.onInverseSurface, + child: Center( + child: Image.asset('assets/images/loading.png'), + ), + ), ), - ), + const SizedBox(width: 14), + Text( + '点击登录', + style: Theme.of(context).textTheme.titleMedium, + ), + ], ), - const SizedBox(width: 14), - Text( - '点击登录', - style: Theme.of(context).textTheme.titleMedium, - ), - ], + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.only(left: 12, right: 12), + child: LayoutBuilder( + builder: (context, constraints) { + TextStyle style = TextStyle( + fontSize: + Theme.of(context).textTheme.titleMedium!.fontSize, + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold); + return SizedBox( + height: constraints.maxWidth / 3 * 0.6, + child: GridView.count( + primary: false, + padding: const EdgeInsets.all(0), + crossAxisCount: 3, + childAspectRatio: 1.67, + children: [ + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('-', style: style), + const SizedBox(height: 8), + Text( + '动态', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '50', + style: style, + ), + const SizedBox(height: 8), + Text( + '关注', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '-', + style: style, + ), + const SizedBox(height: 8), + Text( + '粉丝', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + ], + ), + ); + }, + ), ), const SizedBox(height: 20), - LayoutBuilder( - builder: (context, constraints) { - TextStyle style = TextStyle( - fontSize: Theme.of(context).textTheme.titleMedium!.fontSize, - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold); - return SizedBox( - height: constraints.maxWidth / 3 * 0.6, - child: GridView.count( - primary: false, - padding: const EdgeInsets.all(0), - crossAxisCount: 3, - childAspectRatio: 1.67, - children: [ - InkWell( - onTap: () {}, - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('-', style: style), - const SizedBox(height: 8), - Text( - '动态', - style: Theme.of(context).textTheme.labelMedium, - ), - ], + Padding( + padding: const EdgeInsets.only(left: 12, right: 12), + child: LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + height: constraints.maxWidth / 4 * 0.8, + child: GridView.count( + primary: false, + padding: const EdgeInsets.all(0), + crossAxisCount: 4, + childAspectRatio: 1.25, + children: [ + ActionItem( + icon: const Icon(FontAwesomeIcons.download), + onTap: () => {}, + text: '离线缓存', ), - ), - InkWell( - onTap: () {}, - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '50', - style: style, - ), - const SizedBox(height: 8), - Text( - '关注', - style: Theme.of(context).textTheme.labelMedium, - ), - ], + ActionItem( + icon: const Icon(FontAwesomeIcons.clockRotateLeft), + onTap: () => {}, + text: '历史记录', ), - ), - InkWell( - onTap: () {}, - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '-', - style: style, - ), - const SizedBox(height: 8), - Text( - '粉丝', - style: Theme.of(context).textTheme.labelMedium, - ), - ], + ActionItem( + icon: const Icon(FontAwesomeIcons.star), + onTap: () => {}, + text: '我的收藏', ), - ), - ], - ), - ); - }, - ), - const SizedBox(height: 20), - LayoutBuilder( - builder: (context, constraints) { - return SizedBox( - height: constraints.maxWidth / 4 * 0.8, - child: GridView.count( - primary: false, - padding: const EdgeInsets.all(0), - crossAxisCount: 4, - childAspectRatio: 1.25, - children: [ - ActionItem( - icon: const Icon(FontAwesomeIcons.download), - onTap: () => {}, - text: '离线缓存', - ), - ActionItem( - icon: const Icon(FontAwesomeIcons.clockRotateLeft), - onTap: () => {}, - text: '历史记录', - ), - ActionItem( - icon: const Icon(FontAwesomeIcons.star), - onTap: () => {}, - text: '我的收藏', - ), - ActionItem( - icon: const Icon(FontAwesomeIcons.film), - onTap: () => {}, - text: '稍后再看', - ), - ], - ), - ); - }, + ActionItem( + icon: const Icon(FontAwesomeIcons.film), + onTap: () => {}, + text: '稍后再看', + ), + ], + ), + ); + }, + ), ), ], ), @@ -188,7 +217,10 @@ class ActionItem extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon!.icon!), + Icon( + icon!.icon!, + color: Theme.of(context).colorScheme.outline, + ), const SizedBox(height: 8), Text( text!, diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 8aa0d570..623a2684 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -247,7 +247,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { children: const [ Icon( FontAwesomeIcons.lemon, - size: 17, + size: 15, ), SizedBox(width: 4), Text('关注'), @@ -334,10 +334,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ? widget.videoDetail!.stat!.coin!.toString() : '-'), ActionItem( - icon: const Icon( - FontAwesomeIcons.heart, - size: 17, - ), + icon: const Icon(FontAwesomeIcons.star), onTap: () => {}, selectStatus: false, loadingStatus: widget.loadingStatus, @@ -386,10 +383,11 @@ class ActionItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon!.icon!, + size: 20, color: selectStatus ? Theme.of(context).primaryColor : Theme.of(context).colorScheme.outline), - const SizedBox(height: 2), + const SizedBox(height: 4), AnimatedOpacity( opacity: loadingStatus! ? 0 : 1, duration: const Duration(milliseconds: 200), diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 3885d8f1..8f0e3cb6 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -164,7 +164,7 @@ class ReplyItem extends StatelessWidget { TextSpan( children: [ if (replyItem!.isTop!) - WidgetSpan(child: UpTag(tagText: '置顶')), + WidgetSpan(child: UpTag(tagText: 'TOP')), buildContent(context, replyItem!.content!), ], ), @@ -200,6 +200,15 @@ class ReplyItem extends StatelessWidget { .labelMedium! .copyWith(color: Theme.of(context).colorScheme.outline), ), + if (replyItem!.replyControl != null && + replyItem!.replyControl!.location != null) + Text( + ' • ${replyItem!.replyControl!.location!}', + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Theme.of(context).colorScheme.outline), + ), const Spacer(), if (replyControl!.isUpTop!) Icon(Icons.favorite, color: Colors.red[400], size: 18), @@ -209,7 +218,7 @@ class ReplyItem extends StatelessWidget { child: Row( children: [ Icon( - Icons.thumb_up_alt_outlined, + FontAwesomeIcons.thumbsUp, size: 16, color: color, ), diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 6747ea69..030ccc8e 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -74,7 +74,7 @@ class _VideoDetailPageState extends State { body: Column( children: [ Container( - height: 50, + height: 45, decoration: BoxDecoration( border: Border( bottom: BorderSide( @@ -91,7 +91,6 @@ class _VideoDetailPageState extends State { margin: const EdgeInsets.only(left: 20), child: Obx( () => TabBar( - splashBorderRadius: BorderRadius.circular(6), dividerColor: Colors.transparent, tabs: videoDetailController.tabs .map((String name) => Tab(text: name)) diff --git a/lib/pages/webview/controller.dart b/lib/pages/webview/controller.dart new file mode 100644 index 00000000..593dd53f --- /dev/null +++ b/lib/pages/webview/controller.dart @@ -0,0 +1,69 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/constants.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/utils/cookie.dart'; +import 'package:webview_cookie_manager/webview_cookie_manager.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class WebviewController extends GetxController { + String url = ''; + String type = ''; + String pageTitle = ''; + final WebViewController controller = WebViewController(); + + @override + void onInit() { + super.onInit(); + url = Get.parameters['url']!; + type = Get.parameters['type']!; + pageTitle = Get.parameters['pageTitle']!; + + webviewInit(); + } + + webviewInit() { + controller + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + // 页面加载 + onProgress: (int progress) { + // Update loading bar. + }, + onPageStarted: (String url) {}, + // 加载完成 + onPageFinished: (String url) async { + if (url.startsWith( + 'https://passport.bilibili.com/web/sso/exchange_cookie') || + url.startsWith('https://m.bilibili.com/')) { + try { + var cookies = + await WebviewCookieManager().getCookies(HttpString.baseUrl); + var apiCookies = + await WebviewCookieManager().getCookies(HttpString.baseUrl); + await SetCookie.onSet(cookies, HttpString.baseUrl); + await SetCookie.onSet(apiCookies, HttpString.baseApiUrl); + var result = await UserHttp.userInfo(); + bool isLogin = result['data']['isLogin']; + if (isLogin) { + SmartDialog.showToast('登录成功'); + Get.back(); + } + } catch (e) { + print(e); + } + } + }, + onWebResourceError: (WebResourceError error) {}, + onNavigationRequest: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse(url)); + } +} diff --git a/lib/pages/webview/index.dart b/lib/pages/webview/index.dart new file mode 100644 index 00000000..17efad94 --- /dev/null +++ b/lib/pages/webview/index.dart @@ -0,0 +1,4 @@ +library webview; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/webview/view.dart b/lib/pages/webview/view.dart new file mode 100644 index 00000000..bee73de5 --- /dev/null +++ b/lib/pages/webview/view.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'controller.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class WebviewPage extends StatefulWidget { + const WebviewPage({super.key}); + + @override + State createState() => _WebviewPageState(); +} + +class _WebviewPageState extends State { + final WebviewController _webviewController = Get.put(WebviewController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + title: Text( + _webviewController.pageTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + body: WebViewWidget(controller: _webviewController.controller), + ); + } +} diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 0a0bd88b..47cd8bbe 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -3,6 +3,7 @@ import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/video/detail/index.dart'; +import 'package:pilipala/pages/webview/index.dart'; class Routes { static final List getPages = [ @@ -13,6 +14,8 @@ class Routes { // 视频详情 GetPage(name: '/video', page: () => const VideoDetailPage()), // 图片预览 - GetPage(name: '/preview', page: () => const ImagePreview()) + GetPage(name: '/preview', page: () => const ImagePreview()), + // + GetPage(name: '/webview', page: () => const WebviewPage()) ]; } diff --git a/lib/utils/cookie.dart b/lib/utils/cookie.dart new file mode 100644 index 00000000..42a2cb5d --- /dev/null +++ b/lib/utils/cookie.dart @@ -0,0 +1,27 @@ +import 'dart:io'; +import 'package:cookie_jar/cookie_jar.dart'; +import 'package:pilipala/http/init.dart'; +import 'package:pilipala/utils/utils.dart'; +import 'package:dio_cookie_manager/dio_cookie_manager.dart'; + +class SetCookie { + static onSet(List cookiesList, String url) async { + // domain url + List jarCookies = []; + if (cookiesList.isNotEmpty) { + for (var i in cookiesList) { + Cookie jarCookie = Cookie(i.name, i.value); + jarCookies.add(jarCookie); + } + } + String cookiePath = await Utils.getCookiePath(); + PersistCookieJar cookieJar = PersistCookieJar( + ignoreExpires: true, + storage: FileStorage(cookiePath), + ); + await cookieJar.saveFromResponse(Uri.parse(url), jarCookies); + // 重新设置 cookie + Request.setCookie(); + return true; + } +} diff --git a/pubspec.lock b/pubspec.lock index 36af3acf..78a4654c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -262,6 +262,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_smart_dialog: + dependency: "direct main" + description: + name: flutter_smart_dialog + sha256: da7ed8fe78e301e3c2cdaa533d13ed3edcf1853c1ba1a7383b481739569f7b2a + url: "https://pub.dev" + source: hosted + version: "4.9.0+6" flutter_test: dependency: "direct dev" description: flutter @@ -725,6 +733,46 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.3" + webview_cookie_manager: + dependency: "direct main" + description: + name: webview_cookie_manager + sha256: "425a9feac5cd2cb62a71da3dda5ac2eaf9ece5481ee8d79f3868dc5ba8223ad3" + url: "https://pub.dev" + source: hosted + version: "2.0.6" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: "1a37bdbaaf5fbe09ad8579ab09ecfd473284ce482f900b5aea27cf834386a567" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "1acea8def62592123e2fbbca164ed8681a98a890bdcbb88f916d5b4a22687759" + url: "https://pub.dev" + source: hosted + version: "3.7.0" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "78715dc442b7849dbde74e92bb67de1cecf5addf95531c5fb474e72f5fe9a507" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "61f33512810bf1ee9ac89761a4b02663ff64e8227b7dc80654642acd660fd49d" + url: "https://pub.dev" + source: hosted + version: "3.4.2" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8251e578..7374bbf5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,9 +63,13 @@ dependencies: # webView url_launcher: ^6.1.9 flutter_inappwebview: 5.4.4 + webview_cookie_manager: ^2.0.6 + webview_flutter: ^4.2.0 extended_nested_scroll_view: ^6.0.0 font_awesome_flutter: ^10.4.0 + # toast + flutter_smart_dialog: ^4.9.0+6 dev_dependencies: flutter_test: @@ -77,6 +81,21 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^2.0.0 + # flutter_launcher_icons: + # git: + # url: https://github.com/nvi9/flutter_launcher_icons.git + # ref: e045d40 + +flutter_icons: + android: true + ios: true + remove_alpha_ios: false + image_path: assets/images/logo/logo_android.png + image_path_android: assets/images/logo/logo_android.png + image_path_ios: assets/images/logo/logo_ios.png + adaptive_icon_background: "#ffffff" + adaptive_icon_foreground: assets/images/logo/logo_android.png + adaptive_icon_monochrome: assets/images/logo/logo_android.png # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From f3b57bfe7ba702d4c048ab91a28dc95d0e8b876a Mon Sep 17 00:00:00 2001 From: guozhigq Date: Mon, 8 May 2023 12:46:48 +0800 Subject: [PATCH 21/30] =?UTF-8?q?mod:=20=E4=BD=BF=E7=94=A8PageView?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2IndexedStack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/hot/controller.dart | 7 +----- lib/pages/hot/view.dart | 46 ++++++++++++++++++++++++++--------- lib/pages/main/view.dart | 15 +++++++++--- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/lib/pages/hot/controller.dart b/lib/pages/hot/controller.dart index 0620ecf4..7f475c2d 100644 --- a/lib/pages/hot/controller.dart +++ b/lib/pages/hot/controller.dart @@ -11,12 +11,6 @@ class HotController extends GetxController { bool isLoadingMore = false; bool flag = false; - @override - void onInit() { - super.onInit(); - queryHotFeed('init'); - } - // 获取推荐 Future queryHotFeed(type) async { var res = await VideoHttp.hotVideoList( @@ -34,6 +28,7 @@ class HotController extends GetxController { _currentPage += 1; } isLoadingMore = false; + return res; } // 下拉刷新 diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index 29a0b177..3bddd37c 100644 --- a/lib/pages/hot/view.dart +++ b/lib/pages/hot/view.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:pilipala/common/skeleton/video_card_h.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/pages/hot/controller.dart'; import 'package:pilipala/pages/home/widgets/app_bar.dart'; @@ -14,6 +16,7 @@ class HotPage extends StatefulWidget { class _HotPageState extends State with AutomaticKeepAliveClientMixin { final HotController _hotController = Get.put(HotController()); List videoList = []; + Future? _futureBuilderFuture; @override bool get wantKeepAlive => true; @@ -21,11 +24,7 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { @override void initState() { super.initState(); - _hotController.videoList.listen((value) { - videoList = value; - setState(() {}); - }); - + _futureBuilderFuture = _hotController!.queryHotFeed('init'); _hotController.scrollController.addListener( () { if (_hotController.scrollController.position.pixels >= @@ -52,12 +51,37 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { controller: _hotController.scrollController, slivers: [ const HomeAppBar(), - SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return VideoCardH( - videoItem: videoList[index], - ); - }, childCount: videoList.length)), + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx( + () => SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return VideoCardH( + videoItem: _hotController.videoList[index], + ); + }, childCount: _hotController.videoList.length), + ), + ); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoCardHSkeleton(); + }, childCount: 5), + ); + } + }, + ), SliverToBoxAdapter( child: SizedBox( height: MediaQuery.of(context).padding.bottom + 10, diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 89b952c0..e1503de8 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -15,11 +15,12 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { final MainController _mainController = Get.put(MainController()); final HomeController _homeController = Get.put(HomeController()); final HotController _hotController = Get.put(HotController()); + PageController? _pageController; late AnimationController? _animationController; late Animation? _fadeAnimation; late Animation? _slideAnimation; - int selectedIndex = 2; + int selectedIndex = 0; int? _lastSelectTime; //上次点击时间 @override @@ -36,6 +37,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { _slideAnimation = Tween(begin: 0.8, end: 1.0).animate(_animationController!); _lastSelectTime = DateTime.now().millisecondsSinceEpoch; + _pageController = PageController(initialPage: selectedIndex); } void setIndex(int value) async { @@ -47,7 +49,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { }); setState(() {}); } - + _pageController!.jumpToPage(value); var currentPage = _mainController.pages[value]; if (currentPage is HomePage) { if (_homeController.flag) { @@ -98,8 +100,13 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { reverseCurve: Curves.linear, ), ), - child: IndexedStack( - index: selectedIndex, + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + onPageChanged: (index) { + selectedIndex = index; + setState(() {}); + }, children: _mainController.pages, ), ), From 04668b3591fb0a4416248ed7a5fd9bac2db0e951 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Tue, 9 May 2023 14:39:52 +0800 Subject: [PATCH 22/30] =?UTF-8?q?mod:=20=E6=A0=B7=E5=BC=8F=E3=80=81dio?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Podfile.lock | 14 - lib/common/skeleton/video_card_v.dart | 1 - lib/common/skeleton/video_reply.dart | 6 - lib/common/widgets/http_error.dart | 1 + lib/common/widgets/stat/danmu.dart | 17 +- lib/common/widgets/stat/up.dart | 24 ++ lib/common/widgets/stat/view.dart | 21 +- lib/common/widgets/video_card_h.dart | 12 +- lib/http/init.dart | 4 +- lib/http/interceptor.dart | 13 +- lib/http/user.dart | 4 +- lib/http/video.dart | 60 ++-- lib/models/user/info.dart | 73 ++++ lib/models/video/reply/item.dart | 2 +- lib/pages/home/controller.dart | 3 +- lib/pages/home/view.dart | 67 ++-- lib/pages/home/widgets/app_bar.dart | 23 +- lib/pages/hot/view.dart | 2 +- lib/pages/main/controller.dart | 37 +- lib/pages/mine/controller.dart | 3 + lib/pages/mine/view.dart | 334 ++++++++++-------- lib/pages/preview/controller.dart | 5 - lib/pages/preview/view.dart | 5 - lib/pages/video/detail/introduction/view.dart | 7 +- .../detail/reply/widgets/reply_item.dart | 36 +- lib/pages/webview/controller.dart | 7 +- lib/utils/utils.dart | 21 -- pubspec.lock | 8 - pubspec.yaml | 1 - 29 files changed, 469 insertions(+), 342 deletions(-) create mode 100644 lib/common/widgets/stat/up.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c6b5ee31..c7f1a66e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -5,19 +5,11 @@ PODS: - device_info_plus (0.0.1): - Flutter - Flutter (1.0.0) - - flutter_inappwebview (0.0.1): - - Flutter - - flutter_inappwebview/Core (= 0.0.1) - - OrderedSet (~> 5.0) - - flutter_inappwebview/Core (0.0.1): - - Flutter - - OrderedSet (~> 5.0) - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) - image_gallery_saver (1.5.0): - Flutter - - OrderedSet (5.0.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS @@ -40,7 +32,6 @@ DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) @@ -53,7 +44,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - FMDB - - OrderedSet - ReachabilitySwift EXTERNAL SOURCES: @@ -63,8 +53,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/device_info_plus/ios" Flutter: :path: Flutter - flutter_inappwebview: - :path: ".symlinks/plugins/flutter_inappwebview/ios" image_gallery_saver: :path: ".symlinks/plugins/image_gallery_saver/ios" path_provider_foundation: @@ -86,10 +74,8 @@ SPEC CHECKSUMS: connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2 - OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 diff --git a/lib/common/skeleton/video_card_v.dart b/lib/common/skeleton/video_card_v.dart index aeff595f..859f95aa 100644 --- a/lib/common/skeleton/video_card_v.dart +++ b/lib/common/skeleton/video_card_v.dart @@ -23,7 +23,6 @@ class VideoCardVSkeleton extends StatelessWidget { return Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, borderRadius: BorderRadius.circular(6), ), ); diff --git a/lib/common/skeleton/video_reply.dart b/lib/common/skeleton/video_reply.dart index 219540eb..adfba431 100644 --- a/lib/common/skeleton/video_reply.dart +++ b/lib/common/skeleton/video_reply.dart @@ -71,12 +71,6 @@ class VideoReplySkeleton extends StatelessWidget { ], ), ), - Divider( - height: 1, - indent: 52, - endIndent: 10, - color: Theme.of(context).dividerColor.withOpacity(0.08), - ) ], ), ); diff --git a/lib/common/widgets/http_error.dart b/lib/common/widgets/http_error.dart index b3aa348d..607fffe0 100644 --- a/lib/common/widgets/http_error.dart +++ b/lib/common/widgets/http_error.dart @@ -17,6 +17,7 @@ class HttpError extends StatelessWidget { children: [ Text( errMsg, + textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 10), diff --git a/lib/common/widgets/stat/danmu.dart b/lib/common/widgets/stat/danmu.dart index 44f63b21..d8deb3e2 100644 --- a/lib/common/widgets/stat/danmu.dart +++ b/lib/common/widgets/stat/danmu.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/utils/utils.dart'; @@ -11,21 +12,21 @@ class StatDanMu extends StatelessWidget { @override Widget build(BuildContext context) { + Color color = + theme == 'white' ? Colors.white : Theme.of(context).colorScheme.outline; return Row( children: [ - Image.asset( - 'assets/images/dm_$theme.png', - width: size == 'medium' ? 16 : 14, - height: size == 'medium' ? 16 : 14, + Icon( + CupertinoIcons.ellipses_bubble, + size: 14, + color: color, ), - const SizedBox(width: 2), + const SizedBox(width: 3), Text( Utils.numFormat(danmu!), style: TextStyle( fontSize: size == 'medium' ? 12 : 11, - color: theme == 'white' - ? Colors.white - : Theme.of(context).colorScheme.outline, + color: color, ), ) ], diff --git a/lib/common/widgets/stat/up.dart b/lib/common/widgets/stat/up.dart new file mode 100644 index 00000000..d217e2c4 --- /dev/null +++ b/lib/common/widgets/stat/up.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class UpTag extends StatelessWidget { + const UpTag({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: 14, + height: 10, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + border: Border.all(color: Theme.of(context).colorScheme.outline)), + margin: const EdgeInsets.only(right: 4), + child: Center( + child: Text( + 'UP', + style: TextStyle( + fontSize: 6, color: Theme.of(context).colorScheme.outline), + ), + ), + ); + } +} diff --git a/lib/common/widgets/stat/view.dart b/lib/common/widgets/stat/view.dart index 6f6d1960..302ceee6 100644 --- a/lib/common/widgets/stat/view.dart +++ b/lib/common/widgets/stat/view.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/utils/utils.dart'; @@ -6,26 +7,26 @@ class StatView extends StatelessWidget { final int? view; final String? size; - const StatView({Key? key, this.theme, this.view, this.size}) : super(key: key); + const StatView({Key? key, this.theme, this.view, this.size}) + : super(key: key); @override Widget build(BuildContext context) { + Color color = + theme == 'white' ? Colors.white : Theme.of(context).colorScheme.outline; return Row( children: [ - Image.asset( - 'assets/images/view_$theme.png', - width: size == 'medium' ? 16 : 14, - height: size == 'medium' ? 16 : 14, + Icon( + CupertinoIcons.play_rectangle, + size: 13, + color: color, ), - const SizedBox(width: 2), + const SizedBox(width: 3), Text( Utils.numFormat(view!), - // videoItem['stat']['view'].toString(), style: TextStyle( fontSize: size == 'medium' ? 12 : 11, - color: theme == 'white' - ? Colors.white - : Theme.of(context).colorScheme.outline, + color: color, ), ), ], diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index 6e881831..fd3c289c 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/stat/up.dart'; import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; @@ -137,11 +138,12 @@ class VideoContent extends StatelessWidget { const SizedBox(height: 4), Row( children: [ - Image.asset( - 'assets/images/up_gray.png', - width: 14, - height: 12, - ), + // Image.asset( + // 'assets/images/up_gray.png', + // width: 14, + // height: 12, + // ), + const UpTag(), const SizedBox(width: 2), Text( videoItem.owner.name, diff --git a/lib/http/init.dart b/lib/http/init.dart index 0ac94f85..b5008a60 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -111,7 +111,7 @@ class Request { return response; } on DioError catch (e) { print('get error: $e'); - return Future.error(ApiInterceptor.dioError(e)); + return Future.error(await ApiInterceptor.dioError(e)); } } @@ -132,7 +132,7 @@ class Request { return response; } on DioError catch (e) { print('post error: $e'); - return Future.error(ApiInterceptor.dioError(e)); + return Future.error(await ApiInterceptor.dioError(e)); } } diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart index d7140b00..8ee8c8a0 100644 --- a/lib/http/interceptor.dart +++ b/lib/http/interceptor.dart @@ -1,10 +1,12 @@ import 'package:dio/dio.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart' hide Response; class ApiInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { - // print("请求之前"); + print("请求之前"); // 在请求之前添加头部或认证信息 // options.headers['Authorization'] = 'Bearer token'; // options.headers['Content-Type'] = 'application/json'; @@ -13,15 +15,14 @@ class ApiInterceptor extends Interceptor { @override void onResponse(Response response, ResponseInterceptorHandler handler) { - // print("响应之前"); handler.next(response); } @override - void onError(DioError err, ErrorInterceptorHandler handler) { + void onError(DioError err, ErrorInterceptorHandler handler) async { // 处理网络请求错误 - - handler.next(err); + // handler.next(err); + SmartDialog.showToast(await dioError(err)); super.onError(err, handler); } @@ -43,7 +44,7 @@ class ApiInterceptor extends Interceptor { return "发送请求超时,请检查网络设置"; case DioErrorType.unknown: var res = await checkConect(); - return "$res 网络异常,请稍后重试!"; + return res + " \n 网络异常,请稍后重试!"; default: return "Dio异常"; } diff --git a/lib/http/user.dart b/lib/http/user.dart index f7f19a55..77964f08 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -1,5 +1,6 @@ import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; +import 'package:pilipala/models/user/info.dart'; class UserHttp { static Future userStat({required int mid}) async { @@ -14,7 +15,8 @@ class UserHttp { static Future userInfo() async { var res = await Request().get(Api.userInfo); if (res.data['code'] == 0) { - return {'status': true, 'data': res.data['data']}; + UserInfoData data = UserInfoData.fromJson(res.data['data']); + return {'status': true, 'data': data}; } else { return {'status': false}; } diff --git a/lib/http/video.dart b/lib/http/video.dart index 61f2770e..686bbc2d 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -11,39 +11,47 @@ import 'package:pilipala/models/video_detail_res.dart'; class VideoHttp { // 首页推荐视频 static Future rcmdVideoList({required int ps, required int freshIdx}) async { - var res = await Request().get( - Api.recommendList, - data: { - 'feed_version': 'V3', - 'ps': ps, - 'fresh_idx': freshIdx, - }, - ); - if (res.data['code'] == 0) { - List list = []; - for (var i in res.data['data']['item']) { - list.add(RecVideoItemModel.fromJson(i)); + try { + var res = await Request().get( + Api.recommendList, + data: { + 'feed_version': 'V3', + 'ps': ps, + 'fresh_idx': freshIdx, + }, + ); + if (res.data['code'] == 0) { + List list = []; + for (var i in res.data['data']['item']) { + list.add(RecVideoItemModel.fromJson(i)); + } + return {'status': true, 'data': list}; + } else { + return {'status': false, 'data': []}; } - return {'status': true, 'data': list}; - } else { - return {'status': false, 'data': []}; + } catch (err) { + return {'status': false, 'data': [], 'msg': err}; } } // 最热视频 static Future hotVideoList({required int pn, required int ps}) async { - var res = await Request().get( - Api.hotList, - data: {'pn': pn, 'ps': ps}, - ); - if (res.data['code'] == 0) { - List list = []; - for (var i in res.data['data']['list']) { - list.add(HotVideoItemModel.fromJson(i)); + try { + var res = await Request().get( + Api.hotList, + data: {'pn': pn, 'ps': ps}, + ); + if (res.data['code'] == 0) { + List list = []; + for (var i in res.data['data']['list']) { + list.add(HotVideoItemModel.fromJson(i)); + } + return {'status': true, 'data': list}; + } else { + return {'status': false, 'data': []}; } - return {'status': true, 'data': list}; - } else { - return {'status': false, 'data': []}; + } catch (err) { + return {'status': false, 'data': [], 'msg': err}; } } diff --git a/lib/models/user/info.dart b/lib/models/user/info.dart index 9fa34bb7..ce8b641c 100644 --- a/lib/models/user/info.dart +++ b/lib/models/user/info.dart @@ -1,7 +1,80 @@ class UserInfoData { UserInfoData({ this.isLogin, + this.emailVerified, + this.face, + this.levelInfo, + this.mid, + this.mobileVerified, + this.money, + this.moral, + this.official, + this.officialVerify, + this.pendant, + this.scores, + this.uname, + this.vipDueDate, + this.vipStatus, + this.vipType, + this.vipPayType, + this.vipThemeType, + this.vipLabel, + this.vipAvatarSub, + this.vipNicknameColor, + this.wallet, + this.hasShop, + this.shopUrl, }); bool? isLogin; + int? emailVerified; + String? face; + Map? levelInfo; + int? mid; + int? mobileVerified; + int? money; + int? moral; + Map? official; + Map? officialVerify; + Map? pendant; + int? scores; + String? uname; + int? vipDueDate; + int? vipStatus; + int? vipType; + int? vipPayType; + int? vipThemeType; + Map? vipLabel; + int? vipAvatarSub; + String? vipNicknameColor; + Map? wallet; + bool? hasShop; + String? shopUrl; + + UserInfoData.fromJson(Map json) { + isLogin = json['isLogin'] ?? false; + emailVerified = json['email_verified']; + face = json['face']; + levelInfo = json['level_info']; + mid = json['mid']; + mobileVerified = json['mobile_verified']; + money = json['money']; + moral = json['moral']; + official = json['official']; + officialVerify = json['officialVerify']; + pendant = json['pendant']; + scores = json['scores']; + uname = json['uname']; + vipDueDate = json['vipDueDate']; + vipStatus = json['vipStatus']; + vipType = json['vipType']; + vipPayType = json['vip_pay_type']; + vipThemeType = json['vip_theme_type']; + vipLabel = json['vip_label']; + vipAvatarSub = json['vip_avatar_subscript']; + vipNicknameColor = json['vip_nickname_color']; + wallet = json['wallet']; + hasShop = json['has_shop']; + shopUrl = json['shop_url']; + } } diff --git a/lib/models/video/reply/item.dart b/lib/models/video/reply/item.dart index c6838a13..a1e96561 100644 --- a/lib/models/video/reply/item.dart +++ b/lib/models/video/reply/item.dart @@ -149,6 +149,6 @@ class ReplyControl { entryText = json['sub_reply_entry_text']; titleText = json['sub_reply_title_text']; time = json['time_desc']; - location = json['location'] ?? ''; + location = json['location'] != null ? json['location'].split(':')[1] : ''; } } diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index e96d8b60..7182b134 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -15,7 +15,7 @@ class HomeController extends GetxController { @override void onInit() { super.onInit(); - queryRcmdFeed('init'); + // queryRcmdFeed('init'); } // 获取推荐 @@ -35,6 +35,7 @@ class HomeController extends GetxController { _currentPage += 1; } isLoadingMore = false; + return res; } // 下拉刷新 diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 1adb482b..0d08ed3e 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_card_v.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/video_card_v.dart'; import './controller.dart'; import 'package:pilipala/common/constants.dart'; @@ -16,6 +17,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State with AutomaticKeepAliveClientMixin { final HomeController _homeController = Get.put(HomeController()); + Future? _futureBuilderFuture; List videoList = []; @override @@ -24,6 +26,7 @@ class _HomePageState extends State @override void initState() { super.initState(); + _futureBuilderFuture = _homeController.queryRcmdFeed('init'); _homeController.videoList.listen((value) { videoList = value; setState(() {}); @@ -69,26 +72,25 @@ class _HomePageState extends State ? EdgeInsets.zero : const EdgeInsets.fromLTRB( StyleString.cardSpace, 0, StyleString.cardSpace, 8), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - // 行间距 - mainAxisSpacing: StyleString.cardSpace, - // 列间距 - crossAxisSpacing: StyleString.cardSpace, - // 列数 - crossAxisCount: _homeController.crossAxisCount, - mainAxisExtent: MediaQuery.of(context).size.width / - _homeController.crossAxisCount / - StyleString.aspectRatio + - 70), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return videoList.isNotEmpty - ? VideoCardV(videoItem: videoList[index]) - : const VideoCardVSkeleton(); - }, - childCount: videoList.isNotEmpty ? videoList.length : 10, - ), + sliver: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx(() => contentGrid( + _homeController, _homeController.videoList)); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return contentGrid(_homeController, []); + } + }, ), ), const LoadingMore() @@ -98,6 +100,31 @@ class _HomePageState extends State // ), ); } + + Widget contentGrid(ctr, videoList) { + return SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + // 行间距 + mainAxisSpacing: StyleString.cardSpace, + // 列间距 + crossAxisSpacing: StyleString.cardSpace, + // 列数 + crossAxisCount: ctr.crossAxisCount, + mainAxisExtent: MediaQuery.of(context).size.width / + ctr.crossAxisCount / + StyleString.aspectRatio + + 70, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return videoList!.isNotEmpty + ? VideoCardV(videoItem: videoList![index]) + : const VideoCardVSkeleton(); + }, + childCount: videoList!.isNotEmpty ? videoList!.length : 10, + ), + ); + } } class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { diff --git a/lib/pages/home/widgets/app_bar.dart b/lib/pages/home/widgets/app_bar.dart index cd070d1d..f0b142e0 100644 --- a/lib/pages/home/widgets/app_bar.dart +++ b/lib/pages/home/widgets/app_bar.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -10,11 +11,7 @@ class HomeAppBar extends StatelessWidget { return SliverAppBar( // forceElevated: true, scrolledUnderElevation: 0, - toolbarHeight: Platform.isAndroid - ? (MediaQuery.of(context).padding.top + 6) - : Platform.isIOS - ? MediaQuery.of(context).padding.top - 2 - : kToolbarHeight, + toolbarHeight: MediaQuery.of(context).padding.top, expandedHeight: kToolbarHeight + MediaQuery.of(context).padding.top, automaticallyImplyLeading: false, pinned: true, @@ -39,18 +36,12 @@ class HomeAppBar extends StatelessWidget { actions: [ IconButton( onPressed: () {}, - icon: const FaIcon( - FontAwesomeIcons.magnifyingGlass, - size: 18, - ), - ), - IconButton( - onPressed: () {}, - icon: const FaIcon( - FontAwesomeIcons.envelope, - size: 20, - ), + icon: const Icon(CupertinoIcons.search, size: 22), ), + // IconButton( + // onPressed: () {}, + // icon: const Icon(CupertinoIcons.bell, size: 22), + // ), const SizedBox(width: 10) ], elevation: 0, diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index 3bddd37c..51abee1e 100644 --- a/lib/pages/hot/view.dart +++ b/lib/pages/hot/view.dart @@ -24,7 +24,7 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { @override void initState() { super.initState(); - _futureBuilderFuture = _hotController!.queryHotFeed('init'); + _futureBuilderFuture = _hotController.queryHotFeed('init'); _hotController.scrollController.addListener( () { if (_hotController.scrollController.position.pixels >= diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index f7ef9e49..2206af59 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/pages/home/view.dart'; @@ -12,18 +13,42 @@ class MainController extends GetxController { ]; List navigationBars = [ { - 'icon': const Icon(Icons.home_outlined), - 'selectedIcon': const Icon(Icons.home), + // 'icon': const Icon(Icons.home_outlined), + // 'selectedIcon': const Icon(Icons.home), + 'icon': const Icon( + CupertinoIcons.house, + size: 18, + ), + 'selectedIcon': const Icon( + CupertinoIcons.house_fill, + size: 18, + ), 'label': "推荐", }, { - 'icon': const Icon(Icons.whatshot_outlined), - 'selectedIcon': const Icon(Icons.whatshot_rounded), + // 'icon': const Icon(Icons.whatshot_outlined), + // 'selectedIcon': const Icon(Icons.whatshot_rounded), + 'icon': const Icon( + CupertinoIcons.flame, + size: 20, + ), + 'selectedIcon': const Icon( + CupertinoIcons.flame_fill, + size: 20, + ), 'label': "热门", }, { - 'icon': const Icon(Icons.person_outline), - 'selectedIcon': const Icon(Icons.person), + // 'icon': const Icon(Icons.person_outline), + // 'selectedIcon': const Icon(Icons.person), + 'icon': const Icon( + CupertinoIcons.person, + size: 21, + ), + 'selectedIcon': const Icon( + CupertinoIcons.person_fill, + size: 21, + ), 'label': "我的", } ]; diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index 9e867c78..5c560ebc 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -1,7 +1,10 @@ import 'package:get/get.dart'; import 'package:pilipala/http/user.dart'; +import 'package:pilipala/models/user/info.dart'; class MineController extends GetxController { + UserInfoData? userInfo; + @override void onInit() { super.onInit(); diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index 10c7ab36..54796281 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -22,13 +23,19 @@ class _MinePageState extends State { actions: [ IconButton( onPressed: () {}, - icon: const Icon(Icons.light_mode_rounded), + icon: const Icon( + CupertinoIcons.moon, + size: 22, + ), + // icon: const Icon( + // CupertinoIcons.sun_max, + // size: 22, + // ), ), IconButton( onPressed: () {}, - icon: const FaIcon( - FontAwesomeIcons.sliders, - size: 18, + icon: const Icon( + CupertinoIcons.slider_horizontal_3, ), ), const SizedBox(width: 10), @@ -36,161 +43,183 @@ class _MinePageState extends State { ), body: RefreshIndicator( onRefresh: () async { - await Future.delayed(Duration(seconds: 2)); + await Future.delayed(const Duration(seconds: 2)); }, - child: Column( - children: [ - InkWell( - onTap: () { - Get.toNamed( - '/webview', - parameters: { - 'url': - 'https://passport.bilibili.com/h5-app/passport/login', - 'type': 'login', - 'pageTitle': '登录bilibili', - }, - ); - }, - child: Padding( - padding: const EdgeInsets.only(top: 10, bottom: 10), - child: Row( + child: LayoutBuilder( + builder: (context, constraint) { + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: constraint.maxHeight, + child: Column( children: [ - const SizedBox(width: 20), - ClipOval( - child: Container( - width: 75, - height: 75, - color: Theme.of(context).colorScheme.onInverseSurface, - child: Center( - child: Image.asset('assets/images/loading.png'), + InkWell( + onTap: () { + Get.toNamed( + '/webview', + parameters: { + 'url': + 'https://passport.bilibili.com/h5-app/passport/login', + 'type': 'login', + 'pageTitle': '登录bilibili', + }, + ); + }, + child: Padding( + padding: const EdgeInsets.only(top: 10, bottom: 10), + child: Row( + children: [ + const SizedBox(width: 20), + ClipOval( + child: Container( + width: 75, + height: 75, + color: Theme.of(context) + .colorScheme + .onInverseSurface, + child: Center( + child: + Image.asset('assets/images/loading.png'), + ), + ), + ), + const SizedBox(width: 14), + Text( + '点击登录', + style: Theme.of(context).textTheme.titleMedium, + ), + ], ), ), ), - const SizedBox(width: 14), - Text( - '点击登录', - style: Theme.of(context).textTheme.titleMedium, + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.only(left: 12, right: 12), + child: LayoutBuilder( + builder: (context, constraints) { + TextStyle style = TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleMedium! + .fontSize, + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold); + return SizedBox( + height: constraints.maxWidth / 3 * 0.6, + child: GridView.count( + primary: false, + padding: const EdgeInsets.all(0), + crossAxisCount: 3, + childAspectRatio: 1.67, + children: [ + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('-', style: style), + const SizedBox(height: 8), + Text( + '动态', + style: Theme.of(context) + .textTheme + .labelMedium, + ), + ], + ), + ), + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '50', + style: style, + ), + const SizedBox(height: 8), + Text( + '关注', + style: Theme.of(context) + .textTheme + .labelMedium, + ), + ], + ), + ), + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '-', + style: style, + ), + const SizedBox(height: 8), + Text( + '粉丝', + style: Theme.of(context) + .textTheme + .labelMedium, + ), + ], + ), + ), + ], + ), + ); + }, + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.only(left: 12, right: 12), + child: LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + height: constraints.maxWidth / 4 * 0.8, + child: GridView.count( + primary: false, + padding: const EdgeInsets.all(0), + crossAxisCount: 4, + childAspectRatio: 1.25, + children: [ + ActionItem( + icon: + const Icon(CupertinoIcons.cloud_download), + onTap: () => {}, + text: '离线缓存', + ), + ActionItem( + icon: const Icon(CupertinoIcons.time), + onTap: () => {}, + text: '历史记录', + ), + ActionItem( + icon: const Icon(CupertinoIcons.star), + onTap: () => {}, + text: '我的收藏', + ), + ActionItem( + icon: const Icon(CupertinoIcons.film), + onTap: () => {}, + text: '稍后再看', + ), + ], + ), + ); + }, + ), ), ], ), ), - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.only(left: 12, right: 12), - child: LayoutBuilder( - builder: (context, constraints) { - TextStyle style = TextStyle( - fontSize: - Theme.of(context).textTheme.titleMedium!.fontSize, - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold); - return SizedBox( - height: constraints.maxWidth / 3 * 0.6, - child: GridView.count( - primary: false, - padding: const EdgeInsets.all(0), - crossAxisCount: 3, - childAspectRatio: 1.67, - children: [ - InkWell( - onTap: () {}, - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('-', style: style), - const SizedBox(height: 8), - Text( - '动态', - style: Theme.of(context).textTheme.labelMedium, - ), - ], - ), - ), - InkWell( - onTap: () {}, - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '50', - style: style, - ), - const SizedBox(height: 8), - Text( - '关注', - style: Theme.of(context).textTheme.labelMedium, - ), - ], - ), - ), - InkWell( - onTap: () {}, - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '-', - style: style, - ), - const SizedBox(height: 8), - Text( - '粉丝', - style: Theme.of(context).textTheme.labelMedium, - ), - ], - ), - ), - ], - ), - ); - }, - ), - ), - const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.only(left: 12, right: 12), - child: LayoutBuilder( - builder: (context, constraints) { - return SizedBox( - height: constraints.maxWidth / 4 * 0.8, - child: GridView.count( - primary: false, - padding: const EdgeInsets.all(0), - crossAxisCount: 4, - childAspectRatio: 1.25, - children: [ - ActionItem( - icon: const Icon(FontAwesomeIcons.download), - onTap: () => {}, - text: '离线缓存', - ), - ActionItem( - icon: const Icon(FontAwesomeIcons.clockRotateLeft), - onTap: () => {}, - text: '历史记录', - ), - ActionItem( - icon: const Icon(FontAwesomeIcons.star), - onTap: () => {}, - text: '我的收藏', - ), - ActionItem( - icon: const Icon(FontAwesomeIcons.film), - onTap: () => {}, - text: '稍后再看', - ), - ], - ), - ); - }, - ), - ), - ], + ); + }, ), ), ); @@ -217,10 +246,7 @@ class ActionItem extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - icon!.icon!, - color: Theme.of(context).colorScheme.outline, - ), + Icon(icon!.icon!), const SizedBox(height: 8), Text( text!, diff --git a/lib/pages/preview/controller.dart b/lib/pages/preview/controller.dart index f2af8b9c..fd0d4472 100644 --- a/lib/pages/preview/controller.dart +++ b/lib/pages/preview/controller.dart @@ -69,9 +69,4 @@ class PreviewController extends GetxController { File(path).writeAsBytesSync(response.data); Share.shareXFiles([XFile(path)], subject: imgList[initialPage.value]); } - - // 浏览器中查看 - void onBrowserImg() async { - Utils.openURL(imgList[initialPage.value]); - } } diff --git a/lib/pages/preview/view.dart b/lib/pages/preview/view.dart index d82cd45e..2ff9bd73 100644 --- a/lib/pages/preview/view.dart +++ b/lib/pages/preview/view.dart @@ -74,11 +74,6 @@ class _ImagePreviewState extends State onTap: _previewController.onSaveImg, child: const Text('保存'), ), - PopupMenuItem( - value: 'browser', - onTap: _previewController.onBrowserImg, - child: const Text('浏览器中查看'), - ), ], ), ], diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 623a2684..4bef011b 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; @@ -246,8 +247,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { child: Row( children: const [ Icon( - FontAwesomeIcons.lemon, - size: 15, + CupertinoIcons.plus, + size: 16, ), SizedBox(width: 4), Text('关注'), @@ -383,7 +384,7 @@ class ActionItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon!.icon!, - size: 20, + size: 21, color: selectStatus ? Theme.of(context).primaryColor : Theme.of(context).colorScheme.outline), diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 8f0e3cb6..f920da95 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -18,15 +18,15 @@ class ReplyItem extends StatelessWidget { child: Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB(12, 8, 8, 2), + padding: const EdgeInsets.fromLTRB(12, 6, 8, 0), child: content(context), ), - Divider( - height: 1, - indent: 52, - endIndent: 10, - color: Theme.of(context).dividerColor.withOpacity(0.08), - ) + // Divider( + // height: 1, + // indent: 52, + // endIndent: 10, + // color: Theme.of(context).dividerColor.withOpacity(0.08), + // ) ], ), ); @@ -154,7 +154,7 @@ class ReplyItem extends StatelessWidget { ), // title Container( - margin: const EdgeInsets.only(top: 0, left: 45, right: 6), + margin: const EdgeInsets.only(top: 0, left: 45, right: 6, bottom: 6), child: SelectableRegion( magnifierConfiguration: const TextMagnifierConfiguration(), focusNode: FocusNode(), @@ -201,7 +201,7 @@ class ReplyItem extends StatelessWidget { .copyWith(color: Theme.of(context).colorScheme.outline), ), if (replyItem!.replyControl != null && - replyItem!.replyControl!.location != null) + replyItem!.replyControl!.location != '') Text( ' • ${replyItem!.replyControl!.location!}', style: Theme.of(context) @@ -315,10 +315,6 @@ class ReplyItemRow extends StatelessWidget { maxLines: extraRow == 1 ? 2 : null, TextSpan( children: [ - if (replies![index].isUp) - WidgetSpan( - child: UpTag(), - ), TextSpan( text: replies![index].member.uname + ' ', style: TextStyle( @@ -333,6 +329,10 @@ class ReplyItemRow extends StatelessWidget { print('跳转至用户主页'), }, ), + if (replies![index].isUp) + WidgetSpan( + child: UpTag(), + ), buildContent(context, replies![index].content), ], ), @@ -352,13 +352,14 @@ class ReplyItemRow extends StatelessWidget { useRootNavigator: true, isScrollControlled: true, Container( - height: Get.size.height - Get.size.width * 9 / 16 - 50, + height: Get.size.height - Get.size.width * 9 / 16 - 45, color: Theme.of(context).colorScheme.background, child: Column( children: [ AppBar( automaticallyImplyLeading: false, centerTitle: false, + elevation: 1, title: Text( '评论详情', style: Theme.of(context).textTheme.titleMedium, @@ -367,7 +368,6 @@ class ReplyItemRow extends StatelessWidget { IconButton( icon: const Icon(Icons.close), onPressed: () async { - await Future.delayed(const Duration(milliseconds: 200)); Get.back(); }, ) @@ -546,7 +546,7 @@ InlineSpan buildContent(BuildContext context, content) { arguments: {'initialPage': 0, 'imgList': picList}); }, child: Padding( - padding: EdgeInsets.only(top: 4), + padding: const EdgeInsets.only(top: 4), child: NetworkImgLayer( src: pictureItem['img_src'], width: box.maxWidth / 2, @@ -634,8 +634,8 @@ class UpTag extends StatelessWidget { Widget build(BuildContext context) { Color primary = Theme.of(context).colorScheme.primary; return Container( - width: tagText == 'UP' ? 25 : 32, - height: tagText == 'UP' ? 16 : 18, + width: 24, + height: 15, decoration: BoxDecoration( borderRadius: BorderRadius.circular(3), color: tagText == 'UP' ? primary : null, diff --git a/lib/pages/webview/controller.dart b/lib/pages/webview/controller.dart index 593dd53f..b8d591cc 100644 --- a/lib/pages/webview/controller.dart +++ b/lib/pages/webview/controller.dart @@ -2,6 +2,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/user.dart'; +import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/utils/cookie.dart'; import 'package:webview_cookie_manager/webview_cookie_manager.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -45,10 +46,10 @@ class WebviewController extends GetxController { await SetCookie.onSet(cookies, HttpString.baseUrl); await SetCookie.onSet(apiCookies, HttpString.baseApiUrl); var result = await UserHttp.userInfo(); - bool isLogin = result['data']['isLogin']; - if (isLogin) { + if (result['status'] && result['data'].isLogin) { SmartDialog.showToast('登录成功'); - Get.back(); + Get.find().userInfo = result['data']; + // Get.back(); } } catch (e) { print(e); diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 03d30181..084a6e6f 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,13 +1,10 @@ // 工具函数 import 'dart:io'; import 'dart:math'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:get/get_utils/get_utils.dart'; import 'package:path_provider/path_provider.dart'; class Utils { - final ChromeSafariBrowser browser = ChromeSafariBrowser(); - static Future getCookiePath() async { Directory tempDir = await getApplicationSupportDirectory(); String tempPath = "${tempDir.path}/.plpl/"; @@ -138,22 +135,4 @@ class Utils { static String makeHeroTag(v) { return v.toString() + Random().nextInt(9999).toString(); } - - static openURL(aUrl) async { - try { - await Utils().browser.open( - url: Uri.parse(aUrl), - options: ChromeSafariBrowserClassOptions( - android: AndroidChromeCustomTabsOptions( - shareState: CustomTabsShareState.SHARE_STATE_OFF, - isSingleInstance: false, - isTrustedWebActivity: false, - keepAliveEnabled: true, - ), - ), - ); - } catch (err) { - await InAppBrowser.openWithSystemBrowser(url: Uri.parse(aUrl)); - } - } } diff --git a/pubspec.lock b/pubspec.lock index 78a4654c..45514cc2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -246,14 +246,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.0" - flutter_inappwebview: - dependency: "direct main" - description: - name: flutter_inappwebview - sha256: "1c370ac07de80a579a0047c94c5bb586128d4ef50c0f3f501d6e77010374a319" - url: "https://pub.dev" - source: hosted - version: "5.4.4" flutter_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7374bbf5..63778e94 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,7 +62,6 @@ dependencies: share_plus: ^6.3.1 # webView url_launcher: ^6.1.9 - flutter_inappwebview: 5.4.4 webview_cookie_manager: ^2.0.6 webview_flutter: ^4.2.0 From e612e60361c3431a6577bb74a58ec6efd25e287f Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 10 May 2023 00:35:24 +0800 Subject: [PATCH 23/30] =?UTF-8?q?mod:=20=E7=94=A8=E6=88=B7=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E6=B8=B2=E6=9F=93=E3=80=81=E9=80=80=E5=87=BA=E7=99=BB?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 3 + lib/http/init.dart | 19 +- lib/http/user.dart | 11 + lib/main.dart | 2 + lib/models/user/info.dart | 27 +- lib/models/user/stat.dart | 17 ++ lib/pages/main/controller.dart | 35 ++- lib/pages/main/view.dart | 26 +- lib/pages/mine/controller.dart | 62 ++++- lib/pages/mine/view.dart | 398 ++++++++++++++++++++---------- lib/pages/setting/controller.dart | 23 ++ lib/pages/setting/index.dart | 4 + lib/pages/setting/view.dart | 36 +++ lib/pages/webview/controller.dart | 9 +- lib/router/app_pages.dart | 5 +- lib/utils/cookie.dart | 1 - lib/utils/storage.dart | 24 ++ pubspec.lock | 128 ++++++++++ pubspec.yaml | 3 + 19 files changed, 674 insertions(+), 159 deletions(-) create mode 100644 lib/models/user/stat.dart create mode 100644 lib/pages/setting/controller.dart create mode 100644 lib/pages/setting/index.dart create mode 100644 lib/pages/setting/view.dart create mode 100644 lib/utils/storage.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 62e7047c..32abb1b6 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -23,4 +23,7 @@ class Api { // 获取用户信息 static const String userInfo = '/x/web-interface/nav'; + + // 获取当前用户状态 + static const String userStatOwner = '/x/web-interface/nav/stat'; } diff --git a/lib/http/init.dart b/lib/http/init.dart index b5008a60..87ab8c59 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -12,6 +12,7 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart'; class Request { static final Request _instance = Request._internal(); + static late CookieManager cookieManager; factory Request() => _instance; @@ -31,11 +32,9 @@ class Request { ignoreExpires: true, storage: FileStorage(cookiePath), ); - - dio.interceptors.add(CookieManager(cookieJar)); - - var cookie = await CookieManager(cookieJar) - .cookieJar + cookieManager = CookieManager(cookieJar); + dio.interceptors.add(cookieManager); + var cookie = await cookieManager.cookieJar .loadForRequest(Uri.parse(HttpString.baseUrl)); if (cookie.isEmpty) { try { @@ -46,6 +45,16 @@ class Request { } } + // 移除cookie + static removeCookie() async { + await cookieManager.cookieJar + .saveFromResponse(Uri.parse(HttpString.baseUrl), []); + await cookieManager.cookieJar + .saveFromResponse(Uri.parse(HttpString.baseApiUrl), []); + cookieManager.cookieJar.deleteAll(); + dio.interceptors.add(cookieManager); + } + /* * config it and create */ diff --git a/lib/http/user.dart b/lib/http/user.dart index 77964f08..8f8881ab 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -1,6 +1,7 @@ import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/models/user/info.dart'; +import 'package:pilipala/models/user/stat.dart'; class UserHttp { static Future userStat({required int mid}) async { @@ -17,6 +18,16 @@ class UserHttp { if (res.data['code'] == 0) { UserInfoData data = UserInfoData.fromJson(res.data['data']); return {'status': true, 'data': data}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + static Future userStatOwner() async { + var res = await Request().get(Api.userStatOwner); + if (res.data['code'] == 0) { + UserStat data = UserStat.fromJson(res.data['data']); + return {'status': true, 'data': data}; } else { return {'status': false}; } diff --git a/lib/main.dart b/lib/main.dart index f572a7a7..f853f4d7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,9 +5,11 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/router/app_pages.dart'; import 'package:pilipala/pages/main/view.dart'; +import 'package:pilipala/utils/storage.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await GStrorage.init(); await Request.setCookie(); runApp(const MyApp()); } diff --git a/lib/models/user/info.dart b/lib/models/user/info.dart index ce8b641c..b51c68f0 100644 --- a/lib/models/user/info.dart +++ b/lib/models/user/info.dart @@ -29,7 +29,7 @@ class UserInfoData { bool? isLogin; int? emailVerified; String? face; - Map? levelInfo; + LevelInfo? levelInfo; int? mid; int? mobileVerified; int? money; @@ -55,7 +55,9 @@ class UserInfoData { isLogin = json['isLogin'] ?? false; emailVerified = json['email_verified']; face = json['face']; - levelInfo = json['level_info']; + levelInfo = json['level_info'] != null + ? LevelInfo.fromJson(json['level_info']) + : LevelInfo(); mid = json['mid']; mobileVerified = json['mobile_verified']; money = json['money']; @@ -78,3 +80,24 @@ class UserInfoData { shopUrl = json['shop_url']; } } + +class LevelInfo { + LevelInfo({ + this.currentLevel, + this.currentMin, + this.currentExp, + this.nextExp, + }); + + int? currentLevel; + int? currentMin; + int? currentExp; + int? nextExp; + + LevelInfo.fromJson(Map json) { + currentLevel = json['current_level']; + currentMin = json['current_min']; + currentExp = json['current_exp']; + nextExp = json['next_exp']; + } +} diff --git a/lib/models/user/stat.dart b/lib/models/user/stat.dart new file mode 100644 index 00000000..0b56a499 --- /dev/null +++ b/lib/models/user/stat.dart @@ -0,0 +1,17 @@ +class UserStat { + UserStat({ + this.following, + this.follower, + this.dynamicCount, + }); + + int? following; + int? follower; + int? dynamicCount; + + UserStat.fromJson(Map json) { + following = json['following']; + follower = json['follower']; + dynamicCount = json['dynamic_count']; + } +} diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 2206af59..752ff713 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -1,9 +1,12 @@ import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/pages/home/view.dart'; import 'package:pilipala/pages/hot/view.dart'; import 'package:pilipala/pages/mine/view.dart'; +import 'package:pilipala/utils/storage.dart'; class MainController extends GetxController { List pages = [ @@ -11,7 +14,7 @@ class MainController extends GetxController { const HotPage(), const MinePage(), ]; - List navigationBars = [ + RxList navigationBars = [ { // 'icon': const Icon(Icons.home_outlined), // 'selectedIcon': const Icon(Icons.home), @@ -51,5 +54,33 @@ class MainController extends GetxController { ), 'label': "我的", } - ]; + ].obs; + + @override + void onInit() { + super.onInit(); + readuUserFace(); + } + + // 设置头像 + readuUserFace() async { + Box user = GStrorage.user; + if (user.get(UserBoxKey.userFace) != null) { + navigationBars.last['icon'] = + navigationBars.last['selectedIcon'] = NetworkImgLayer( + width: 25, + height: 25, + type: 'avatar', + src: user.get(UserBoxKey.userFace), + ); + navigationBars.last['label'] = '我'; + } + } + + // 重置 + resetLast() { + navigationBars.last['icon'] = const Icon(Icons.person_outline); + navigationBars.last['selectedIcon'] = const Icon(Icons.person); + navigationBars.last['label'] = '我的'; + } } diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index e1503de8..bce9bbc2 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -20,7 +20,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { late AnimationController? _animationController; late Animation? _fadeAnimation; late Animation? _slideAnimation; - int selectedIndex = 0; + int selectedIndex = 2; int? _lastSelectTime; //上次点击时间 @override @@ -111,17 +111,19 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { ), ), ), - bottomNavigationBar: NavigationBar( - elevation: 1, - destinations: _mainController.navigationBars.map((e) { - return NavigationDestination( - icon: e['icon'], - selectedIcon: e['selectedIcon'], - label: e['label'], - ); - }).toList(), - selectedIndex: selectedIndex, - onDestinationSelected: (value) => setIndex(value), + bottomNavigationBar: Obx( + () => NavigationBar( + elevation: 1, + destinations: _mainController.navigationBars.map((e) { + return NavigationDestination( + icon: e['icon'], + selectedIcon: e['selectedIcon'], + label: e['label'], + ); + }).toList(), + selectedIndex: selectedIndex, + onDestinationSelected: (value) => setIndex(value), + ), ), ); } diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index 5c560ebc..62a62438 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -1,17 +1,69 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/models/user/info.dart'; +import 'package:pilipala/models/user/stat.dart'; +import 'package:pilipala/pages/main/controller.dart'; +import 'package:pilipala/utils/storage.dart'; class MineController extends GetxController { - UserInfoData? userInfo; + // 用户信息 头像、昵称、lv + Rx userInfo = UserInfoData().obs; + // 用户状态 动态、关注、粉丝 + Rx userStat = UserStat().obs; + Box user = GStrorage.user; + RxBool userLogin = false.obs; - @override - void onInit() { - super.onInit(); - // queryUserInfo(); + onLogin() { + Get.toNamed( + '/webview', + parameters: { + 'url': 'https://passport.bilibili.com/h5-app/passport/login', + 'type': 'login', + 'pageTitle': '登录bilibili', + }, + ); } Future queryUserInfo() async { var res = await UserHttp.userInfo(); + if (res['status']) { + if (res['data'].isLogin) { + userInfo.value = res['data']; + user.put(UserBoxKey.userName, res['data'].uname); + user.put(UserBoxKey.userFace, res['data'].face); + user.put(UserBoxKey.userMid, res['data'].mid); + user.put(UserBoxKey.userLogin, true); + userLogin.value = true; + Get.find().readuUserFace(); + } else { + resetUserInfo(); + } + } else { + resetUserInfo(); + // SmartDialog.showToast(res['msg']); + } + await queryUserStatOwner(); + return res; + } + + Future queryUserStatOwner() async { + var res = await UserHttp.userStatOwner(); + if (res['status']) { + userStat.value = res['data']; + } + return res; + } + + Future resetUserInfo() async { + userInfo.value = UserInfoData(); + userStat.value = UserStat(); + await user.delete(UserBoxKey.userName); + await user.delete(UserBoxKey.userFace); + await user.delete(UserBoxKey.userMid); + await user.delete(UserBoxKey.userLogin); + userLogin.value = false; + Get.find().resetLast(); } } diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index 54796281..7976659f 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -1,8 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'controller.dart'; class MinePage extends StatefulWidget { @@ -22,18 +22,18 @@ class _MinePageState extends State { title: null, actions: [ IconButton( - onPressed: () {}, - icon: const Icon( - CupertinoIcons.moon, + onPressed: () { + Get.changeThemeMode(ThemeMode.dark); + }, + icon: Icon( + Get.theme == ThemeData.light() + ? CupertinoIcons.moon + : CupertinoIcons.sun_max, size: 22, ), - // icon: const Icon( - // CupertinoIcons.sun_max, - // size: 22, - // ), ), IconButton( - onPressed: () {}, + onPressed: () => Get.toNamed('/setting'), icon: const Icon( CupertinoIcons.slider_horizontal_3, ), @@ -43,7 +43,8 @@ class _MinePageState extends State { ), body: RefreshIndicator( onRefresh: () async { - await Future.delayed(const Duration(seconds: 2)); + await _mineController.queryUserInfo(); + await _mineController.queryUserStatOwner(); }, child: LayoutBuilder( builder: (context, constraint) { @@ -53,127 +54,19 @@ class _MinePageState extends State { height: constraint.maxHeight, child: Column( children: [ - InkWell( - onTap: () { - Get.toNamed( - '/webview', - parameters: { - 'url': - 'https://passport.bilibili.com/h5-app/passport/login', - 'type': 'login', - 'pageTitle': '登录bilibili', - }, - ); + FutureBuilder( + future: _mineController.queryUserInfo(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data['status']) { + return Obx(() => userInfoBuild()); + } else { + return userInfoBuild(); + } + } else { + return userInfoBuild(); + } }, - child: Padding( - padding: const EdgeInsets.only(top: 10, bottom: 10), - child: Row( - children: [ - const SizedBox(width: 20), - ClipOval( - child: Container( - width: 75, - height: 75, - color: Theme.of(context) - .colorScheme - .onInverseSurface, - child: Center( - child: - Image.asset('assets/images/loading.png'), - ), - ), - ), - const SizedBox(width: 14), - Text( - '点击登录', - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - ), - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.only(left: 12, right: 12), - child: LayoutBuilder( - builder: (context, constraints) { - TextStyle style = TextStyle( - fontSize: Theme.of(context) - .textTheme - .titleMedium! - .fontSize, - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold); - return SizedBox( - height: constraints.maxWidth / 3 * 0.6, - child: GridView.count( - primary: false, - padding: const EdgeInsets.all(0), - crossAxisCount: 3, - childAspectRatio: 1.67, - children: [ - InkWell( - onTap: () {}, - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('-', style: style), - const SizedBox(height: 8), - Text( - '动态', - style: Theme.of(context) - .textTheme - .labelMedium, - ), - ], - ), - ), - InkWell( - onTap: () {}, - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '50', - style: style, - ), - const SizedBox(height: 8), - Text( - '关注', - style: Theme.of(context) - .textTheme - .labelMedium, - ), - ], - ), - ), - InkWell( - onTap: () {}, - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '-', - style: style, - ), - const SizedBox(height: 8), - Text( - '粉丝', - style: Theme.of(context) - .textTheme - .labelMedium, - ), - ], - ), - ), - ], - ), - ); - }, - ), ), const SizedBox(height: 20), Padding( @@ -224,6 +117,251 @@ class _MinePageState extends State { ), ); } + + Widget userInfoBuild() { + return Column( + children: [ + const SizedBox(height: 5), + GestureDetector( + onTap: () => _mineController.onLogin(), + child: ClipOval( + child: Container( + width: 85, + height: 85, + color: Theme.of(context).colorScheme.onInverseSurface, + child: Center( + child: _mineController.userInfo.value.face != null + ? NetworkImgLayer( + src: _mineController.userInfo.value.face, + width: 85, + height: 85) + : Image.asset('assets/images/loading.png'), + ), + ), + ), + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _mineController.userInfo.value.uname ?? '点击头像登录', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(width: 4), + Image.asset( + 'assets/images/lv/lv${_mineController.userInfo.value.levelInfo != null ? _mineController.userInfo.value.levelInfo!.currentLevel : '0'}.png', + height: 10, + ), + ], + ), + const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich(TextSpan(children: [ + TextSpan( + text: '硬币: ', + style: + TextStyle(color: Theme.of(context).colorScheme.outline)), + TextSpan( + text: (_mineController.userInfo.value.money ?? 'pilipala') + .toString(), + style: + TextStyle(color: Theme.of(context).colorScheme.primary)), + ])) + ], + ), + const SizedBox(height: 5), + if (_mineController.userInfo.value.levelInfo != null) ...[ + LayoutBuilder( + builder: (context, BoxConstraints box) { + return SizedBox( + width: box.maxWidth, + height: 24, + child: Stack( + children: [ + Positioned( + top: 0, + right: 0, + child: SizedBox( + height: 22, + width: box.maxWidth * + (1 - + (_mineController + .userInfo.value.levelInfo!.currentExp! / + _mineController + .userInfo.value.levelInfo!.nextExp!)), + child: Center( + child: Text( + (_mineController + .userInfo.value.levelInfo!.nextExp! - + _mineController + .userInfo.value.levelInfo!.currentExp!) + .toString(), + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 12, + ), + ), + ), + ), + ), + ], + ), + ); + }, + ), + LayoutBuilder( + builder: (context, BoxConstraints box) { + return Container( + width: box.maxWidth, + height: 1, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Theme.of(context).colorScheme.onInverseSurface, + ), + child: Stack( + children: [ + Positioned( + top: 0, + left: 0, + bottom: 0, + child: Container( + width: box.maxWidth * + (_mineController + .userInfo.value.levelInfo!.currentExp! / + _mineController + .userInfo.value.levelInfo!.nextExp!), + height: 1, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ), + ); + }, + ), + ], + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.only(left: 12, right: 12), + child: LayoutBuilder( + builder: (context, constraints) { + TextStyle style = TextStyle( + fontSize: Theme.of(context).textTheme.titleMedium!.fontSize, + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold); + return SizedBox( + height: constraints.maxWidth / 3 * 0.6, + child: GridView.count( + primary: false, + padding: const EdgeInsets.all(0), + crossAxisCount: 3, + childAspectRatio: 1.67, + children: [ + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: + (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, child: child); + }, + child: Text( + (_mineController.userStat.value.dynamicCount ?? + '-') + .toString(), + key: ValueKey(_mineController + .userStat.value.dynamicCount + .toString()), + style: style), + ), + const SizedBox(height: 8), + Text( + '动态', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: + (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, child: child); + }, + child: Text( + (_mineController.userStat.value.following ?? + '-') + .toString(), + key: ValueKey(_mineController + .userStat.value.following + .toString()), + style: style), + ), + const SizedBox(height: 8), + Text( + '关注', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: + (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, child: child); + }, + child: Text( + (_mineController.userStat.value.follower ?? '-') + .toString(), + key: ValueKey(_mineController + .userStat.value.follower + .toString()), + style: style), + ), + const SizedBox(height: 8), + Text( + '粉丝', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + ], + ), + ); + }, + ), + ), + ], + ); + } } class ActionItem extends StatelessWidget { diff --git a/lib/pages/setting/controller.dart b/lib/pages/setting/controller.dart new file mode 100644 index 00000000..af6b6026 --- /dev/null +++ b/lib/pages/setting/controller.dart @@ -0,0 +1,23 @@ +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/http/init.dart'; +import 'package:pilipala/pages/mine/controller.dart'; +import 'package:pilipala/utils/storage.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; + +class SettingController extends GetxController { + Box user = GStrorage.user; + RxBool userLogin = false.obs; + + @override + void onInit() { + super.onInit(); + userLogin.value = user.get(UserBoxKey.userLogin) ?? false; + } + + loginOut() async { + await Request.removeCookie(); + await Get.find().resetUserInfo(); + userLogin.value = user.get(UserBoxKey.userLogin) ?? false; + } +} diff --git a/lib/pages/setting/index.dart b/lib/pages/setting/index.dart new file mode 100644 index 00000000..30fa06b3 --- /dev/null +++ b/lib/pages/setting/index.dart @@ -0,0 +1,4 @@ +library setting; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart new file mode 100644 index 00000000..63321afa --- /dev/null +++ b/lib/pages/setting/view.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/pages/setting/index.dart'; + +class SettingPage extends StatefulWidget { + const SettingPage({super.key}); + + @override + State createState() => _SettingPageState(); +} + +class _SettingPageState extends State { + final SettingController _settingController = Get.put(SettingController()); + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('设置'), + ), + body: Column( + children: [ + Obx( + () => Visibility( + visible: _settingController.userLogin.value, + child: ListTile( + onTap: () => _settingController.loginOut(), + dense: false, + title: const Text('退出登录'), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/webview/controller.dart b/lib/pages/webview/controller.dart index b8d591cc..eee80c02 100644 --- a/lib/pages/webview/controller.dart +++ b/lib/pages/webview/controller.dart @@ -1,6 +1,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/constants.dart'; +import 'package:pilipala/http/init.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/utils/cookie.dart'; @@ -21,6 +22,12 @@ class WebviewController extends GetxController { pageTitle = Get.parameters['pageTitle']!; webviewInit(); + if (type == 'login') { + controller.clearCache(); + controller.clearLocalStorage(); + WebViewCookieManager().clearCookies(); + controller.setUserAgent(Request().headerUa('mob')); + } } webviewInit() { @@ -49,7 +56,7 @@ class WebviewController extends GetxController { if (result['status'] && result['data'].isLogin) { SmartDialog.showToast('登录成功'); Get.find().userInfo = result['data']; - // Get.back(); + Get.back(); } } catch (e) { print(e); diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 47cd8bbe..753ba8a0 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -4,6 +4,7 @@ import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/webview/index.dart'; +import 'package:pilipala/pages/setting/index.dart'; class Routes { static final List getPages = [ @@ -16,6 +17,8 @@ class Routes { // 图片预览 GetPage(name: '/preview', page: () => const ImagePreview()), // - GetPage(name: '/webview', page: () => const WebviewPage()) + GetPage(name: '/webview', page: () => const WebviewPage()), + // 设置 + GetPage(name: '/setting', page: () => const SettingPage()), ]; } diff --git a/lib/utils/cookie.dart b/lib/utils/cookie.dart index 42a2cb5d..8d5c891b 100644 --- a/lib/utils/cookie.dart +++ b/lib/utils/cookie.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/utils/utils.dart'; -import 'package:dio_cookie_manager/dio_cookie_manager.dart'; class SetCookie { static onSet(List cookiesList, String url) async { diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart new file mode 100644 index 00000000..24374f36 --- /dev/null +++ b/lib/utils/storage.dart @@ -0,0 +1,24 @@ +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; + +class GStrorage { + static late final Box user; + + static Future init() async { + final dir = await getApplicationDocumentsDirectory(); + final path = dir.path; + Hive.init('$path/hive'); + user = await Hive.openBox('user'); + } +} + +// 约定 key +class UserBoxKey { + static const String userName = 'userName'; + // 头像 + static const String userFace = 'userFace'; + // mid + static const String userMid = 'userMid'; + // 登录状态 + static const String userLogin = 'userLogin'; +} diff --git a/pubspec.lock b/pubspec.lock index 45514cc2..10b7569f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "8880b4cfe7b5b17d57c052a5a3a8cc1d4f546261c7cc8fbd717bd53f48db0568" + url: "https://pub.dev" + source: hosted + version: "59.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: a89627f49b0e70e068130a36571409726b04dab12da7e5625941d2c8ec278b96 + url: "https://pub.dev" + source: hosted + version: "5.11.1" args: dependency: transitive description: @@ -25,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" + url: "https://pub.dev" + source: hosted + version: "2.4.0" cached_network_image: dependency: "direct main" description: @@ -89,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.4" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" cookie_jar: dependency: "direct main" description: @@ -121,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad + url: "https://pub.dev" + source: hosted + version: "2.3.1" dbus: dependency: transitive description: @@ -288,6 +328,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.6.5" + glob: + dependency: transitive + description: + name: glob + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "65998cc4d2cd9680a3d9709d893d2f6bb15e6c1f92626c3f1fa650b4b3281521" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http: dependency: transitive description: @@ -344,6 +416,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" + source: hosted + version: "1.1.1" matcher: dependency: transitive description: @@ -392,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" path: dependency: transitive description: @@ -528,6 +616,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" rxdart: dependency: transitive description: @@ -557,6 +653,22 @@ packages: description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: b20e191de6964e98032573cecb1d2b169d96ba63fdb586d24dcd1003ba7e94f6 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" + source: hosted + version: "1.3.3" source_span: dependency: transitive description: @@ -725,6 +837,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.3" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" + source: hosted + version: "1.0.2" webview_cookie_manager: dependency: "direct main" description: @@ -789,6 +909,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.2" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" sdks: dart: ">=2.19.6 <3.0.0" flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index 63778e94..a856f809 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,8 @@ dependencies: # 存储 path_provider: ^2.0.14 + hive: ^2.2.3 + hive_flutter: ^1.1.0 # 设备信息 device_info_plus: ^8.2.0 @@ -84,6 +86,7 @@ dev_dependencies: # git: # url: https://github.com/nvi9/flutter_launcher_icons.git # ref: e045d40 + hive_generator: ^2.0.0 flutter_icons: android: true From ea674c4b4a8db89fb905fada60eb92f0cd2bbfcd Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 11 May 2023 00:14:00 +0800 Subject: [PATCH 24/30] =?UTF-8?q?feat:=20=E6=94=B6=E8=97=8F=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 4 + lib/http/user.dart | 22 ++- lib/models/user/fav_folder.dart | 108 ++++++++++++++ lib/pages/fav/controller.dart | 3 + lib/pages/fav/index.dart | 4 + lib/pages/fav/view.dart | 20 +++ lib/pages/home/widgets/app_bar.dart | 8 ++ lib/pages/main/controller.dart | 67 +++++---- lib/pages/main/view.dart | 2 +- lib/pages/media/controller.dart | 41 ++++++ lib/pages/media/index.dart | 4 + lib/pages/media/view.dart | 214 ++++++++++++++++++++++++++++ lib/pages/mine/controller.dart | 4 +- lib/pages/mine/view.dart | 45 +----- lib/router/app_pages.dart | 6 + 15 files changed, 481 insertions(+), 71 deletions(-) create mode 100644 lib/models/user/fav_folder.dart create mode 100644 lib/pages/fav/controller.dart create mode 100644 lib/pages/fav/index.dart create mode 100644 lib/pages/fav/view.dart create mode 100644 lib/pages/media/controller.dart create mode 100644 lib/pages/media/index.dart create mode 100644 lib/pages/media/view.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 32abb1b6..80a3882d 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -26,4 +26,8 @@ class Api { // 获取当前用户状态 static const String userStatOwner = '/x/web-interface/nav/stat'; + + // 收藏夹 + // https://api.bilibili.com/x/v3/fav/folder/created/list?pn=1&ps=10&up_mid=17340771 + static const String userFavFolder = '/x/v3/fav/folder/created/list'; } diff --git a/lib/http/user.dart b/lib/http/user.dart index 8f8881ab..bf23d3ee 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -1,5 +1,6 @@ import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/stat.dart'; @@ -29,7 +30,26 @@ class UserHttp { UserStat data = UserStat.fromJson(res.data['data']); return {'status': true, 'data': data}; } else { - return {'status': false}; + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } + + // 收藏夹 + static Future userfavFolder({ + required int pn, + required int ps, + required int mid, + }) async { + var res = await Request().get(Api.userFavFolder, data: { + 'pn': pn, + 'ps': ps, + 'up_mid': mid, + }); + if (res.data['code'] == 0) { + FavFolderData data = FavFolderData.fromJson(res.data['data']); + return {'status': true, 'data': data}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; } } } diff --git a/lib/models/user/fav_folder.dart b/lib/models/user/fav_folder.dart new file mode 100644 index 00000000..0e0e61eb --- /dev/null +++ b/lib/models/user/fav_folder.dart @@ -0,0 +1,108 @@ +class FavFolderData { + FavFolderData({ + this.count, + this.list, + this.hasMore, + }); + + int? count; + List? list; + bool? hasMore; + + FavFolderData.fromJson(Map json) { + count = json['count']; + list = json['list'] != null + ? json['list'] + .map((e) => FavFolderItemData.fromJson(e)) + .toList() + : [FavFolderItemData()]; + hasMore = json['has_more']; + } +} + +class FavFolderItemData { + FavFolderItemData({ + this.id, + this.fid, + this.mid, + this.attr, + this.title, + this.cover, + this.upper, + this.coverType, + this.intro, + this.ctime, + this.mtime, + this.state, + this.favState, + this.mediaCount, + this.viewCount, + this.vt, + this.playSwitch, + this.type, + this.link, + this.bvid, + }); + + int? id; + int? fid; + int? mid; + int? attr; + String? title; + String? cover; + Upper? upper; + int? coverType; + String? intro; + int? ctime; + int? mtime; + int? state; + int? favState; + int? mediaCount; + int? viewCount; + int? vt; + int? playSwitch; + int? type; + String? link; + String? bvid; + + FavFolderItemData.fromJson(Map json) { + id = json['id']; + fid = json['fid']; + mid = json['mid']; + attr = json['attr']; + title = json['title']; + cover = json['cover']; + upper = Upper.fromJson(json['upper']); + coverType = json['cover_type']; + intro = json['intro']; + ctime = json['ctime']; + mtime = json['mtime']; + state = json['state']; + favState = json['fav_state']; + mediaCount = json['media_count']; + viewCount = json['view_count']; + vt = json['vt']; + playSwitch = json['play_switch']; + type = json['type']; + link = json['link']; + bvid = json['bvid']; + } +} + +class Upper { + Upper({ + this.mid, + this.name, + this.face, + }); + + int? mid; + String? name; + String? face; + + Upper.fromJson(Map json) { + mid = json['mid']; + name = json['name']; + face = json['face']; + } +} diff --git a/lib/pages/fav/controller.dart b/lib/pages/fav/controller.dart new file mode 100644 index 00000000..ff14f9f9 --- /dev/null +++ b/lib/pages/fav/controller.dart @@ -0,0 +1,3 @@ +import 'package:get/get.dart'; + +class FavController extends GetxController {} diff --git a/lib/pages/fav/index.dart b/lib/pages/fav/index.dart new file mode 100644 index 00000000..84d36325 --- /dev/null +++ b/lib/pages/fav/index.dart @@ -0,0 +1,4 @@ +library fav; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart new file mode 100644 index 00000000..762fcf18 --- /dev/null +++ b/lib/pages/fav/view.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class FavPage extends StatefulWidget { + const FavPage({super.key}); + + @override + State createState() => _FavPageState(); +} + +class _FavPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + title: Text('我的收藏'), + ), + ); + } +} diff --git a/lib/pages/home/widgets/app_bar.dart b/lib/pages/home/widgets/app_bar.dart index f0b142e0..872d7eef 100644 --- a/lib/pages/home/widgets/app_bar.dart +++ b/lib/pages/home/widgets/app_bar.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/pages/mine/view.dart'; class HomeAppBar extends StatelessWidget { const HomeAppBar({super.key}); @@ -42,6 +44,12 @@ class HomeAppBar extends StatelessWidget { // onPressed: () {}, // icon: const Icon(CupertinoIcons.bell, size: 22), // ), + IconButton( + onPressed: () { + Get.bottomSheet(const MinePage()); + }, + icon: const Icon(CupertinoIcons.person, size: 22), + ), const SizedBox(width: 10) ], elevation: 0, diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 752ff713..25b219e7 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -5,26 +5,26 @@ import 'package:hive/hive.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/pages/home/view.dart'; import 'package:pilipala/pages/hot/view.dart'; -import 'package:pilipala/pages/mine/view.dart'; +import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/utils/storage.dart'; class MainController extends GetxController { List pages = [ const HomePage(), const HotPage(), - const MinePage(), + const MediaPage(), ]; RxList navigationBars = [ { // 'icon': const Icon(Icons.home_outlined), // 'selectedIcon': const Icon(Icons.home), 'icon': const Icon( - CupertinoIcons.house, - size: 18, + CupertinoIcons.square_favorites_alt, + size: 21, ), 'selectedIcon': const Icon( - CupertinoIcons.house_fill, - size: 18, + CupertinoIcons.square_favorites_alt_fill, + size: 21, ), 'label': "推荐", }, @@ -41,46 +41,57 @@ class MainController extends GetxController { ), 'label': "热门", }, + // { + // 'icon': const Icon( + // CupertinoIcons.person, + // size: 21, + // ), + // 'selectedIcon': const Icon( + // CupertinoIcons.person_fill, + // size: 21, + // ), + // 'label': "我的", + // }, { // 'icon': const Icon(Icons.person_outline), // 'selectedIcon': const Icon(Icons.person), 'icon': const Icon( - CupertinoIcons.person, + CupertinoIcons.tray_full, size: 21, ), 'selectedIcon': const Icon( - CupertinoIcons.person_fill, + CupertinoIcons.tray_full_fill, size: 21, ), - 'label': "我的", + 'label': "媒体库", } ].obs; @override void onInit() { super.onInit(); - readuUserFace(); + // readuUserFace(); } // 设置头像 - readuUserFace() async { - Box user = GStrorage.user; - if (user.get(UserBoxKey.userFace) != null) { - navigationBars.last['icon'] = - navigationBars.last['selectedIcon'] = NetworkImgLayer( - width: 25, - height: 25, - type: 'avatar', - src: user.get(UserBoxKey.userFace), - ); - navigationBars.last['label'] = '我'; - } - } + // readuUserFace() async { + // Box user = GStrorage.user; + // if (user.get(UserBoxKey.userFace) != null) { + // navigationBars.last['icon'] = + // navigationBars.last['selectedIcon'] = NetworkImgLayer( + // width: 25, + // height: 25, + // type: 'avatar', + // src: user.get(UserBoxKey.userFace), + // ); + // navigationBars.last['label'] = '我'; + // } + // } // 重置 - resetLast() { - navigationBars.last['icon'] = const Icon(Icons.person_outline); - navigationBars.last['selectedIcon'] = const Icon(Icons.person); - navigationBars.last['label'] = '我的'; - } + // resetLast() { + // navigationBars.last['icon'] = const Icon(Icons.person_outline); + // navigationBars.last['selectedIcon'] = const Icon(Icons.person); + // navigationBars.last['label'] = '我的'; + // } } diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index bce9bbc2..ded53199 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -20,7 +20,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { late AnimationController? _animationController; late Animation? _fadeAnimation; late Animation? _slideAnimation; - int selectedIndex = 2; + int selectedIndex = 0; int? _lastSelectTime; //上次点击时间 @override diff --git a/lib/pages/media/controller.dart b/lib/pages/media/controller.dart new file mode 100644 index 00000000..0c0f31f8 --- /dev/null +++ b/lib/pages/media/controller.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/utils/storage.dart'; + +class MediaController extends GetxController { + Rx favFolderData = FavFolderData().obs; + List list = [ + { + 'icon': Icons.file_download_outlined, + 'title': '离线缓存', + 'onTap': () => Get.toNamed('/fav'), + }, + { + 'icon': Icons.history, + 'title': '观看记录', + 'onTap': () => Get.toNamed('/fav'), + }, + { + 'icon': Icons.star_border, + 'title': '我的收藏', + 'onTap': () => Get.toNamed('/fav'), + }, + { + 'icon': Icons.watch_later_outlined, + 'title': '稍后再看', + 'onTap': () => Get.toNamed('/fav'), + }, + ]; + + Future queryFavFolder() async { + var res = await await UserHttp.userfavFolder( + pn: 1, + ps: 5, + mid: GStrorage.user.get(UserBoxKey.userMid), + ); + favFolderData.value = res['data']; + return res; + } +} diff --git a/lib/pages/media/index.dart b/lib/pages/media/index.dart new file mode 100644 index 00000000..8fae4891 --- /dev/null +++ b/lib/pages/media/index.dart @@ -0,0 +1,4 @@ +library media; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart new file mode 100644 index 00000000..dd2b0446 --- /dev/null +++ b/lib/pages/media/view.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/pages/media/index.dart'; + +class MediaPage extends StatefulWidget { + const MediaPage({super.key}); + + @override + State createState() => _MediaPageState(); +} + +class _MediaPageState extends State + with AutomaticKeepAliveClientMixin { + final MediaController _mediaController = Get.put(MediaController()); + Future? _futureBuilderFuture; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _futureBuilderFuture = _mediaController.queryFavFolder(); + } + + @override + Widget build(BuildContext context) { + Color primary = Theme.of(context).colorScheme.primary; + return Scaffold( + appBar: AppBar(toolbarHeight: 30), + body: Column( + children: [ + ListTile( + leading: null, + title: Padding( + padding: const EdgeInsets.only(left: 20), + child: Text( + '媒体库', + style: TextStyle( + fontSize: Theme.of(context).textTheme.titleLarge!.fontSize, + ), + ), + ), + ), + for (var i in _mediaController.list) ...[ + ListTile( + onTap: () => i['onTap'](), + leading: Padding( + padding: const EdgeInsets.only(left: 15), + child: Icon( + i['icon'], + color: primary, + ), + ), + minLeadingWidth: 0, + title: Text(i['title']), + ), + ], + favFolder() + ], + ), + ); + } + + Widget favFolder() { + return Column( + children: [ + Divider( + height: 35, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + ListTile( + onTap: () {}, + leading: null, + dense: true, + title: Padding( + padding: const EdgeInsets.only(left: 10), + child: Obx( + () => Text.rich( + TextSpan( + children: [ + TextSpan( + text: '收藏夹 ', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.titleMedium!.fontSize, + ), + ), + if (_mediaController.favFolderData.value.count != null) + TextSpan( + text: _mediaController.favFolderData.value.count + .toString(), + style: TextStyle( + fontSize: + Theme.of(context).textTheme.titleSmall!.fontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ), + ), + ), + trailing: Padding( + padding: const EdgeInsets.only(right: 10), + child: Text( + '查看全部', + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline), + ), + ), + ), + // const SizedBox(height: 10), + SizedBox( + width: double.infinity, + height: 170, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + const SizedBox(width: 20), + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data; + if (data['status']) { + return Obx(() => Row( + children: [ + if (_mediaController.favFolderData.value.list != + null) ...[ + for (FavFolderItemData i in _mediaController + .favFolderData.value.list!) ...[ + FavFolderItem(item: i), + const SizedBox(width: 14) + ] + ] + ], + )); + } else { + return SizedBox( + height: 160, + child: Center(child: Text(data['msg'])), + ); + } + } else { + // 骨架屏 + return SizedBox(); + } + }), + // for (var i in [1, 2, 3]) ...[const FavFolderItem()], + const SizedBox(width: 10) + ], + ), + ), + ], + ); + } +} + +class FavFolderItem extends StatelessWidget { + FavFolderItem({super.key, this.item}); + FavFolderItemData? item; + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Container( + width: 110 * 16 / 9, + height: 110, + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + color: Theme.of(context).colorScheme.onInverseSurface, + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.onInverseSurface, + offset: const Offset(4, -12), // 阴影与容器的距离 + blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。 + spreadRadius: 0.0, // 在应用模糊之前,框应该膨胀的量。 + ), + ], + ), + child: LayoutBuilder( + builder: (context, BoxConstraints box) { + return NetworkImgLayer( + src: item!.cover, + width: box.maxWidth, + height: box.maxHeight, + ); + }, + ), + ), + Text( + ' ${item!.title}', + overflow: TextOverflow.fade, + maxLines: 1, + ), + Text( + ' 共${item!.mediaCount}条视频', + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: Theme.of(context).colorScheme.outline), + ) + ], + ); + } +} diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index 62a62438..93b0d1d0 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -36,7 +36,7 @@ class MineController extends GetxController { user.put(UserBoxKey.userMid, res['data'].mid); user.put(UserBoxKey.userLogin, true); userLogin.value = true; - Get.find().readuUserFace(); + // Get.find().readuUserFace(); } else { resetUserInfo(); } @@ -64,6 +64,6 @@ class MineController extends GetxController { await user.delete(UserBoxKey.userMid); await user.delete(UserBoxKey.userLogin); userLogin.value = false; - Get.find().resetLast(); + // Get.find().resetLast(); } } diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index 7976659f..85e43ad0 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -19,6 +19,11 @@ class _MinePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + automaticallyImplyLeading: false, + scrolledUnderElevation: 0, + elevation: 0, + toolbarHeight: kTextTabBarHeight + 20, + backgroundColor: Colors.transparent, title: null, actions: [ IconButton( @@ -54,6 +59,7 @@ class _MinePageState extends State { height: constraint.maxHeight, child: Column( children: [ + const SizedBox(height: 10), FutureBuilder( future: _mineController.queryUserInfo(), builder: (context, snapshot) { @@ -69,45 +75,6 @@ class _MinePageState extends State { }, ), const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.only(left: 12, right: 12), - child: LayoutBuilder( - builder: (context, constraints) { - return SizedBox( - height: constraints.maxWidth / 4 * 0.8, - child: GridView.count( - primary: false, - padding: const EdgeInsets.all(0), - crossAxisCount: 4, - childAspectRatio: 1.25, - children: [ - ActionItem( - icon: - const Icon(CupertinoIcons.cloud_download), - onTap: () => {}, - text: '离线缓存', - ), - ActionItem( - icon: const Icon(CupertinoIcons.time), - onTap: () => {}, - text: '历史记录', - ), - ActionItem( - icon: const Icon(CupertinoIcons.star), - onTap: () => {}, - text: '我的收藏', - ), - ActionItem( - icon: const Icon(CupertinoIcons.film), - onTap: () => {}, - text: '稍后再看', - ), - ], - ), - ); - }, - ), - ), ], ), ), diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 753ba8a0..9b7e385b 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,10 +1,12 @@ import 'package:get/get.dart'; +import 'package:pilipala/pages/fav/index.dart'; import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/webview/index.dart'; import 'package:pilipala/pages/setting/index.dart'; +import 'package:pilipala/pages/media/index.dart'; class Routes { static final List getPages = [ @@ -20,5 +22,9 @@ class Routes { GetPage(name: '/webview', page: () => const WebviewPage()), // 设置 GetPage(name: '/setting', page: () => const SettingPage()), + // + GetPage(name: '/media', page: () => const MediaPage()), + // + GetPage(name: '/fav', page: () => const FavPage()), ]; } From e426236741abd552e3e69c8325f6ca855871ff8f Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 11 May 2023 13:45:02 +0800 Subject: [PATCH 25/30] =?UTF-8?q?feat:=20=E6=94=B6=E8=97=8F=E5=A4=B9?= =?UTF-8?q?=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 10 + lib/http/user.dart | 24 ++ lib/models/user/fav_detail.dart | 100 +++++++++ lib/pages/favDetail/controller.dart | 29 +++ lib/pages/favDetail/index.dart | 4 + lib/pages/favDetail/view.dart | 206 ++++++++++++++++++ .../favDetail/widget/fav_video_card.dart | 143 ++++++++++++ lib/pages/media/view.dart | 98 +++++---- .../video/detail/introduction/controller.dart | 4 +- lib/router/app_pages.dart | 3 + 10 files changed, 575 insertions(+), 46 deletions(-) create mode 100644 lib/models/user/fav_detail.dart create mode 100644 lib/pages/favDetail/controller.dart create mode 100644 lib/pages/favDetail/index.dart create mode 100644 lib/pages/favDetail/view.dart create mode 100644 lib/pages/favDetail/widget/fav_video_card.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 80a3882d..b47272d2 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -30,4 +30,14 @@ class Api { // 收藏夹 // https://api.bilibili.com/x/v3/fav/folder/created/list?pn=1&ps=10&up_mid=17340771 static const String userFavFolder = '/x/v3/fav/folder/created/list'; + + /// 收藏夹 详情 + /// media_id int 收藏夹id + /// pn int 当前页 + /// ps int pageSize + /// keyword String 搜索词 + /// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿 + /// tid int 分区id + // https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0 + static const String userFavFolderDetail = '/x/v3/fav/resource/list'; } diff --git a/lib/http/user.dart b/lib/http/user.dart index bf23d3ee..6d4be974 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -1,5 +1,6 @@ import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; +import 'package:pilipala/models/user/fav_detail.dart'; import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/stat.dart'; @@ -52,4 +53,27 @@ class UserHttp { return {'status': false, 'data': [], 'msg': res.data['message']}; } } + + static Future userFavFolderDetail( + {required int mediaId, + required int pn, + required int ps, + String keyword = '', + String order = 'mtime'}) async { + var res = await Request().get(Api.userFavFolderDetail, data: { + 'media_id': mediaId, + 'pn': pn, + 'ps': ps, + 'keyword': keyword, + 'order': order, + 'type': 0, + 'tid': 0 + }); + if (res.data['code'] == 0) { + FavDetailData data = FavDetailData.fromJson(res.data['data']); + return {'status': true, 'data': data}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } } diff --git a/lib/models/user/fav_detail.dart b/lib/models/user/fav_detail.dart new file mode 100644 index 00000000..3085dc9d --- /dev/null +++ b/lib/models/user/fav_detail.dart @@ -0,0 +1,100 @@ +import 'package:pilipala/models/model_owner.dart'; + +class FavDetailData { + FavDetailData({ + this.info, + this.medias, + this.hasMore, + }); + + Map? info; + List? medias; + bool? hasMore; + + FavDetailData.fromJson(Map json) { + info = json['info']; + medias = json['medias'] != null + ? json['medias'] + .map((e) => FavDetailItemData.fromJson(e)) + .toList() + : [FavDetailItemData()]; + hasMore = json['has_more']; + } +} + +class FavDetailItemData { + FavDetailItemData({ + this.id, + this.type, + this.title, + this.pic, + this.intro, + this.page, + this.duration, + this.owner, + this.attr, + this.cntInfo, + this.link, + this.ctime, + this.pubdate, + this.favTime, + this.bvId, + this.bvid, + // this.season, + // this.ogv, + this.stat, + }); + + int? id; + int? type; + String? title; + String? pic; + String? intro; + int? page; + int? duration; + Owner? owner; + int? attr; + Map? cntInfo; + String? link; + int? ctime; + int? pubdate; + int? favTime; + String? bvId; + String? bvid; + Stat? stat; + + FavDetailItemData.fromJson(Map json) { + id = json['id']; + type = json['type']; + title = json['title']; + pic = json['cover']; + intro = json['intro']; + page = json['page']; + duration = json['duration']; + owner = Owner.fromJson(json['upper']); + attr = json['attr']; + cntInfo = json['cnt_info']; + link = json['link']; + ctime = json['ctime']; + pubdate = json['pubtime']; + favTime = json['fav_time']; + bvId = json['bv_id']; + bvid = json['bvid']; + stat = Stat.fromJson(json['cnt_info']); + } +} + +class Stat { + Stat({ + this.view, + this.danmaku, + }); + + int? view; + int? danmaku; + + Stat.fromJson(Map json) { + view = json['play']; + danmaku = json['danmaku']; + } +} diff --git a/lib/pages/favDetail/controller.dart b/lib/pages/favDetail/controller.dart new file mode 100644 index 00000000..21303115 --- /dev/null +++ b/lib/pages/favDetail/controller.dart @@ -0,0 +1,29 @@ +import 'package:get/get.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/models/user/fav_detail.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; + +class FavDetailController extends GetxController { + FavFolderItemData? item; + Rx favDetailData = FavDetailData().obs; + int? mediaId; + + @override + void onInit() { + item = Get.arguments; + if (Get.parameters.keys.isNotEmpty) { + mediaId = int.parse(Get.parameters['mediaId']!); + } + super.onInit(); + } + + Future queryUserFavFolderDetail() async { + var res = await await UserHttp.userFavFolderDetail( + pn: 1, + ps: 15, + mediaId: mediaId!, + ); + favDetailData.value = res['data']; + return res; + } +} diff --git a/lib/pages/favDetail/index.dart b/lib/pages/favDetail/index.dart new file mode 100644 index 00000000..dfeafac8 --- /dev/null +++ b/lib/pages/favDetail/index.dart @@ -0,0 +1,4 @@ +library favdetail; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/favDetail/view.dart b/lib/pages/favDetail/view.dart new file mode 100644 index 00000000..1fb48886 --- /dev/null +++ b/lib/pages/favDetail/view.dart @@ -0,0 +1,206 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/pages/favDetail/index.dart'; + +import 'widget/fav_video_card.dart'; + +class FavDetailPage extends StatefulWidget { + const FavDetailPage({super.key}); + + @override + State createState() => _FavDetailPageState(); +} + +class _FavDetailPageState extends State { + late final ScrollController _controller = ScrollController(); + final FavDetailController _favDetailController = + Get.put(FavDetailController()); + late StreamController titleStreamC; // a + + @override + void initState() { + super.initState(); + titleStreamC = StreamController(); + _controller.addListener( + () { + if (_controller.offset > 160) { + titleStreamC.add(true); + } else if (_controller.offset <= 160) { + titleStreamC.add(false); + } + }, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + controller: _controller, + slivers: [ + SliverAppBar( + expandedHeight: 260 - MediaQuery.of(context).padding.top, + pinned: true, + title: StreamBuilder( + stream: titleStreamC.stream, + initialData: false, + builder: (context, AsyncSnapshot snapshot) { + return AnimatedOpacity( + opacity: snapshot.data ? 1 : 0, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 500), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _favDetailController.item!.title!, + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + '共${_favDetailController.item!.mediaCount!}条视频', + style: Theme.of(context).textTheme.labelMedium, + ) + ], + ) + ], + ), + ); + }, + ), + // actions: [ + // IconButton( + // onPressed: () {}, + // icon: const Icon(Icons.more_vert), + // ), + // const SizedBox(width: 4) + // ], + flexibleSpace: FlexibleSpaceBar( + background: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.withOpacity(0.2), + ), + ), + ), + padding: EdgeInsets.only( + top: kTextTabBarHeight + + MediaQuery.of(context).padding.top + + 30, + left: 20, + right: 20), + child: SizedBox( + height: 200, + child: Row( + // mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 180, + height: 110, + child: NetworkImgLayer( + width: 180, + height: 110, + src: _favDetailController.item!.cover, + ), + ), + const SizedBox(width: 14), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + _favDetailController.item!.title!, + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleMedium! + .fontSize, + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text( + _favDetailController.item!.upper!.name!, + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize, + color: Theme.of(context).colorScheme.outline), + ) + ], + ) + ], + ), + ), + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), + child: Text( + '共${_favDetailController.item!.mediaCount}条视频', + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + letterSpacing: 1), + ), + ), + ), + FutureBuilder( + future: _favDetailController.queryUserFavFolderDetail(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data; + if (data['status']) { + return Obx( + () => SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return FavVideoCardH( + videoItem: _favDetailController + .favDetailData.value.medias![index], + ); + }, + childCount: _favDetailController + .favDetailData.value.medias!.length), + ), + ); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + return const SliverToBoxAdapter( + child: Center( + child: Text('加载中'), + ), + ); + } + }, + ), + SliverToBoxAdapter( + child: SizedBox( + height: MediaQuery.of(context).padding.bottom + 20, + ), + ) + ], + ), + ); + } +} diff --git a/lib/pages/favDetail/widget/fav_video_card.dart b/lib/pages/favDetail/widget/fav_video_card.dart new file mode 100644 index 00000000..1cfc3c91 --- /dev/null +++ b/lib/pages/favDetail/widget/fav_video_card.dart @@ -0,0 +1,143 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/stat/up.dart'; +import 'package:pilipala/common/widgets/stat/view.dart'; +import 'package:pilipala/utils/utils.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; + +// 收藏视频卡片 - 水平布局 +class FavVideoCardH extends StatelessWidget { + var videoItem; + + FavVideoCardH({Key? key, required this.videoItem}) : super(key: key); + + @override + Widget build(BuildContext context) { + int id = videoItem.id; + String heroTag = Utils.makeHeroTag(id); + return InkWell( + onTap: () async { + await Future.delayed(const Duration(milliseconds: 200)); + Get.toNamed('/video?aid=$id', + arguments: {'videoItem': videoItem, 'heroTag': heroTag}); + }, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 5, 12, 5), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double width = + (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; + return SizedBox( + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = MediaQuery.of(context).devicePixelRatio; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + // src: videoItem['pic'] + + // '@${(maxWidth * 2).toInt()}w', + src: videoItem.pic + '@.webp', + width: maxWidth, + height: maxHeight, + ), + ), + // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), + Positioned( + right: 4, + bottom: 4, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 1, horizontal: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.black54.withOpacity(0.4)), + child: Text( + Utils.timeFormat(videoItem.duration!), + style: const TextStyle( + fontSize: 11, color: Colors.white), + ), + ), + ) + ], + ); + }, + ), + ), + VideoContent(videoItem: videoItem) + ], + ), + ); + }, + ), + ), + ], + ), + ); + } +} + +class VideoContent extends StatelessWidget { + final videoItem; + const VideoContent({super.key, required this.videoItem}); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 2, 6, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + videoItem.title, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: Theme.of(context).textTheme.titleSmall!.fontSize, + fontWeight: FontWeight.w500), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + const SizedBox(height: 4), + Text( + videoItem.owner.name, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + Row( + children: [ + StatView( + theme: 'gray', + view: videoItem.cntInfo['play'], + ), + const SizedBox(width: 8), + Text( + Utils.dateFormat(videoItem.pubdate!), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.outline), + ) + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart index dd2b0446..c4474a28 100644 --- a/lib/pages/media/view.dart +++ b/lib/pages/media/view.dart @@ -40,6 +40,7 @@ class _MediaPageState extends State '媒体库', style: TextStyle( fontSize: Theme.of(context).textTheme.titleLarge!.fontSize, + fontWeight: FontWeight.bold, ), ), ), @@ -47,6 +48,7 @@ class _MediaPageState extends State for (var i in _mediaController.list) ...[ ListTile( onTap: () => i['onTap'](), + dense: true, leading: Padding( padding: const EdgeInsets.only(left: 15), child: Icon( @@ -84,9 +86,9 @@ class _MediaPageState extends State TextSpan( text: '收藏夹 ', style: TextStyle( - fontSize: - Theme.of(context).textTheme.titleMedium!.fontSize, - ), + fontSize: + Theme.of(context).textTheme.titleMedium!.fontSize, + fontWeight: FontWeight.bold), ), if (_mediaController.favFolderData.value.count != null) TextSpan( @@ -165,50 +167,56 @@ class FavFolderItem extends StatelessWidget { FavFolderItemData? item; @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - Container( - width: 110 * 16 / 9, - height: 110, - margin: const EdgeInsets.only(bottom: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - color: Theme.of(context).colorScheme.onInverseSurface, - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.onInverseSurface, - offset: const Offset(4, -12), // 阴影与容器的距离 - blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。 - spreadRadius: 0.0, // 在应用模糊之前,框应该膨胀的量。 - ), - ], + return GestureDetector( + onTap: () => Get.toNamed('/favDetail', arguments: item, parameters: { + 'mediaId': item!.id.toString(), + }), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Container( + width: 180, + height: 110, + margin: const EdgeInsets.only(bottom: 8), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Theme.of(context).colorScheme.onInverseSurface, + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.onInverseSurface, + offset: const Offset(4, -12), // 阴影与容器的距离 + blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。 + spreadRadius: 0.0, // 在应用模糊之前,框应该膨胀的量。 + ), + ], + ), + child: LayoutBuilder( + builder: (context, BoxConstraints box) { + return NetworkImgLayer( + src: item!.cover, + width: box.maxWidth, + height: box.maxHeight, + ); + }, + ), ), - child: LayoutBuilder( - builder: (context, BoxConstraints box) { - return NetworkImgLayer( - src: item!.cover, - width: box.maxWidth, - height: box.maxHeight, - ); - }, + Text( + ' ${item!.title}', + overflow: TextOverflow.fade, + maxLines: 1, ), - ), - Text( - ' ${item!.title}', - overflow: TextOverflow.fade, - maxLines: 1, - ), - Text( - ' 共${item!.mediaCount}条视频', - style: Theme.of(context) - .textTheme - .labelSmall! - .copyWith(color: Theme.of(context).colorScheme.outline), - ) - ], + Text( + ' 共${item!.mediaCount}条视频', + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: Theme.of(context).colorScheme.outline), + ) + ], + ), ); } } diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 191a6d43..1ddaf530 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -35,7 +35,9 @@ class VideoIntroController extends GetxController { var args = Get.arguments['videoItem']; videoItem!['pic'] = args.pic; videoItem!['title'] = args.title; - videoItem!['stat'] = args.stat; + if (args.stat != null) { + videoItem!['stat'] = args.stat; + } videoItem!['pubdate'] = args.pubdate; videoItem!['owner'] = args.owner; } diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 9b7e385b..1ac52a93 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'package:pilipala/pages/fav/index.dart'; +import 'package:pilipala/pages/favDetail/index.dart'; import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/preview/index.dart'; @@ -26,5 +27,7 @@ class Routes { GetPage(name: '/media', page: () => const MediaPage()), // GetPage(name: '/fav', page: () => const FavPage()), + // + GetPage(name: '/favDetail', page: () => const FavDetailPage()), ]; } From 7bcdd209bac618499c1876937e6a928ad2608063 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 12 May 2023 00:03:10 +0800 Subject: [PATCH 26/30] =?UTF-8?q?feat:=20=E8=A7=86=E9=A2=91=E6=93=8D?= =?UTF-8?q?=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 65 ++++++- lib/http/init.dart | 11 ++ lib/http/video.dart | 71 +++++++ lib/pages/fav/controller.dart | 17 +- lib/pages/fav/view.dart | 57 +++++- lib/pages/main/controller.dart | 8 +- lib/pages/media/controller.dart | 8 +- lib/pages/media/view.dart | 19 +- .../video/detail/introduction/controller.dart | 63 ++++++ lib/pages/video/detail/introduction/view.dart | 180 ++++++++++++++---- 10 files changed, 447 insertions(+), 52 deletions(-) diff --git a/lib/http/api.dart b/lib/http/api.dart index b47272d2..b056b7cd 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -1,12 +1,72 @@ class Api { // 推荐视频 - static const String recommendList = '/x/web-interface/index/top/feed/rcmd'; + static const String recommendList = '/x/web-interface/index/top/rcmd'; + // 热门视频 static const String hotList = '/x/web-interface/popular'; + // 视频详情 // 竖屏 https://api.bilibili.com/x/web-interface/view?aid=527403921 // https://api.bilibili.com/x/web-interface/view/detail 获取视频超详细信息(web端) static const String videoIntro = '/x/web-interface/view'; + // 视频详情 超详细 + // https://api.bilibili.com/x/web-interface/view/detail?aid=527403921 + + /// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/action.md + // 点赞 Post + /// aid num 稿件avid 必要(可选) avid与bvid任选一个 + /// bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + /// like num 操作方式 必要 1:点赞 2:取消赞 + // csrf str CSRF Token(位于cookie) 必要 + // https://api.bilibili.com/x/web-interface/archive/like + static const String likeVideo = '/x/web-interface/archive/like'; + + //判断视频是否被点赞(双端)Get + // access_key str APP登录Token APP方式必要 + /// aid num 稿件avid 必要(可选) avid与bvid任选一个 + /// bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + // https://api.bilibili.com/x/web-interface/archive/has/like + static const String hasLikeVideo = '/x/web-interface/archive/has/like'; + + // 视频点踩 web端不支持 + + // 投币视频(web端)POST + /// aid num 稿件avid 必要(可选) avid与bvid任选一个 + /// bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + /// multiply num 投币数量 必要 上限为2 + /// select_like num 是否附加点赞 非必要 0:不点赞 1:同时点赞 默认为0 + // csrf str CSRF Token(位于cookie) 必要 + // https://api.bilibili.com/x/web-interface/coin/add + static const String coinVideo = '/x/web-interface/coin/add'; + + // 判断视频是否被投币(双端)GET + // access_key str APP登录Token APP方式必要 + /// aid num 稿件avid 必要(可选) avid与bvid任选一个 + /// bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + /// https://api.bilibili.com/x/web-interface/archive/coins + static const String hasCoinVideo = '/x/web-interface/archive/coins'; + + // 收藏视频(双端)POST + // access_key str APP登录Token APP方式必要 + /// rid num 稿件avid 必要 + /// type num 必须为2 必要 + /// add_media_ids nums 需要加入的收藏夹mlid 非必要 同时添加多个,用,(%2C)分隔 + /// del_media_ids nums 需要取消的收藏夹mlid 非必要 同时取消多个,用,(%2C)分隔 + // csrf str CSRF Token(位于cookie) Cookie方式必要 + // https://api.bilibili.com/medialist/gateway/coll/resource/deal + // https://api.bilibili.com/x/v3/fav/resource/deal + static const String favVideo = '/medialist/gateway/coll/resource/deal'; + + // 判断视频是否被收藏(双端)GET + /// aid + // https://api.bilibili.com/x/v2/fav/video/favoured + static const String hasFavVideo = '/x/v2/fav/video/favoured'; + + // 分享视频 (Web端) POST + // https://api.bilibili.com/x/web-interface/share/add + // aid num 稿件avid 必要(可选) avid与bvid任选一个 + // bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + // csrf str CSRF Token(位于cookie) 必要 // 视频详情页 相关视频 static const String relatedList = '/x/web-interface/archive/related'; @@ -40,4 +100,7 @@ class Api { /// tid int 分区id // https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0 static const String userFavFolderDetail = '/x/v3/fav/resource/list'; + + // 正在直播的up & 关注的up + // https://api.bilibili.com/x/polymer/web-dynamic/v1/portal } diff --git a/lib/http/init.dart b/lib/http/init.dart index 87ab8c59..eca0a6d4 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -55,6 +55,17 @@ class Request { dio.interceptors.add(cookieManager); } + // 从cookie中获取 csrf token + static Future getCsrf() async { + var cookies = await cookieManager.cookieJar + .loadForRequest(Uri.parse(HttpString.baseApiUrl)); + // for (var i in cookies) { + // print(i); + // } + var token = cookies.firstWhere((e) => e.name == 'bili_jct').value; + return token; + } + /* * config it and create */ diff --git a/lib/http/video.dart b/lib/http/video.dart index 686bbc2d..128b6993 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -1,3 +1,7 @@ +import 'dart:io'; + +import 'package:dio_cookie_manager/dio_cookie_manager.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/models/model_hot_video_item.dart'; @@ -86,8 +90,75 @@ class VideoHttp { list.add(HotVideoItemModel.fromJson(i)); } return {'status': true, 'data': list}; + } else { + return {'status': false, 'data': []}; + } + } + + // 获取点赞状态 + static Future hasLikeVideo({required String aid}) async { + var res = await Request().get(Api.hasLikeVideo, data: {'aid': aid}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': []}; + } + } + + // 获取投币状态 + static Future hasCoinVideo({required String aid}) async { + var res = await Request().get(Api.hasCoinVideo, data: {'aid': aid}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; } else { return {'status': true, 'data': []}; } } + + // 获取收藏状态 + static Future hasFavVideo({required String aid}) async { + var res = await Request().get(Api.hasFavVideo, data: {'aid': aid}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': []}; + } + } + + // 一键三连 + // (取消)点赞 + static Future likeVideo({required String aid, required bool type}) async { + var res = await Request().post( + Api.likeVideo, + data: { + 'aid': aid, + 'like': type ? 1 : 2, + 'csrf': await Request.getCsrf(), + }, + ); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } + + // (取消)收藏 + static Future favVideo( + {required String aid, required bool type, required String ids}) async { + Map data = {'rid': aid, 'type': 2}; + // type true 添加收藏 false 取消收藏 + if (type) { + data['add_media_ids'] = ids; + } else { + data['del_media_ids'] = ids; + } + var res = await Request() + .post(Api.favVideo, data: {'aid': aid, 'like': type ? 1 : 2}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': []}; + } + } } diff --git a/lib/pages/fav/controller.dart b/lib/pages/fav/controller.dart index ff14f9f9..0321ea39 100644 --- a/lib/pages/fav/controller.dart +++ b/lib/pages/fav/controller.dart @@ -1,3 +1,18 @@ import 'package:get/get.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/utils/storage.dart'; -class FavController extends GetxController {} +class FavController extends GetxController { + Rx favFolderData = FavFolderData().obs; + + Future queryFavFolder() async { + var res = await await UserHttp.userfavFolder( + pn: 1, + ps: 10, + mid: GStrorage.user.get(UserBoxKey.userMid), + ); + favFolderData.value = res['data']; + return res; + } +} diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart index 762fcf18..3f23f451 100644 --- a/lib/pages/fav/view.dart +++ b/lib/pages/fav/view.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/pages/fav/index.dart'; class FavPage extends StatefulWidget { const FavPage({super.key}); @@ -8,12 +11,64 @@ class FavPage extends StatefulWidget { } class _FavPageState extends State { + final FavController _favController = Get.put(FavController()); + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: false, - title: Text('我的收藏'), + title: const Text('我的收藏'), + ), + body: FutureBuilder( + future: _favController.queryFavFolder(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx( + () => ListView.builder( + itemCount: _favController.favFolderData.value.list!.length, + itemBuilder: (context, index) { + return ListTile( + onTap: () => Get.toNamed( + '/favDetail', + arguments: + _favController.favFolderData.value.list![index], + parameters: { + 'mediaId': _favController + .favFolderData.value.list![index].id + .toString(), + }, + ), + leading: const Icon(Icons.folder_special_outlined), + minLeadingWidth: 0, + title: Text(_favController + .favFolderData.value.list![index].title!), + subtitle: Text( + '${_favController.favFolderData.value.list![index].mediaCount}个内容', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize), + ), + ); + }, + ), + ); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return Text('请求中'); + } + }, ), ); } diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 25b219e7..7066cd9f 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -56,12 +56,12 @@ class MainController extends GetxController { // 'icon': const Icon(Icons.person_outline), // 'selectedIcon': const Icon(Icons.person), 'icon': const Icon( - CupertinoIcons.tray_full, - size: 21, + CupertinoIcons.folder, + size: 20, ), 'selectedIcon': const Icon( - CupertinoIcons.tray_full_fill, - size: 21, + CupertinoIcons.folder_fill, + size: 20, ), 'label': "媒体库", } diff --git a/lib/pages/media/controller.dart b/lib/pages/media/controller.dart index 0c0f31f8..f32ca4c3 100644 --- a/lib/pages/media/controller.dart +++ b/lib/pages/media/controller.dart @@ -10,12 +10,12 @@ class MediaController extends GetxController { { 'icon': Icons.file_download_outlined, 'title': '离线缓存', - 'onTap': () => Get.toNamed('/fav'), + 'onTap': () {}, }, { 'icon': Icons.history, 'title': '观看记录', - 'onTap': () => Get.toNamed('/fav'), + 'onTap': () {}, }, { 'icon': Icons.star_border, @@ -24,8 +24,8 @@ class MediaController extends GetxController { }, { 'icon': Icons.watch_later_outlined, - 'title': '稍后再看', - 'onTap': () => Get.toNamed('/fav'), + 'title': '稍候再看', + 'onTap': () => {}, }, ]; diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart index c4474a28..6557000c 100644 --- a/lib/pages/media/view.dart +++ b/lib/pages/media/view.dart @@ -56,8 +56,13 @@ class _MediaPageState extends State color: primary, ), ), + contentPadding: + const EdgeInsets.only(left: 15, top: 2, bottom: 2), minLeadingWidth: 0, - title: Text(i['title']), + title: Text( + i['title'], + style: const TextStyle(fontSize: 15), + ), ), ], favFolder() @@ -105,13 +110,11 @@ class _MediaPageState extends State ), ), ), - trailing: Padding( - padding: const EdgeInsets.only(right: 10), - child: Text( - '查看全部', - style: TextStyle( - fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.outline), + trailing: IconButton( + onPressed: () => _mediaController.queryFavFolder(), + icon: const Icon( + Icons.refresh, + size: 20, ), ), ), diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 1ddaf530..c7e7b831 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/video.dart'; @@ -26,6 +28,13 @@ class VideoIntroController extends GetxController { // up主粉丝数 Map userStat = {'follower': '-'}; + // 是否点赞 + RxBool hasLike = false.obs; + // 是否投币 + RxBool hasCoin = false.obs; + // 是否收藏 + RxBool hasFav = false.obs; + @override void onInit() { super.onInit(); @@ -57,6 +66,13 @@ class VideoIntroController extends GetxController { } // 获取到粉丝数再返回 await queryUserStat(); + // 获取点赞状态 + queryHasLikeVideo(); + // 获取投币状态 + queryHasCoinVideo(); + // 获取收藏状态 + queryHasFavVideo(); + return result; } @@ -67,4 +83,51 @@ class VideoIntroController extends GetxController { userStat = result['data']; } } + + // 获取点赞状态 + Future queryHasLikeVideo() async { + var result = await VideoHttp.hasLikeVideo(aid: aid); + // data num 被点赞标志 0:未点赞 1:已点赞 + hasLike.value = result["data"] == 1 ? true : false; + } + + // 获取投币状态 + Future queryHasCoinVideo() async { + var result = await VideoHttp.hasCoinVideo(aid: aid); + hasCoin.value = result["data"]['multiply'] == 0 ? false : true; + } + + // 获取收藏状态 + Future queryHasFavVideo() async { + var result = await VideoHttp.hasFavVideo(aid: aid); + hasFav.value = result["data"]['favoured']; + } + + // 一键三连 + + // (取消)点赞 + Future actionLikeVideo() async { + var result = await VideoHttp.likeVideo(aid: aid, type: !hasLike.value); + if (result['status']) { + hasLike.value = result["data"] == 1 ? true : false; + } else { + SmartDialog.showToast(result['msg']); + } + } + + // 投币 + Future actionCoinVideo() async { + print('投币'); + } + + // (取消)收藏 + Future actionFavVideo() async { + print('(取消)收藏'); + // var result = await VideoHttp.favVideo(aid: aid, type: true, ids: ''); + } + + // 分享视频 + Future actionShareVideo() async { + print('分享视频'); + } } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 4bef011b..f9027cee 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -4,6 +4,8 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/pages/fav/index.dart'; +import 'package:pilipala/pages/favDetail/index.dart'; import 'package:pilipala/pages/video/detail/widgets/expandable_section.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/stat/danmu.dart'; @@ -100,6 +102,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { /// 手动控制 late Animation? _manualAnimation; + final FavController _favController = Get.put(FavController()); + @override void initState() { super.initState(); @@ -113,6 +117,102 @@ class _VideoInfoState extends State with TickerProviderStateMixin { Tween(begin: 0.5, end: 1.5).animate(_manualController!); } + showFavBottomSheet() { + Get.bottomSheet( + useRootNavigator: true, + isScrollControlled: true, + Container( + height: 450, + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + AppBar( + toolbarHeight: 50, + automaticallyImplyLeading: false, + centerTitle: false, + elevation: 1, + title: Text( + '选择文件夹', + style: Theme.of(context).textTheme.titleMedium, + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('完成'), + ), + const SizedBox(width: 6), + ], + ), + Expanded( + child: Material( + child: FutureBuilder( + future: _favController.queryFavFolder(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx( + () => ListView.builder( + itemCount: _favController + .favFolderData.value.list!.length + + 1, + itemBuilder: (context, index) { + if (index == 0) { + return const SizedBox(height: 15); + } else { + return ListTile( + onTap: () {}, + dense: true, + leading: + const Icon(Icons.folder_special_outlined), + minLeadingWidth: 0, + title: Text(_favController.favFolderData.value + .list![index - 1].title!), + subtitle: Text( + '${_favController.favFolderData.value.list![index - 1].mediaCount}个内容', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .outline, + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize), + ), + trailing: Transform.scale( + scale: 0.9, + child: Checkbox( + value: false, + onChanged: (bool? checkValue) {}, + ), + ), + ); + } + }, + ), + ); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return Text('请求中'); + } + }, + ), + ), + ), + ], + ), + ), + persistent: false, + backgroundColor: Theme.of(context).bottomSheetTheme.backgroundColor, + ); + } + @override Widget build(BuildContext context) { return SliverPadding( @@ -293,7 +393,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ), ), const SizedBox(height: 5), - _actionGrid(context), + _actionGrid(context, widget.videoIntroController), ], ) : const Center(child: CircularProgressIndicator()), @@ -302,7 +402,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { } // 喜欢 投币 分享 - Widget _actionGrid(BuildContext context) { + Widget _actionGrid(BuildContext context, videoIntroController) { return LayoutBuilder(builder: (context, constraints) { return SizedBox( height: constraints.maxWidth / 5 * 0.8, @@ -312,39 +412,50 @@ class _VideoInfoState extends State with TickerProviderStateMixin { crossAxisCount: 5, childAspectRatio: 1.25, children: [ - ActionItem( - icon: const Icon(FontAwesomeIcons.thumbsUp), - onTap: () => {}, - selectStatus: false, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.like!.toString() - : '-'), + Obx( + () => ActionItem( + icon: const Icon(FontAwesomeIcons.thumbsUp), + selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), + onTap: () => videoIntroController.actionLikeVideo(), + selectStatus: videoIntroController.hasLike.value, + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.like!.toString() + : '-'), + ), ActionItem( icon: const Icon(FontAwesomeIcons.thumbsDown), + selectIcon: const Icon(FontAwesomeIcons.solidThumbsDown), onTap: () => {}, selectStatus: false, loadingStatus: widget.loadingStatus, text: '不喜欢'), - ActionItem( - icon: const Icon(FontAwesomeIcons.b), - onTap: () => {}, - selectStatus: false, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.coin!.toString() - : '-'), - ActionItem( - icon: const Icon(FontAwesomeIcons.star), - onTap: () => {}, - selectStatus: false, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.favorite!.toString() - : '-'), + Obx( + () => ActionItem( + icon: const Icon(FontAwesomeIcons.b), + selectIcon: const Icon(FontAwesomeIcons.b), + onTap: () => videoIntroController.actionCoinVideo(), + selectStatus: videoIntroController.hasCoin.value, + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.coin!.toString() + : '-'), + ), + Obx( + () => ActionItem( + icon: const Icon(FontAwesomeIcons.star), + selectIcon: const Icon(FontAwesomeIcons.star), + // onTap: () => videoIntroController.actionFavVideo(), + onTap: () => showFavBottomSheet(), + selectStatus: videoIntroController.hasFav.value, + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.favorite!.toString() + : '-'), + ), ActionItem( icon: const Icon(FontAwesomeIcons.shareFromSquare), - onTap: () => {}, + onTap: () => videoIntroController.actionShareVideo(), selectStatus: false, loadingStatus: widget.loadingStatus, text: !widget.loadingStatus @@ -359,6 +470,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { class ActionItem extends StatelessWidget { Icon? icon; + Icon? selectIcon; Function? onTap; bool? loadingStatus; String? text; @@ -367,6 +479,7 @@ class ActionItem extends StatelessWidget { ActionItem({ Key? key, this.icon, + this.selectIcon, this.onTap, this.loadingStatus, this.text, @@ -378,16 +491,17 @@ class ActionItem extends StatelessWidget { return Material( child: Ink( child: InkWell( - onTap: () {}, + onTap: () => onTap!(), borderRadius: StyleString.mdRadius, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon!.icon!, - size: 21, - color: selectStatus - ? Theme.of(context).primaryColor - : Theme.of(context).colorScheme.outline), + const SizedBox(height: 4), + selectStatus + ? Icon(selectIcon!.icon!, + size: 21, color: Theme.of(context).primaryColor) + : Icon(icon!.icon!, + size: 21, color: Theme.of(context).colorScheme.outline), const SizedBox(height: 4), AnimatedOpacity( opacity: loadingStatus! ? 0 : 1, From 4d85eedd7b440ad3e9dec78eef6bda27548df1c3 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 12 May 2023 10:12:15 +0800 Subject: [PATCH 27/30] =?UTF-8?q?feat:=20=E6=8E=A8=E8=8D=90=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=98=BE=E7=A4=BA=E3=80=8C=E5=B7=B2=E5=85=B3=E6=B3=A8?= =?UTF-8?q?=E3=80=8D=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/widgets/video_card_v.dart | 19 +++++++++++++++++++ lib/http/video.dart | 10 +++------- lib/models/model_rec_video_item.dart | 3 +++ lib/pages/home/controller.dart | 1 + 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 6e52b2ed..e0aa9ff4 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -133,6 +133,25 @@ class VideoContent extends StatelessWidget { ), ), const SizedBox(width: 4) + ] else if (videoItem.isFollowed == 1) ...[ + Container( + padding: const EdgeInsets.fromLTRB(3, 1, 3, 1), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primaryContainer + .withOpacity(0.6), + borderRadius: BorderRadius.circular(3)), + child: Text( + '已关注', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.labelSmall!.fontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + const SizedBox(width: 4) ], Expanded( child: LayoutBuilder(builder: diff --git a/lib/http/video.dart b/lib/http/video.dart index 128b6993..002bff22 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -1,7 +1,3 @@ -import 'dart:io'; - -import 'package:dio_cookie_manager/dio_cookie_manager.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/models/model_hot_video_item.dart'; @@ -19,7 +15,7 @@ class VideoHttp { var res = await Request().get( Api.recommendList, data: { - 'feed_version': 'V3', + 'feed_version': 'V4', 'ps': ps, 'fresh_idx': freshIdx, }, @@ -31,10 +27,10 @@ class VideoHttp { } return {'status': true, 'data': list}; } else { - return {'status': false, 'data': []}; + return {'status': false, 'data': [], 'msg': ''}; } } catch (err) { - return {'status': false, 'data': [], 'msg': err}; + return {'status': false, 'data': [], 'msg': err.toString()}; } } diff --git a/lib/models/model_rec_video_item.dart b/lib/models/model_rec_video_item.dart index 849def9f..b651aa00 100644 --- a/lib/models/model_rec_video_item.dart +++ b/lib/models/model_rec_video_item.dart @@ -13,6 +13,7 @@ class RecVideoItemModel { this.pubdate, this.owner, this.stat, + this.isFollowed, this.rcmdReason, }); @@ -27,6 +28,7 @@ class RecVideoItemModel { int? pubdate = -1; Owner? owner; Stat? stat; + int? isFollowed; RcmdReason? rcmdReason; RecVideoItemModel.fromJson(Map json) { @@ -41,6 +43,7 @@ class RecVideoItemModel { pubdate = json["pubdate"]; owner = Owner.fromJson(json["owner"]); stat = Stat.fromJson(json["stat"]); + isFollowed = json["is_followed"] ?? 0; rcmdReason = json["rcmd_reason"] != null ? RcmdReason.fromJson(json["rcmd_reason"]) : RcmdReason(content: ''); diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index 7182b134..c5606c79 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -25,6 +25,7 @@ class HomeController extends GetxController { freshIdx: _currentPage, ); if (res['status']) { + print('type: $type'); if (type == 'init') { videoList.value = res['data']; } else if (type == 'onRefresh') { From 598a293a09560e20d7df384723a76f1b38e9b97f Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 13 May 2023 23:49:39 +0800 Subject: [PATCH 28/30] =?UTF-8?q?feat:=20=E4=B8=80=E9=94=AE=E4=B8=89?= =?UTF-8?q?=E8=BF=9E=E3=80=81=E8=A7=86=E9=A2=91=E9=A1=B5(=E5=8F=96?= =?UTF-8?q?=E6=B6=88)=E6=94=B6=E8=97=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/images/logo/logo_big.png | Bin 0 -> 45678 bytes lib/common/widgets/video_card_h.dart | 9 +- lib/http/api.dart | 16 +- lib/http/init.dart | 3 +- lib/http/video.dart | 51 +++- lib/models/user/fav_folder.dart | 2 +- lib/pages/favDetail/view.dart | 38 ++- lib/pages/home/controller.dart | 1 - lib/pages/media/view.dart | 187 +++++++------ lib/pages/mine/controller.dart | 3 + lib/pages/mine/view.dart | 1 + .../video/detail/introduction/controller.dart | 115 +++++++- lib/pages/video/detail/introduction/view.dart | 262 ++++++++++-------- lib/pages/video/detail/view.dart | 20 +- lib/pages/webview/controller.dart | 10 +- pubspec.yaml | 1 + 16 files changed, 449 insertions(+), 270 deletions(-) create mode 100644 assets/images/logo/logo_big.png diff --git a/assets/images/logo/logo_big.png b/assets/images/logo/logo_big.png new file mode 100644 index 0000000000000000000000000000000000000000..62370832a6acde823990724fe3bf1dc7370d4c59 GIT binary patch literal 45678 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 literal 0 HcmV?d00001 diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index fd3c289c..1217e700 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -1,13 +1,13 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/constants.dart'; -import 'package:pilipala/common/widgets/stat/up.dart'; import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; // 视频卡片 - 水平布局 class VideoCardH extends StatelessWidget { + // ignore: prefer_typing_uninitialized_variables var videoItem; VideoCardH({Key? key, required this.videoItem}) : super(key: key); @@ -138,13 +138,6 @@ class VideoContent extends StatelessWidget { const SizedBox(height: 4), Row( children: [ - // Image.asset( - // 'assets/images/up_gray.png', - // width: 14, - // height: 12, - // ), - const UpTag(), - const SizedBox(width: 2), Text( videoItem.owner.name, style: TextStyle( diff --git a/lib/http/api.dart b/lib/http/api.dart index b056b7cd..94efb69a 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -55,7 +55,7 @@ class Api { // csrf str CSRF Token(位于cookie) Cookie方式必要 // https://api.bilibili.com/medialist/gateway/coll/resource/deal // https://api.bilibili.com/x/v3/fav/resource/deal - static const String favVideo = '/medialist/gateway/coll/resource/deal'; + static const String favVideo = '/x/v3/fav/resource/deal'; // 判断视频是否被收藏(双端)GET /// aid @@ -68,6 +68,20 @@ class Api { // bvid str 稿件bvid 必要(可选) avid与bvid任选一个 // csrf str CSRF Token(位于cookie) 必要 + // 一键三连 + // https://api.bilibili.com/x/web-interface/archive/like/triple + // aid num 稿件avid 必要(可选) avid与bvid任选一个 + // bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + // csrf str CSRF Token(位于cookie) 必要 + static const String oneThree = '/x/web-interface/archive/like/triple'; + + // 获取指定用户创建的所有收藏夹信息 + // 该接口也能查询目标内容id存在于那些收藏夹中 + // up_mid num 目标用户mid 必要 + // type num 目标内容属性 非必要 默认为全部 0:全部 2:视频稿件 + // rid num 目标 视频稿件avid + static const String videoInFolder = '/x/v3/fav/folder/created/list-all'; + // 视频详情页 相关视频 static const String relatedList = '/x/web-interface/archive/related'; diff --git a/lib/http/init.dart b/lib/http/init.dart index eca0a6d4..240251dd 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -138,13 +138,14 @@ class Request { /* * post请求 */ - post(url, {data, options, cancelToken, extra}) async { + post(url, {data, queryParameters, options, cancelToken, extra}) async { print('post-data: $data'); Response response; try { response = await dio.post( url, data: data, + queryParameters: queryParameters, options: options, cancelToken: cancelToken, ); diff --git a/lib/http/video.dart b/lib/http/video.dart index 002bff22..d01f6889 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -2,6 +2,7 @@ import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/models/model_hot_video_item.dart'; import 'package:pilipala/models/model_rec_video_item.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/video_detail_res.dart'; /// res.data['code'] == 0 请求正常返回结果 @@ -122,11 +123,26 @@ class VideoHttp { } // 一键三连 + static Future oneThree({required String aid}) async { + var res = await Request().post( + Api.oneThree, + queryParameters: { + 'aid': aid, + 'csrf': await Request.getCsrf(), + }, + ); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } + // (取消)点赞 static Future likeVideo({required String aid, required bool type}) async { var res = await Request().post( Api.likeVideo, - data: { + queryParameters: { 'aid': aid, 'like': type ? 1 : 2, 'csrf': await Request.getCsrf(), @@ -141,20 +157,33 @@ class VideoHttp { // (取消)收藏 static Future favVideo( - {required String aid, required bool type, required String ids}) async { - Map data = {'rid': aid, 'type': 2}; - // type true 添加收藏 false 取消收藏 - if (type) { - data['add_media_ids'] = ids; - } else { - data['del_media_ids'] = ids; - } - var res = await Request() - .post(Api.favVideo, data: {'aid': aid, 'like': type ? 1 : 2}); + {required String aid, + required bool type, + required String addIds, + required String delIds}) async { + var res = await Request().post(Api.favVideo, queryParameters: { + 'rid': aid, + 'type': 2, + 'add_media_ids': addIds, + 'del_media_ids': delIds, + 'csrf': await Request.getCsrf(), + }); if (res.data['code'] == 0) { return {'status': true, 'data': res.data['data']}; } else { return {'status': false, 'data': []}; } } + + // 查看视频被收藏在哪个文件夹 + static Future videoInFolder({required int mid, required String rid}) async { + var res = await Request() + .get(Api.videoInFolder, data: {'up_mid': mid, 'rid': rid}); + if (res.data['code'] == 0) { + FavFolderData data = FavFolderData.fromJson(res.data['data']); + return {'status': true, 'data': data}; + } else { + return {'status': false, 'data': []}; + } + } } diff --git a/lib/models/user/fav_folder.dart b/lib/models/user/fav_folder.dart index 0e0e61eb..6d3f9975 100644 --- a/lib/models/user/fav_folder.dart +++ b/lib/models/user/fav_folder.dart @@ -72,7 +72,7 @@ class FavFolderItemData { attr = json['attr']; title = json['title']; cover = json['cover']; - upper = Upper.fromJson(json['upper']); + upper = json['upper'] != null ? Upper.fromJson(json['upper']) : Upper(); coverType = json['cover_type']; intro = json['intro']; ctime = json['ctime']; diff --git a/lib/pages/favDetail/view.dart b/lib/pages/favDetail/view.dart index 1fb48886..e637c6d5 100644 --- a/lib/pages/favDetail/view.dart +++ b/lib/pages/favDetail/view.dart @@ -167,18 +167,27 @@ class _FavDetailPageState extends State { if (snapshot.connectionState == ConnectionState.done) { Map data = snapshot.data; if (data['status']) { - return Obx( - () => SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return FavVideoCardH( - videoItem: _favDetailController - .favDetailData.value.medias![index], - ); - }, - childCount: _favDetailController - .favDetailData.value.medias!.length), - ), - ); + if (_favDetailController.item!.mediaCount == 0) { + return const SliverToBoxAdapter( + child: SizedBox( + height: 300, + child: Center(child: Text('没有内容')), + ), + ); + } else { + return Obx( + () => SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return FavVideoCardH( + videoItem: _favDetailController + .favDetailData.value.medias![index], + ); + }, + childCount: _favDetailController + .favDetailData.value.medias!.length), + ), + ); + } } else { return HttpError( errMsg: data['msg'], @@ -187,8 +196,9 @@ class _FavDetailPageState extends State { } } else { return const SliverToBoxAdapter( - child: Center( - child: Text('加载中'), + child: SizedBox( + height: 300, + child: Center(child: Text('加载中')), ), ); } diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index c5606c79..7182b134 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -25,7 +25,6 @@ class HomeController extends GetxController { freshIdx: _currentPage, ); if (res['status']) { - print('type: $type'); if (type == 'init') { videoList.value = res['data']; } else if (type == 'onRefresh') { diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart index 6557000c..a18b1e3d 100644 --- a/lib/pages/media/view.dart +++ b/lib/pages/media/view.dart @@ -122,43 +122,56 @@ class _MediaPageState extends State SizedBox( width: double.infinity, height: 170, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - const SizedBox(width: 20), - FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - Map data = snapshot.data; - if (data['status']) { - return Obx(() => Row( - children: [ - if (_mediaController.favFolderData.value.list != - null) ...[ - for (FavFolderItemData i in _mediaController - .favFolderData.value.list!) ...[ - FavFolderItem(item: i), - const SizedBox(width: 14) - ] - ] - ], - )); - } else { - return SizedBox( - height: 160, - child: Center(child: Text(data['msg'])), - ); - } - } else { - // 骨架屏 - return SizedBox(); - } - }), - // for (var i in [1, 2, 3]) ...[const FavFolderItem()], - const SizedBox(width: 10) - ], - ), + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data; + if (data['status']) { + List favFolderList = + _mediaController.favFolderData.value.list!; + int favFolderCount = + _mediaController.favFolderData.value.count!; + bool flag = favFolderCount > favFolderList.length; + return Obx(() => ListView.builder( + itemCount: _mediaController + .favFolderData.value.list!.length + + (flag ? 1 : 0), + itemBuilder: (context, index) { + if (flag && index == favFolderList.length) { + return Padding( + padding: const EdgeInsets.only( + right: 14, bottom: 35), + child: Center( + child: IconButton( + onPressed: () => Get.toNamed('/fav'), + icon: Icon( + Icons.arrow_forward_ios, + size: 18, + color: Theme.of(context).primaryColor, + ), + ), + )); + } else { + return FavFolderItem( + item: _mediaController + .favFolderData.value.list![index], + index: index); + } + }, + scrollDirection: Axis.horizontal, + )); + } else { + return SizedBox( + height: 160, + child: Center(child: Text(data['msg'])), + ); + } + } else { + // 骨架屏 + return SizedBox(); + } + }), ), ], ); @@ -166,59 +179,63 @@ class _MediaPageState extends State } class FavFolderItem extends StatelessWidget { - FavFolderItem({super.key, this.item}); + FavFolderItem({super.key, this.item, this.index}); FavFolderItemData? item; + int? index; @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () => Get.toNamed('/favDetail', arguments: item, parameters: { - 'mediaId': item!.id.toString(), - }), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - Container( - width: 180, - height: 110, - margin: const EdgeInsets.only(bottom: 8), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Theme.of(context).colorScheme.onInverseSurface, - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.onInverseSurface, - offset: const Offset(4, -12), // 阴影与容器的距离 - blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。 - spreadRadius: 0.0, // 在应用模糊之前,框应该膨胀的量。 - ), - ], + return Container( + margin: EdgeInsets.only(left: index == 0 ? 20 : 0, right: 14), + child: GestureDetector( + onTap: () => Get.toNamed('/favDetail', arguments: item, parameters: { + 'mediaId': item!.id.toString(), + }), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Container( + width: 180, + height: 110, + margin: const EdgeInsets.only(bottom: 8), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Theme.of(context).colorScheme.onInverseSurface, + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.onInverseSurface, + offset: const Offset(4, -12), // 阴影与容器的距离 + blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。 + spreadRadius: 0.0, // 在应用模糊之前,框应该膨胀的量。 + ), + ], + ), + child: LayoutBuilder( + builder: (context, BoxConstraints box) { + return NetworkImgLayer( + src: item!.cover, + width: box.maxWidth, + height: box.maxHeight, + ); + }, + ), ), - child: LayoutBuilder( - builder: (context, BoxConstraints box) { - return NetworkImgLayer( - src: item!.cover, - width: box.maxWidth, - height: box.maxHeight, - ); - }, + Text( + ' ${item!.title}', + overflow: TextOverflow.fade, + maxLines: 1, ), - ), - Text( - ' ${item!.title}', - overflow: TextOverflow.fade, - maxLines: 1, - ), - Text( - ' 共${item!.mediaCount}条视频', - style: Theme.of(context) - .textTheme - .labelSmall! - .copyWith(color: Theme.of(context).colorScheme.outline), - ) - ], + Text( + ' 共${item!.mediaCount}条视频', + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: Theme.of(context).colorScheme.outline), + ) + ], + ), ), ); } diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index 93b0d1d0..db73fd6f 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -27,6 +27,9 @@ class MineController extends GetxController { } Future queryUserInfo() async { + if (user.get(UserBoxKey.userLogin) == null) { + return {'status': false}; + } var res = await UserHttp.userInfo(); if (res['status']) { if (res['data'].isLogin) { diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index 85e43ad0..e0a72d96 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -64,6 +64,7 @@ class _MinePageState extends State { future: _mineController.queryUserInfo(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { + print(snapshot.data); if (snapshot.data['status']) { return Obx(() => userInfoBuild()); } else { diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index c7e7b831..71616423 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -1,10 +1,13 @@ 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/user.dart'; import 'package:pilipala/http/video.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/pages/video/detail/controller.dart'; +import 'package:pilipala/utils/storage.dart'; class VideoIntroController extends GetxController { // 视频aid @@ -34,6 +37,11 @@ class VideoIntroController extends GetxController { RxBool hasCoin = false.obs; // 是否收藏 RxBool hasFav = false.obs; + Box user = GStrorage.user; + bool userLogin = false; + Rx favFolderData = FavFolderData().obs; + List addMediaIdsNew = []; + List delMediaIdsNew = []; @override void onInit() { @@ -51,6 +59,7 @@ class VideoIntroController extends GetxController { videoItem!['owner'] = args.owner; } } + userLogin = user.get(UserBoxKey.userLogin) != null; } // 获取视频简介 @@ -66,12 +75,14 @@ class VideoIntroController extends GetxController { } // 获取到粉丝数再返回 await queryUserStat(); - // 获取点赞状态 - queryHasLikeVideo(); - // 获取投币状态 - queryHasCoinVideo(); - // 获取收藏状态 - queryHasFavVideo(); + if (userLogin) { + // 获取点赞状态 + queryHasLikeVideo(); + // 获取投币状态 + queryHasCoinVideo(); + // 获取收藏状态 + queryHasFavVideo(); + } return result; } @@ -104,12 +115,54 @@ class VideoIntroController extends GetxController { } // 一键三连 + Future actionOneThree() async { + if (hasLike.value && hasCoin.value && hasFav.value) { + // 已点赞、投币、收藏 + SmartDialog.showToast('🙏 UP已经收到了~'); + return false; + } + SmartDialog.show( + useSystem: true, + animationType: SmartAnimationType.centerFade_otherSlide, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('提示'), + content: const Text('一键三连 给UP送温暖'), + actions: [ + TextButton( + onPressed: () => SmartDialog.dismiss(), + child: const Text('点错了')), + TextButton( + onPressed: () async { + var result = await VideoHttp.oneThree(aid: aid); + if (result['status']) { + hasLike.value = result["data"]["like"]; + hasCoin.value = result["data"]["coin"]; + hasFav.value = result["data"]["fav"]; + SmartDialog.showToast('三连成功 🎉'); + } else { + SmartDialog.showToast(result['msg']); + } + SmartDialog.dismiss(); + }, + child: const Text('确认'), + ) + ], + ); + }, + ); + } // (取消)点赞 Future actionLikeVideo() async { var result = await VideoHttp.likeVideo(aid: aid, type: !hasLike.value); if (result['status']) { hasLike.value = result["data"] == 1 ? true : false; + if (hasLike.value) { + SmartDialog.showToast('已点赞 👍'); + } else { + SmartDialog.showToast('取消赞'); + } } else { SmartDialog.showToast(result['msg']); } @@ -122,12 +175,58 @@ class VideoIntroController extends GetxController { // (取消)收藏 Future actionFavVideo() async { - print('(取消)收藏'); - // var result = await VideoHttp.favVideo(aid: aid, type: true, ids: ''); + try { + for (var i in favFolderData.value.list!.toList()) { + if (i.favState == 1) { + addMediaIdsNew.add(i.id); + } else { + delMediaIdsNew.add(i.id); + } + } + } catch (e) {} + var result = await VideoHttp.favVideo( + aid: aid, + type: true, + addIds: addMediaIdsNew.join(','), + delIds: delMediaIdsNew.join(',')); + if (result['status']) { + if (result['data']['prompt']) { + addMediaIdsNew = []; + delMediaIdsNew = []; + Get.back(); + // 重新获取收藏状态 + queryHasFavVideo(); + SmartDialog.showToast('✅ 操作成功'); + } + } } // 分享视频 Future actionShareVideo() async { print('分享视频'); } + + Future queryVideoInFolder() async { + var result = await VideoHttp.videoInFolder( + mid: user.get(UserBoxKey.userMid), rid: aid); + if (result['status']) { + favFolderData.value = result['data']; + } + return result; + } + + // 选择文件夹 + onChoose(bool checkValue, int index) { + List datalist = favFolderData.value.list!; + for (var i = 0; i < datalist.length; i++) { + if (i == index) { + datalist[i].favState = checkValue == true ? 1 : 0; + datalist[i].mediaCount = checkValue == true + ? datalist[i].mediaCount! + 1 + : datalist[i].mediaCount! - 1; + } + } + favFolderData.value.list = datalist; + favFolderData.refresh(); + } } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index f9027cee..843126ed 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -54,10 +54,7 @@ class _VideoIntroPanelState extends State if (snapshot.data['status']) { // 请求成功 // return _buildView(context, false, videoDetail); - return VideoInfo( - loadingStatus: false, - videoDetail: videoDetail, - videoIntroController: videoIntroController); + return VideoInfo(loadingStatus: false, videoDetail: videoDetail); } else { // 请求错误 return HttpError( @@ -66,10 +63,7 @@ class _VideoIntroPanelState extends State ); } } else { - return VideoInfo( - loadingStatus: true, - videoDetail: videoDetail, - videoIntroController: videoIntroController); + return VideoInfo(loadingStatus: true, videoDetail: videoDetail); } }, ); @@ -79,13 +73,8 @@ class _VideoIntroPanelState extends State class VideoInfo extends StatefulWidget { bool loadingStatus = false; VideoDetailData? videoDetail; - VideoIntroController? videoIntroController; - VideoInfo( - {Key? key, - required this.loadingStatus, - this.videoDetail, - this.videoIntroController}) + VideoInfo({Key? key, required this.loadingStatus, this.videoDetail}) : super(key: key); @override @@ -94,6 +83,8 @@ class VideoInfo extends StatefulWidget { class _VideoInfoState extends State with TickerProviderStateMixin { Map videoItem = Get.put(VideoIntroController()).videoItem!; + final VideoIntroController videoIntroController = + Get.put(VideoIntroController(), tag: Get.arguments['heroTag']); bool isExpand = false; /// 手动控制动画的控制器 @@ -137,7 +128,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ), actions: [ TextButton( - onPressed: () => Get.back(), + onPressed: () => videoIntroController.actionFavVideo(), child: const Text('完成'), ), const SizedBox(width: 6), @@ -146,30 +137,34 @@ class _VideoInfoState extends State with TickerProviderStateMixin { Expanded( child: Material( child: FutureBuilder( - future: _favController.queryFavFolder(), + future: videoIntroController.queryVideoInFolder(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { Map data = snapshot.data as Map; if (data['status']) { return Obx( () => ListView.builder( - itemCount: _favController + itemCount: videoIntroController .favFolderData.value.list!.length + 1, itemBuilder: (context, index) { if (index == 0) { - return const SizedBox(height: 15); + return const SizedBox(height: 10); } else { return ListTile( - onTap: () {}, + onTap: () => videoIntroController.onChoose( + videoIntroController.favFolderData.value + .list![index - 1].favState != + 1, + index - 1), dense: true, leading: const Icon(Icons.folder_special_outlined), minLeadingWidth: 0, - title: Text(_favController.favFolderData.value - .list![index - 1].title!), + title: Text(videoIntroController.favFolderData + .value.list![index - 1].title!), subtitle: Text( - '${_favController.favFolderData.value.list![index - 1].mediaCount}个内容', + '${videoIntroController.favFolderData.value.list![index - 1].mediaCount}个内容', style: TextStyle( color: Theme.of(context) .colorScheme @@ -182,8 +177,15 @@ class _VideoInfoState extends State with TickerProviderStateMixin { trailing: Transform.scale( scale: 0.9, child: Checkbox( - value: false, - onChanged: (bool? checkValue) {}, + value: videoIntroController + .favFolderData + .value + .list![index - 1] + .favState == + 1, + onChanged: (bool? checkValue) => + videoIntroController.onChoose( + checkValue!, index - 1), ), ), ); @@ -302,65 +304,6 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ], ), ), - const SizedBox(height: 12), - Row( - children: [ - NetworkImgLayer( - type: 'avatar', - src: !widget.loadingStatus - ? widget.videoDetail!.owner!.face - : videoItem['owner'].face, - width: 38, - height: 38, - fadeInDuration: Duration.zero, - fadeOutDuration: Duration.zero, - ), - const SizedBox(width: 14), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(!widget.loadingStatus - ? widget.videoDetail!.owner!.name - : videoItem['owner'].name), - // const SizedBox(width: 10), - Text( - widget.loadingStatus - ? '- 粉丝' - : '${Utils.numFormat(widget.videoIntroController!.userStat['follower'])}粉丝', - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .labelSmall! - .fontSize, - color: Theme.of(context).colorScheme.outline), - ), - ], - ), - const Spacer(), - AnimatedOpacity( - opacity: widget.loadingStatus ? 0 : 1, - duration: const Duration(milliseconds: 150), - child: SizedBox( - height: 36, - child: ElevatedButton( - onPressed: () {}, - child: Row( - children: const [ - Icon( - CupertinoIcons.plus, - size: 16, - ), - SizedBox(width: 4), - Text('关注'), - ], - ), - ), - ), - ), - const SizedBox(width: 4), - ], - ), - const SizedBox(height: 10), // 简介 默认收起 if (!widget.loadingStatus) ExpandedSection( @@ -392,8 +335,64 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ), ), ), - const SizedBox(height: 5), - _actionGrid(context, widget.videoIntroController), + const SizedBox(height: 8), + _actionGrid(context, videoIntroController), + Divider( + height: 26, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + Row( + children: [ + NetworkImgLayer( + type: 'avatar', + src: !widget.loadingStatus + ? widget.videoDetail!.owner!.face + : videoItem['owner'].face, + width: 38, + height: 38, + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, + ), + const SizedBox(width: 14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(!widget.loadingStatus + ? widget.videoDetail!.owner!.name + : videoItem['owner'].name), + // const SizedBox(width: 10), + Text( + widget.loadingStatus + ? '- 粉丝' + : '${Utils.numFormat(videoIntroController.userStat['follower'])}粉丝', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize, + color: Theme.of(context).colorScheme.outline), + ), + ], + ), + const Spacer(), + AnimatedOpacity( + opacity: widget.loadingStatus ? 0 : 1, + duration: const Duration(milliseconds: 150), + child: SizedBox( + height: 36, + child: ElevatedButton( + onPressed: () {}, + child: const Text('关注'), + ), + ), + ), + ], + ), + Divider( + height: 26, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + // const SizedBox(height: 10), ], ) : const Center(child: CircularProgressIndicator()), @@ -412,6 +411,29 @@ class _VideoInfoState extends State with TickerProviderStateMixin { crossAxisCount: 5, childAspectRatio: 1.25, children: [ + // ActionItem( + // icon: const Icon(FontAwesomeIcons.s), + // selectIcon: const Icon(FontAwesomeIcons.s), + // onTap: () => {}, + // selectStatus: true, + // loadingStatus: false, + // text: '三连', + // ), + // Column( + // children: [], + // ), + InkWell( + onTap: () => videoIntroController.actionOneThree(), + borderRadius: StyleString.mdRadius, + child: Padding( + padding: const EdgeInsets.all(12), + child: Image.asset( + 'assets/images/logo/logo_big.png', + width: 10, + height: 10, + ), + ), + ), Obx( () => ActionItem( icon: const Icon(FontAwesomeIcons.thumbsUp), @@ -423,13 +445,13 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ? widget.videoDetail!.stat!.like!.toString() : '-'), ), - ActionItem( - icon: const Icon(FontAwesomeIcons.thumbsDown), - selectIcon: const Icon(FontAwesomeIcons.solidThumbsDown), - onTap: () => {}, - selectStatus: false, - loadingStatus: widget.loadingStatus, - text: '不喜欢'), + // ActionItem( + // icon: const Icon(FontAwesomeIcons.thumbsDown), + // selectIcon: const Icon(FontAwesomeIcons.solidThumbsDown), + // onTap: () => {}, + // selectStatus: false, + // loadingStatus: widget.loadingStatus, + // text: '不喜欢'), Obx( () => ActionItem( icon: const Icon(FontAwesomeIcons.b), @@ -445,7 +467,6 @@ class _VideoInfoState extends State with TickerProviderStateMixin { () => ActionItem( icon: const Icon(FontAwesomeIcons.star), selectIcon: const Icon(FontAwesomeIcons.star), - // onTap: () => videoIntroController.actionFavVideo(), onTap: () => showFavBottomSheet(), selectStatus: videoIntroController.hasFav.value, loadingStatus: widget.loadingStatus, @@ -488,37 +509,32 @@ class ActionItem extends StatelessWidget { @override Widget build(BuildContext context) { - return Material( - child: Ink( - child: InkWell( - onTap: () => onTap!(), - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 4), - selectStatus - ? Icon(selectIcon!.icon!, - size: 21, color: Theme.of(context).primaryColor) - : Icon(icon!.icon!, - size: 21, color: Theme.of(context).colorScheme.outline), - const SizedBox(height: 4), - AnimatedOpacity( - opacity: loadingStatus! ? 0 : 1, - duration: const Duration(milliseconds: 200), - child: Text( - text!, - style: TextStyle( - color: selectStatus - ? Theme.of(context).primaryColor - : Theme.of(context).colorScheme.outline, - fontSize: - Theme.of(context).textTheme.labelSmall?.fontSize), - ), - ), - ], + return InkWell( + onTap: () => onTap!(), + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 4), + selectStatus + ? Icon(selectIcon!.icon!, + size: 21, color: Theme.of(context).primaryColor) + : Icon(icon!.icon!, + size: 21, color: Theme.of(context).colorScheme.outline), + const SizedBox(height: 4), + AnimatedOpacity( + opacity: loadingStatus! ? 0 : 1, + duration: const Duration(milliseconds: 200), + child: Text( + text ?? '', + style: TextStyle( + color: selectStatus + ? Theme.of(context).primaryColor + : Theme.of(context).colorScheme.outline, + fontSize: Theme.of(context).textTheme.labelSmall?.fontSize), + ), ), - ), + ], ), ); } diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 030ccc8e..3265ea24 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -114,23 +114,11 @@ class _VideoDetailPageState extends State { children: [ Builder( builder: (context) { - return CustomScrollView( - key: const PageStorageKey('简介'), + return const CustomScrollView( + key: PageStorageKey('简介'), slivers: [ - const VideoIntroPanel(), - SliverPadding( - padding: - const EdgeInsets.only(top: 8, bottom: 5), - sliver: SliverToBoxAdapter( - child: Divider( - height: 1, - color: Theme.of(context) - .dividerColor - .withOpacity(0.1), - ), - ), - ), - const RelatedVideoPanel(), + VideoIntroPanel(), + RelatedVideoPanel(), ], ); }, diff --git a/lib/pages/webview/controller.dart b/lib/pages/webview/controller.dart index eee80c02..9fe1b670 100644 --- a/lib/pages/webview/controller.dart +++ b/lib/pages/webview/controller.dart @@ -1,10 +1,13 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/http/user.dart'; +import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/utils/cookie.dart'; +import 'package:pilipala/utils/storage.dart'; import 'package:webview_cookie_manager/webview_cookie_manager.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -52,10 +55,15 @@ class WebviewController extends GetxController { await WebviewCookieManager().getCookies(HttpString.baseUrl); await SetCookie.onSet(cookies, HttpString.baseUrl); await SetCookie.onSet(apiCookies, HttpString.baseApiUrl); + await UserHttp.userInfo(); var result = await UserHttp.userInfo(); + print('网页登录: $result'); if (result['status'] && result['data'].isLogin) { SmartDialog.showToast('登录成功'); - Get.find().userInfo = result['data']; + Box user = GStrorage.user; + user.put(UserBoxKey.userLogin, true); + Get.find().userInfo.value = result['data']; + Get.find().queryRcmdFeed('onRefresh'); Get.back(); } } catch (e) { diff --git a/pubspec.yaml b/pubspec.yaml index a856f809..47c6ee49 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -113,6 +113,7 @@ flutter: assets: - assets/images/ - assets/images/lv/ + - assets/images/logo/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware From 7ae1716f448f836ba85b70de8fb0c50f45bbee5c Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 14 May 2023 00:29:00 +0800 Subject: [PATCH 29/30] =?UTF-8?q?feat:=20=E6=94=B6=E8=97=8F=E5=A4=B9?= =?UTF-8?q?=E5=8F=96=E6=B6=88=E6=94=B6=E8=97=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/video.dart | 9 +- lib/pages/favDetail/controller.dart | 21 +++ lib/pages/favDetail/view.dart | 15 +- .../favDetail/widget/fav_video_card.dart | 166 ++++++++++-------- .../video/detail/introduction/controller.dart | 1 - 5 files changed, 127 insertions(+), 85 deletions(-) diff --git a/lib/http/video.dart b/lib/http/video.dart index d01f6889..b1e4698c 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -157,15 +157,12 @@ class VideoHttp { // (取消)收藏 static Future favVideo( - {required String aid, - required bool type, - required String addIds, - required String delIds}) async { + {required String aid, String? addIds, String? delIds}) async { var res = await Request().post(Api.favVideo, queryParameters: { 'rid': aid, 'type': 2, - 'add_media_ids': addIds, - 'del_media_ids': delIds, + 'add_media_ids': addIds ?? '', + 'del_media_ids': delIds ?? '', 'csrf': await Request.getCsrf(), }); if (res.data['code'] == 0) { diff --git a/lib/pages/favDetail/controller.dart b/lib/pages/favDetail/controller.dart index 21303115..cc59676a 100644 --- a/lib/pages/favDetail/controller.dart +++ b/lib/pages/favDetail/controller.dart @@ -1,5 +1,7 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/user.dart'; +import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/user/fav_detail.dart'; import 'package:pilipala/models/user/fav_folder.dart'; @@ -26,4 +28,23 @@ class FavDetailController extends GetxController { favDetailData.value = res['data']; return res; } + + onCancelFav(int id) async { + var result = await VideoHttp.favVideo( + aid: id.toString(), addIds: '', delIds: mediaId.toString()); + if (result['status']) { + if (result['data']['prompt']) { + List dataList = favDetailData.value.medias!; + for (var i in dataList) { + if (i.id == id) { + dataList.remove(i); + break; + } + } + favDetailData.value.medias = dataList; + favDetailData.refresh(); + SmartDialog.showToast('取消收藏'); + } + } + } } diff --git a/lib/pages/favDetail/view.dart b/lib/pages/favDetail/view.dart index e637c6d5..7ee7d429 100644 --- a/lib/pages/favDetail/view.dart +++ b/lib/pages/favDetail/view.dart @@ -152,12 +152,15 @@ class _FavDetailPageState extends State { SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), - child: Text( - '共${_favDetailController.item!.mediaCount}条视频', - style: TextStyle( - fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.outline, - letterSpacing: 1), + child: Obx( + () => Text( + '共${_favDetailController.favDetailData.value.medias != null ? _favDetailController.favDetailData.value.medias!.length : '-'}条视频', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + letterSpacing: 1), + ), ), ), ), diff --git a/lib/pages/favDetail/widget/fav_video_card.dart b/lib/pages/favDetail/widget/fav_video_card.dart index 1cfc3c91..d4e9f6da 100644 --- a/lib/pages/favDetail/widget/fav_video_card.dart +++ b/lib/pages/favDetail/widget/fav_video_card.dart @@ -1,14 +1,18 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/constants.dart'; -import 'package:pilipala/common/widgets/stat/up.dart'; +import 'package:pilipala/common/widgets/stat/danmu.dart'; import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import '../controller.dart'; + // 收藏视频卡片 - 水平布局 class FavVideoCardH extends StatelessWidget { var videoItem; + final FavDetailController _favDetailController = + Get.put(FavDetailController()); FavVideoCardH({Key? key, required this.videoItem}) : super(key: key); @@ -16,75 +20,99 @@ class FavVideoCardH extends StatelessWidget { Widget build(BuildContext context) { int id = videoItem.id; String heroTag = Utils.makeHeroTag(id); - return InkWell( - onTap: () async { - await Future.delayed(const Duration(milliseconds: 200)); - Get.toNamed('/video?aid=$id', - arguments: {'videoItem': videoItem, 'heroTag': heroTag}); + return Dismissible( + movementDuration: const Duration(milliseconds: 300), + background: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.clear_all_rounded), + SizedBox(width: 6), + Text('取消收藏') + ], + )), + direction: DismissDirection.endToStart, + key: ValueKey(videoItem.id), + onDismissed: (DismissDirection direction) { + _favDetailController.onCancelFav(videoItem.id); + // widget.onDeleteNotice(); }, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(12, 5, 12, 5), - child: LayoutBuilder( - builder: (context, boxConstraints) { - double width = - (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; - return SizedBox( - height: width / StyleString.aspectRatio, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AspectRatio( - aspectRatio: StyleString.aspectRatio, - child: LayoutBuilder( - builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - double PR = MediaQuery.of(context).devicePixelRatio; - return Stack( - children: [ - Hero( - tag: heroTag, - child: NetworkImgLayer( - // src: videoItem['pic'] + - // '@${(maxWidth * 2).toInt()}w', - src: videoItem.pic + '@.webp', - width: maxWidth, - height: maxHeight, - ), - ), - // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), - Positioned( - right: 4, - bottom: 4, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 1, horizontal: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Colors.black54.withOpacity(0.4)), - child: Text( - Utils.timeFormat(videoItem.duration!), - style: const TextStyle( - fontSize: 11, color: Colors.white), + child: InkWell( + onTap: () async { + await Future.delayed(const Duration(milliseconds: 200)); + Get.toNamed('/video?aid=$id', + arguments: {'videoItem': videoItem, 'heroTag': heroTag}); + }, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 5, 12, 5), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double width = + (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; + return SizedBox( + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = + MediaQuery.of(context).devicePixelRatio; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + // src: videoItem['pic'] + + // '@${(maxWidth * 2).toInt()}w', + src: videoItem.pic + '@.webp', + width: maxWidth, + height: maxHeight, ), ), - ) - ], - ); - }, + // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), + Positioned( + right: 4, + bottom: 4, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 1, horizontal: 6), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(4), + color: + Colors.black54.withOpacity(0.4)), + child: Text( + Utils.timeFormat(videoItem.duration!), + style: const TextStyle( + fontSize: 11, color: Colors.white), + ), + ), + ) + ], + ); + }, + ), ), - ), - VideoContent(videoItem: videoItem) - ], - ), - ); - }, + VideoContent(videoItem: videoItem) + ], + ), + ); + }, + ), ), - ), - ], + ], + ), ), ); } @@ -112,7 +140,6 @@ class VideoContent extends StatelessWidget { overflow: TextOverflow.ellipsis, ), const Spacer(), - const SizedBox(height: 4), Text( videoItem.owner.name, style: TextStyle( @@ -127,12 +154,7 @@ class VideoContent extends StatelessWidget { view: videoItem.cntInfo['play'], ), const SizedBox(width: 8), - Text( - Utils.dateFormat(videoItem.pubdate!), - style: TextStyle( - fontSize: 11, - color: Theme.of(context).colorScheme.outline), - ) + StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku']) ], ), ], diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 71616423..faf7cfb6 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -186,7 +186,6 @@ class VideoIntroController extends GetxController { } catch (e) {} var result = await VideoHttp.favVideo( aid: aid, - type: true, addIds: addMediaIdsNew.join(','), delIds: delMediaIdsNew.join(',')); if (result['status']) { From 351cc0a850d479d6a996e65bc998c820d0f1d72d Mon Sep 17 00:00:00 2001 From: guozhigq Date: Mon, 15 May 2023 07:32:39 +0800 Subject: [PATCH 30/30] =?UTF-8?q?feat:=20=E8=AF=84=E8=AE=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 4 + lib/http/video.dart | 41 +++ lib/models/common/reply_type.dart | 46 +++ lib/models/video/reply/item.dart | 5 + lib/pages/video/detail/reply/controller.dart | 39 ++- lib/pages/video/detail/reply/view.dart | 267 ++++++++++++++---- .../detail/reply/widgets/reply_item.dart | 44 ++- 7 files changed, 380 insertions(+), 66 deletions(-) create mode 100644 lib/models/common/reply_type.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 94efb69a..b8c5c47c 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -91,6 +91,10 @@ class Api { // 楼中楼 static const String replyReplyList = '/x/v2/reply/reply'; + // 发表评论 + // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/comment/action.md + static const String replyAdd = '/x/v2/reply/add'; + // 用户(被)关注数、投稿数 // https://api.bilibili.com/x/relation/stat?vmid=697166795 static const String userStat = '/x/relation/stat'; diff --git a/lib/http/video.dart b/lib/http/video.dart index b1e4698c..1e6bbec9 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -1,5 +1,8 @@ +import 'dart:developer'; + import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; +import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/model_hot_video_item.dart'; import 'package:pilipala/models/model_rec_video_item.dart'; import 'package:pilipala/models/user/fav_folder.dart'; @@ -183,4 +186,42 @@ class VideoHttp { return {'status': false, 'data': []}; } } + + // 发表评论 replyAdd + + // type num 评论区类型代码 必要 类型代码见表 + // oid num 目标评论区id 必要 + // root num 根评论rpid 非必要 二级评论以上使用 + // parent num 父评论rpid 非必要 二级评论同根评论id 大于二级评论为要回复的评论id + // message str 发送评论内容 必要 最大1000字符 + // plat num 发送平台标识 非必要 1:web端 2:安卓客户端 3:ios客户端 4:wp客户端 + static Future replyAdd({ + required ReplyType type, + required int oid, + required String message, + int? root, + int? parent, + }) async { + if(message == ''){ + return {'status': false, 'data': [], 'msg': '请输入评论内容'}; + } + print('root:$root'); + print('parent: $parent'); + + var res = await Request() + .post(Api.replyAdd, queryParameters: { + 'type': type.index, + 'oid': oid, + 'root': root ?? '', + 'parent': parent == null || parent == 0 ? '' : parent, + 'message': message, + 'csrf': await Request.getCsrf(), + }); + log(res.toString()); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': []}; + } + } } diff --git a/lib/models/common/reply_type.dart b/lib/models/common/reply_type.dart new file mode 100644 index 00000000..a6e8bdb1 --- /dev/null +++ b/lib/models/common/reply_type.dart @@ -0,0 +1,46 @@ +enum ReplyType { + unset, + // 视频 + video, + // 话题 + topic, + // 活动 + activity, + // 小视频 + videoS, + // 小黑屋封禁信息 + blockMsg, + // 公告信息 + publicMsg, + // 直播活动 + liveActivity, + // 活动稿件 + activityFile, + // 直播公告 + livePublic, + // 相簿 + album, + // 专栏 + column, + // 票务 + ticket, + // 音频 + audio, + + // 点评 + comment, + // 动态 + dynamics, + // 播单 + playList, + // 音乐播单 + musicPlayList, + // 漫画 + comics1, + // 漫画 + comics2, + // 漫画 + comics3, + // 课程 + course, +} diff --git a/lib/models/video/reply/item.dart b/lib/models/video/reply/item.dart index a1e96561..3ae811ec 100644 --- a/lib/models/video/reply/item.dart +++ b/lib/models/video/reply/item.dart @@ -30,6 +30,7 @@ class ReplyItemModel { this.replyControl, this.isUp, this.isTop, + this.cardLabel, }); int? rpid; @@ -59,6 +60,7 @@ class ReplyItemModel { ReplyControl? replyControl; bool? isUp; bool? isTop = false; + List? cardLabel; ReplyItemModel.fromJson(Map json, upperMid, {isTopStatus = false}) { @@ -95,6 +97,9 @@ class ReplyItemModel { : ReplyControl.fromJson(json['reply_control']); isUp = upperMid.toString() == json['member']['mid']; isTop = isTopStatus; + cardLabel = json['card_label'] != null + ? json['card_label'].map((e) => e['text_content']).toList() + : []; } } diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index 6e813fc8..267b0354 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -1,8 +1,11 @@ import 'dart:developer'; import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/reply.dart'; +import 'package:pilipala/http/video.dart'; +import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/video/reply/data.dart'; import 'package:pilipala/models/video/reply/item.dart'; @@ -10,7 +13,7 @@ class VideoReplyController extends GetxController { VideoReplyController( this.aid, this.rpid, - this.level, + this.level ); final ScrollController scrollController = ScrollController(); // 视频aid 请求时使用的oid @@ -24,6 +27,15 @@ class VideoReplyController extends GetxController { int currentPage = 0; bool isLoadingMore = false; RxBool noMore = false.obs; + RxBool autoFocus = false.obs; + // 当前回复的回复 + ReplyItemModel? currentReplyItem; + // 回复来源 + String replySource = 'main'; + // 根评论 id 回复楼中楼回复使用 + int? rPid; + // 默认回复主楼 + String replyLevel = '0'; Future queryReplyList({type = 'init'}) async { isLoadingMore = true; @@ -77,4 +89,29 @@ class VideoReplyController extends GetxController { Future onLoad() async { queryReplyList(type: 'onLoad'); } + + wakeUpReply() { + autoFocus.value = true; + } + + // 发表评论 + Future submitReplyAdd() async { + print('replyLevel: $replyLevel'); + // print('rpid: $rpid'); + // print('currentReplyItem!.rpid: ${currentReplyItem!.rpid}'); + + + var result = await VideoHttp.replyAdd( + type: ReplyType.video, + oid: int.parse(aid!), + root: replyLevel == '0' ? 0 : replyLevel == '1' ? currentReplyItem!.rpid : rPid, + parent: replyLevel == '0' ? 0 : replyLevel == '1' ? currentReplyItem!.rpid : currentReplyItem!.rpid, + message: replyLevel == '2' ? ' 回复 @${currentReplyItem!.member!.uname!} : 2楼31' : '2楼31', + ); + if(result['status']){ + SmartDialog.showToast(result['data']['success_toast']); + }else{ + SmartDialog.showToast(result['message']); + } + } } diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index 0f8cae68..89aa090f 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/skeleton/video_reply.dart'; @@ -10,11 +13,11 @@ import 'widgets/reply_item.dart'; class VideoReplyPanel extends StatefulWidget { int oid; int rpid; - String level; + String? level; VideoReplyPanel({ this.oid = 0, this.rpid = 0, - this.level = '', + this.level, super.key, }); @@ -23,11 +26,16 @@ class VideoReplyPanel extends StatefulWidget { } class _VideoReplyPanelState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { late VideoReplyController _videoReplyController; + late AnimationController fabAnimationCtr; + late AnimationController replyAnimationCtl; // List? replyList; Future? _futureBuilderFuture; + bool _isFabVisible = true; + String replyLevel = '1'; + // 添加页面缓存 @override bool get wantKeepAlive => true; @@ -35,16 +43,28 @@ class _VideoReplyPanelState extends State @override void initState() { super.initState(); - if (widget.level == '2') { + replyLevel = widget.level ?? '1'; + if (widget.level != null && widget.level == '2') { _videoReplyController = Get.put( VideoReplyController( widget.oid.toString(), widget.rpid.toString(), '2'), tag: widget.rpid.toString()); + _videoReplyController.rPid = widget.rpid; } else { _videoReplyController = Get.put( VideoReplyController(Get.parameters['aid']!, '', '1'), tag: Get.arguments['heroTag']); } + // if(replyLevel != ''){ + // _videoReplyController.replyLevel = replyLevel; + // } + print( + '_videoReplyController.replyLevel: ${_videoReplyController.replyLevel}'); + + fabAnimationCtr = AnimationController( + vsync: this, duration: const Duration(milliseconds: 300)); + replyAnimationCtl = AnimationController( + vsync: this, duration: const Duration(milliseconds: 500)); _futureBuilderFuture = _videoReplyController.queryReplyList(); _videoReplyController.scrollController.addListener( @@ -56,10 +76,55 @@ class _VideoReplyPanelState extends State _videoReplyController.onLoad(); } } + + final ScrollDirection direction = + _videoReplyController.scrollController.position.userScrollDirection; + if (direction == ScrollDirection.forward) { + _showFab(); + } else if (direction == ScrollDirection.reverse) { + _hideFab(); + } }, ); } + void _showFab() { + if (!_isFabVisible) { + _isFabVisible = true; + fabAnimationCtr.forward(); + } + } + + void _hideFab() { + if (_isFabVisible) { + _isFabVisible = false; + fabAnimationCtr.reverse(); + } + } + + void _showReply(source, {ReplyItemModel? replyItem, replyLevel}) async { + // source main 直接回复 floor 楼中楼回复 + if (source == 'floor') { + _videoReplyController.currentReplyItem = replyItem; + _videoReplyController.replySource = source; + _videoReplyController.replyLevel = replyLevel ?? '1'; + } else { + _videoReplyController.replyLevel = '0'; + } + + replyAnimationCtl.forward(); + await Future.delayed(const Duration(microseconds: 100)); + _videoReplyController.wakeUpReply(); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + fabAnimationCtr.dispose(); + _videoReplyController.scrollController.dispose(); + } + @override Widget build(BuildContext context) { return RefreshIndicator( @@ -68,63 +133,147 @@ class _VideoReplyPanelState extends State _videoReplyController.currentPage = 0; return await _videoReplyController.queryReplyList(); }, - child: CustomScrollView( - controller: _videoReplyController.scrollController, - key: const PageStorageKey('评论'), - slivers: [ - FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - Map data = snapshot.data as Map; - if (data['status']) { - // 请求成功 - return Obx( - () => SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index == _videoReplyController.replyList.length) { - return Container( - padding: EdgeInsets.only( - bottom: - MediaQuery.of(context).padding.bottom), - height: - MediaQuery.of(context).padding.bottom + 60, - child: Center( - child: Obx(() => Text( - _videoReplyController.noMore.value - ? '没有更多了' - : '加载中')), - ), - ); - } else { - return ReplyItem( - replyItem: _videoReplyController.replyList[index], - ); - } - }, - childCount: _videoReplyController.replyList.length + 1, + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Stack( + children: [ + CustomScrollView( + controller: _videoReplyController.scrollController, + key: const PageStorageKey('评论'), + slivers: [ + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + // 请求成功 + return Obx( + () => SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == + _videoReplyController.replyList.length) { + return Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context) + .padding + .bottom), + height: + MediaQuery.of(context).padding.bottom + + 60, + child: Center( + child: Obx(() => Text( + _videoReplyController.noMore.value + ? '没有更多了' + : '加载中')), + ), + ); + } else { + return ReplyItem( + replyItem: _videoReplyController + .replyList[index], + weakUpReply: (replyItem, replyLevel) => + _showReply( + 'floor', + replyItem: replyItem, + replyLevel: replyLevel, + ), + replyLevel: replyLevel); + } + }, + childCount: + _videoReplyController.replyList.length + 1, + ), + ), + ); + } else { + // 请求错误 + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoReplySkeleton(); + }, childCount: 5), + ); + } + }, + ) + ], + ), + Positioned( + bottom: MediaQuery.of(context).padding.bottom + 14, + right: 14, + child: SlideTransition( + position: Tween( + // begin: const Offset(0, 2), + // 评论内容为空/不足一屏 + begin: const Offset(0, 0), + end: const Offset(0, 0), + ).animate(CurvedAnimation( + parent: fabAnimationCtr, + curve: Curves.easeInOut, + )), + child: FloatingActionButton( + heroTag: null, + onPressed: () => _showReply('main'), + tooltip: '发表评论', + child: const Icon(Icons.reply), + ), + ), + ), + Obx( + () => Positioned( + bottom: 0, + left: 0, + right: 0, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 2), + end: const Offset(0, 0), + ).animate(CurvedAnimation( + parent: replyAnimationCtl, + curve: Curves.easeInOut, + )), + child: Container( + height: 100 + MediaQuery.of(context).padding.bottom, + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom), + color: Theme.of(context).colorScheme.surfaceVariant, + child: Padding( + padding: const EdgeInsets.only(left: 14, right: 14), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Visibility( + visible: _videoReplyController.autoFocus.value, + child: const TextField( + autofocus: true, + maxLines: null, + decoration: InputDecoration( + hintText: "友善评论", border: InputBorder.none), + ), + ), + TextButton( + onPressed: () => + _videoReplyController.submitReplyAdd(), + child: const Text('发送'), + ) + ], ), ), - ); - } else { - // 请求错误 - return HttpError( - errMsg: data['msg'], - fn: () => setState(() {}), - ); - } - } else { - // 骨架屏 - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return const VideoReplySkeleton(); - }, childCount: 5), - ); - } - }, - ) - ], + ), + ), + ), + ), + ], + ), ), ); } diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index f920da95..d063cd5a 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -8,8 +8,10 @@ import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/utils/utils.dart'; class ReplyItem extends StatelessWidget { - ReplyItem({super.key, this.replyItem}); + ReplyItem({super.key, this.replyItem, this.weakUpReply, this.replyLevel}); ReplyItemModel? replyItem; + Function? weakUpReply; + String? replyLevel; @override Widget build(BuildContext context) { @@ -173,6 +175,7 @@ class ReplyItem extends StatelessWidget { ), // 操作区域 bottonAction(context, replyItem!.replyControl), + const SizedBox(height: 3), if (replyItem!.replies!.isNotEmpty) ...[ Padding( padding: const EdgeInsets.only(top: 2, bottom: 12), @@ -193,6 +196,15 @@ class ReplyItem extends StatelessWidget { return Row( children: [ const SizedBox(width: 48), + if (replyItem!.cardLabel!.isNotEmpty && + replyItem!.cardLabel!.contains('热评')) + Text( + '热评 • ', + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Theme.of(context).colorScheme.primary), + ), Text( Utils.dateFormat(replyItem!.ctime), style: Theme.of(context) @@ -210,10 +222,22 @@ class ReplyItem extends StatelessWidget { .copyWith(color: Theme.of(context).colorScheme.outline), ), const Spacer(), - if (replyControl!.isUpTop!) + if (replyItem!.upAction!.like!) Icon(Icons.favorite, color: Colors.red[400], size: 18), SizedBox( - height: 35, + height: 28, + width: 42, + child: TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + child: Text('回复', style: Theme.of(context) + .textTheme + .labelMedium), + onPressed: () => weakUpReply!(replyItem, replyLevel), + )), + SizedBox( + height: 32, child: TextButton( child: Row( children: [ @@ -314,6 +338,10 @@ class ReplyItemRow extends StatelessWidget { : TextOverflow.visible, maxLines: extraRow == 1 ? 2 : null, TextSpan( + recognizer: TapGestureRecognizer() + ..onTap = () { + replyReply(context); + }, children: [ TextSpan( text: replies![index].member.uname + ' ', @@ -395,7 +423,11 @@ InlineSpan buildContent(BuildContext context, content) { content.jumpUrl.isEmpty && content.vote.isEmpty && content.pictures.isEmpty) { - return TextSpan(text: content.message); + return TextSpan(text: content.message, + recognizer: TapGestureRecognizer() + ..onTap = ()=> { + print('点击') + },); } List spanChilds = []; // 匹配表情 @@ -635,7 +667,7 @@ class UpTag extends StatelessWidget { Color primary = Theme.of(context).colorScheme.primary; return Container( width: 24, - height: 15, + height: 14, decoration: BoxDecoration( borderRadius: BorderRadius.circular(3), color: tagText == 'UP' ? primary : null, @@ -645,7 +677,7 @@ class UpTag extends StatelessWidget { child: Text( tagText!, style: TextStyle( - fontSize: 10, + fontSize: 9, color: tagText == 'UP' ? Theme.of(context).colorScheme.onPrimary : primary,