mod: 视频详情、跳转Hero效果
This commit is contained in:
@ -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,
|
||||
|
||||
32
lib/http/video.dart
Normal file
32
lib/http/video.dart
Normal file
@ -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;
|
||||
// }
|
||||
}
|
||||
524
lib/models/video_detail_res.dart
Normal file
524
lib/models/video_detail_res.dart
Normal file
@ -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<String, dynamic> json) {
|
||||
code = json["code"];
|
||||
message = json["message"];
|
||||
ttl = json["ttl"];
|
||||
data = json["data"] == null ? null : VideoDetailData.fromJson(json["data"]);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
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>? descV2;
|
||||
int? state;
|
||||
int? duration;
|
||||
Map<String, int>? rights;
|
||||
Owner? owner;
|
||||
Stat? stat;
|
||||
String? videoDynamic;
|
||||
int? cid;
|
||||
Dimension? dimension;
|
||||
dynamic premiere;
|
||||
int? teenageMode;
|
||||
bool? isChargeableSeason;
|
||||
bool? isStory;
|
||||
bool? noCache;
|
||||
List<Page>? 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<String, dynamic> 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<DescV2>.from(json["desc_v2"]!.map((e) => DescV2.fromJson(e)));
|
||||
state = json["state"];
|
||||
duration = json["duration"];
|
||||
rights =
|
||||
Map.from(json["rights"]!).map((k, v) => MapEntry<String, int>(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<Page>.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<String, dynamic> 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<dynamic>.from(descV2!.map((e) => e.toJson())),
|
||||
"state": state,
|
||||
"duration": duration,
|
||||
"rights":
|
||||
Map.from(rights!).map((k, v) => MapEntry<String, dynamic>(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<dynamic>.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<String, dynamic> json) {
|
||||
rawText = json["raw_text"];
|
||||
type = json["type"];
|
||||
bizId = json["biz_id"];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
|
||||
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<String, dynamic> json) {
|
||||
width = json["width"];
|
||||
height = json["height"];
|
||||
rotate = json["rotate"];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
|
||||
data["width"] = width;
|
||||
data["height"] = height;
|
||||
data["rotate"] = rotate;
|
||||
data["data"] = data;
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class HonorReply {
|
||||
List<Honor>? honor;
|
||||
|
||||
HonorReply({
|
||||
this.honor,
|
||||
});
|
||||
|
||||
fromRawJson(String str) => HonorReply.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
HonorReply.fromJson(Map<String, dynamic> json) {
|
||||
honor = json["honor"] == null
|
||||
? []
|
||||
: List<Honor>.from(json["honor"]!.map((x) => Honor.fromJson(x)));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
|
||||
data["honor"] =
|
||||
honor == null ? [] : List<dynamic>.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<String, dynamic> json) {
|
||||
aid = json["aid"];
|
||||
type = json["type"];
|
||||
desc = json["desc"];
|
||||
weeklyRecommendNum = json["weekly_recommend_num"];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
|
||||
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<String, dynamic> json) {
|
||||
mid = json["mid"];
|
||||
name = json["name"];
|
||||
face = json["face"];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
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<String, dynamic> 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<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
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<String, dynamic> 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<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
|
||||
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<dynamic>? list;
|
||||
|
||||
Subtitle({
|
||||
this.allowSubmit,
|
||||
this.list,
|
||||
});
|
||||
|
||||
fromRawJson(String str) => Subtitle.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
Subtitle.fromJson(Map<String, dynamic> json) {
|
||||
allowSubmit = json["allow_submit"];
|
||||
list = json["list"] == null
|
||||
? []
|
||||
: List<dynamic>.from(json["list"]!.map((x) => x));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
|
||||
data["allow_submit"] = allowSubmit;
|
||||
data["list"] = list == null ? [] : List<dynamic>.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<String, dynamic> json) {
|
||||
urlImageAniCut = json["url_image_ani_cut"];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {"url_image_ani_cut": urlImageAniCut};
|
||||
}
|
||||
|
||||
class Label {}
|
||||
@ -47,7 +47,6 @@ class HomeController extends GetxController {
|
||||
|
||||
// 上拉加载
|
||||
Future onLoad() async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
queryRcmdFeed('onLoad');
|
||||
}
|
||||
|
||||
|
||||
@ -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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
47
lib/pages/video/detail/introduction/controller.dart
Normal file
47
lib/pages/video/detail/introduction/controller.dart
Normal file
@ -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<VideoDetailData> 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;
|
||||
}
|
||||
}
|
||||
4
lib/pages/video/detail/introduction/index.dart
Normal file
4
lib/pages/video/detail/introduction/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library video_detail_introduction;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
471
lib/pages/video/detail/introduction/view.dart
Normal file
471
lib/pages/video/detail/introduction/view.dart
Normal file
@ -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<VideoIntroPanel> createState() => _VideoIntroPanelState();
|
||||
}
|
||||
|
||||
class _VideoIntroPanelState extends State<VideoIntroPanel> {
|
||||
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<String>('简介'),
|
||||
// slivers: <Widget>[
|
||||
// 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<VideoInfo> createState() => _VideoInfoState();
|
||||
}
|
||||
|
||||
class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
Map videoItem = Get.put(VideoIntroController()).videoItem!;
|
||||
bool isExpand = false;
|
||||
|
||||
/// 手动控制动画的控制器
|
||||
late AnimationController? _manualController;
|
||||
|
||||
/// 手动控制
|
||||
late Animation<double>? _manualAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
/// 不设置重复,使用代码控制进度,动画时间1秒
|
||||
_manualController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
);
|
||||
_manualAnimation =
|
||||
Tween<double>(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: <Widget>[
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -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<VideoDetailPage> {
|
||||
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<VideoDetailPage> {
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
|
||||
context),
|
||||
),
|
||||
// const VideoIntroPanel(),
|
||||
const SliverToBoxAdapter(
|
||||
child: Text('简介'),
|
||||
)
|
||||
const VideoIntroPanel(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
83
lib/pages/video/detail/widgets/expandable_section.dart
Normal file
83
lib/pages/video/detail/widgets/expandable_section.dart
Normal file
@ -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<ExpandedSection>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController expandController;
|
||||
late Animation<double> 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<double> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user