mod: 视频详情、跳转Hero效果
This commit is contained in:
@ -15,6 +15,7 @@ class VideoCardV extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
String heroTag = Utils.makeHeroTag(videoItem.id);
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 0.8,
|
elevation: 0.8,
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
@ -26,7 +27,7 @@ class VideoCardV extends StatelessWidget {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
await Future.delayed(const Duration(milliseconds: 200));
|
await Future.delayed(const Duration(milliseconds: 200));
|
||||||
Get.toNamed('/video?aid=${videoItem.id}',
|
Get.toNamed('/video?aid=${videoItem.id}',
|
||||||
arguments: {'videoItem': videoItem});
|
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
print('长按');
|
print('长按');
|
||||||
@ -46,12 +47,15 @@ class VideoCardV extends StatelessWidget {
|
|||||||
double PR = MediaQuery.of(context).devicePixelRatio;
|
double PR = MediaQuery.of(context).devicePixelRatio;
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
NetworkImgLayer(
|
Hero(
|
||||||
// 指定图片尺寸
|
tag: heroTag,
|
||||||
// src: videoItem.pic + '@${(maxWidth * 2).toInt()}w',
|
child: NetworkImgLayer(
|
||||||
src: videoItem.pic + '@.webp',
|
// 指定图片尺寸
|
||||||
width: maxWidth,
|
// src: videoItem.pic + '@${(maxWidth * 2).toInt()}w',
|
||||||
height: maxHeight,
|
src: videoItem.pic + '@.webp',
|
||||||
|
width: maxWidth,
|
||||||
|
height: maxHeight,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
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 {
|
Future onLoad() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
queryRcmdFeed('onLoad');
|
queryRcmdFeed('onLoad');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ class VideoDetailController extends GetxController {
|
|||||||
// 请求状态
|
// 请求状态
|
||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
|
|
||||||
|
String heroTag = '';
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
@ -24,6 +25,7 @@ class VideoDetailController extends GetxController {
|
|||||||
videoItem['pic'] = args.pic;
|
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:flutter/material.dart';
|
||||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
import 'package:pilipala/pages/video/detail/controller.dart';
|
import 'package:pilipala/pages/video/detail/controller.dart';
|
||||||
|
import 'package:pilipala/pages/video/detail/introduction/index.dart';
|
||||||
|
|
||||||
class VideoDetailPage extends StatefulWidget {
|
class VideoDetailPage extends StatefulWidget {
|
||||||
const VideoDetailPage({Key? key}) : super(key: key);
|
const VideoDetailPage({Key? key}) : super(key: key);
|
||||||
@ -52,10 +53,13 @@ class _VideoDetailPageState extends State<VideoDetailPage> {
|
|||||||
double maxWidth = boxConstraints.maxWidth;
|
double maxWidth = boxConstraints.maxWidth;
|
||||||
double maxHeight = boxConstraints.maxHeight;
|
double maxHeight = boxConstraints.maxHeight;
|
||||||
double PR = MediaQuery.of(context).devicePixelRatio;
|
double PR = MediaQuery.of(context).devicePixelRatio;
|
||||||
return NetworkImgLayer(
|
return Hero(
|
||||||
src: videoDetailController.videoItem['pic'],
|
tag: videoDetailController.heroTag,
|
||||||
width: maxWidth,
|
child: NetworkImgLayer(
|
||||||
height: maxHeight,
|
src: videoDetailController.videoItem['pic'],
|
||||||
|
width: maxWidth,
|
||||||
|
height: maxHeight,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -112,10 +116,7 @@ class _VideoDetailPageState extends State<VideoDetailPage> {
|
|||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
|
||||||
context),
|
context),
|
||||||
),
|
),
|
||||||
// const VideoIntroPanel(),
|
const VideoIntroPanel(),
|
||||||
const SliverToBoxAdapter(
|
|
||||||
child: Text('简介'),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
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:io';
|
||||||
|
import 'dart:math';
|
||||||
import 'package:get/get_utils/get_utils.dart';
|
import 'package:get/get_utils/get_utils.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
@ -130,4 +131,8 @@ class Utils {
|
|||||||
}
|
}
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String makeHeroTag(v) {
|
||||||
|
return v.toString() + Random().nextInt(9999).toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user