feat: 新增动态页面

This commit is contained in:
guozhigq
2023-06-24 23:47:05 +08:00
parent 7174eef890
commit 714cbb2d0f
10 changed files with 844 additions and 12 deletions

BIN
assets/images/play.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -135,6 +135,8 @@ class Api {
// https://api.bilibili.com/x/polymer/web-dynamic/v1/portal // https://api.bilibili.com/x/polymer/web-dynamic/v1/portal
// 关注的up动态 // 关注的up动态
// https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all
static const String followDynamic = '/x/polymer/web-dynamic/v1/feed/all';
// 获取稍后再看 // 获取稍后再看
static const String seeYouLater = '/x/v2/history/toview'; static const String seeYouLater = '/x/v2/history/toview';

29
lib/http/dynamics.dart Normal file
View File

@ -0,0 +1,29 @@
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/dynamics/result.dart';
class DynamicsHttp {
static Future followDynamic({
String? type,
int? page,
String? offset,
}) async {
var res = await Request().get(Api.followDynamic, data: {
'type': type ?? 'all',
'page': page ?? 1,
'offest': page == 1 ? '' : offset,
'features': 'itemOpusStyle'
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': DynamicsDataModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
}
}

View File

@ -0,0 +1,415 @@
class DynamicsDataModel {
DynamicsDataModel({
this.hasMore,
this.items,
this.offset,
});
bool? hasMore;
List<DynamicItemModel>? items;
String? offset;
DynamicsDataModel.fromJson(Map<String, dynamic> json) {
hasMore = json['has_more'];
items = json['items']
.map<DynamicItemModel>((e) => DynamicItemModel.fromJson(e))
.toList();
offset = json['offset'];
}
}
// 单个动态
class DynamicItemModel {
DynamicItemModel({
// this.basic,
this.idStr,
this.modules,
this.orig,
this.type,
this.visible,
});
// Map? basic;
String? idStr;
ItemModulesModel? modules;
ItemOrigModel? orig;
String? type;
bool? visible;
DynamicItemModel.fromJson(Map<String, dynamic> json) {
// basic = json['basic'];
idStr = json['id_str'];
modules = ItemModulesModel.fromJson(json['modules']);
orig = json['orig'] != null ? ItemOrigModel.fromJson(json['orig']) : null;
type = json['type'];
visible = json['visible'];
}
}
class ItemOrigModel {
ItemOrigModel({
this.basic,
this.isStr,
this.modules,
this.type,
this.visible,
});
Map? basic;
String? isStr;
ItemModulesModel? modules;
String? type;
bool? visible;
ItemOrigModel.fromJson(Map<String, dynamic> json) {
basic = json['basic'];
isStr = json['is_str'];
modules = ItemModulesModel.fromJson(json['modules']);
type = json['type'];
visible = json['visible'];
}
}
// 单个动态详情
class ItemModulesModel {
ItemModulesModel({
this.moduleAuthor,
this.moduleDynamic,
// this.moduleInter,
this.moduleStat,
});
ModuleAuthorModel? moduleAuthor;
ModuleDynamicModel? moduleDynamic;
// ModuleInterModel? moduleInter;
ModuleStatModel? moduleStat;
ItemModulesModel.fromJson(Map<String, dynamic> json) {
moduleAuthor = json['module_author'] != null
? ModuleAuthorModel.fromJson(json['module_author'])
: null;
moduleDynamic = json['module_dynamic'] != null
? ModuleDynamicModel.fromJson(json['module_dynamic'])
: null;
// moduleInter = ModuleInterModel.fromJson(json['module_interaction']);
moduleStat = json['module_stat'] != null
? ModuleStatModel.fromJson(json['module_stat'])
: null;
}
}
// 单个动态详情 - 作者信息
class ModuleAuthorModel {
ModuleAuthorModel({
// this.avatar,
// this.decorate,
this.face,
this.following,
this.jumpUrl,
this.label,
this.mid,
this.name,
// this.officialVerify,
// this.pandant,
this.pubAction,
// this.pubLocationText,
this.pubTime,
this.pubTs,
this.type,
// this.vip,
});
String? face;
bool? following;
String? jumpUrl;
String? label;
int? mid;
String? name;
String? pubAction;
String? pubTime;
int? pubTs;
String? type;
ModuleAuthorModel.fromJson(Map<String, dynamic> json) {
face = json['face'];
following = json['following'];
jumpUrl = json['jump_url'];
label = json['label'];
mid = json['mid'];
name = json['name'];
pubAction = json['pub_action'];
pubTime = json['pub_time'];
pubTs = json['pub_ts'];
type = json['type'];
}
}
// 单个动态详情 - 动态信息
class ModuleDynamicModel {
ModuleDynamicModel({
// this.additional,
this.desc,
this.major,
this.topic,
});
String? additional;
DynamicDescModel? desc;
DynamicMajorModel? major;
Map? topic;
ModuleDynamicModel.fromJson(Map<String, dynamic> json) {
desc =
json['desc'] != null ? DynamicDescModel.fromJson(json['desc']) : null;
if (json['major'] != null) {
major = DynamicMajorModel.fromJson(json['major']);
}
topic = json['topic'];
}
}
// 单个动态详情 - 评论?信息
// class ModuleInterModel {
// ModuleInterModel({
// });
// ModuleInterModel.fromJson(Map<String, dynamic> json) {
// }
// }
class DynamicDescModel {
DynamicDescModel({
this.richTextNode,
this.text,
});
List? richTextNode;
String? text;
DynamicDescModel.fromJson(Map<String, dynamic> json) {
richTextNode = json['rich_text_nodes'];
text = json['text'];
}
}
//
class DynamicMajorModel {
DynamicMajorModel({
this.archive,
this.draw,
this.type,
});
DynamicArchiveModel? archive;
DynamicDrawModel? draw;
DynamicOpusModel? opus;
// MAJOR_TYPE_DRAW 图片
// MAJOR_TYPE_ARCHIVE 视频
// MAJOR_TYPE_OPUS 图文/文章
String? type;
DynamicMajorModel.fromJson(Map<String, dynamic> json) {
archive = json['archive'] != null
? DynamicArchiveModel.fromJson(json['archive'])
: null;
draw =
json['draw'] != null ? DynamicDrawModel.fromJson(json['draw']) : null;
opus =
json['opus'] != null ? DynamicOpusModel.fromJson(json['opus']) : null;
type = json['type'];
}
}
class DynamicArchiveModel {
DynamicArchiveModel({
this.aid,
this.badge,
this.bvid,
this.cover,
this.desc,
this.disablePreview,
this.durationText,
this.jumpUrl,
this.stat,
this.title,
this.type,
});
String? aid;
Map? badge;
String? bvid;
String? cover;
String? desc;
int? disablePreview;
String? durationText;
String? jumpUrl;
Stat? stat;
String? title;
int? type;
DynamicArchiveModel.fromJson(Map<String, dynamic> json) {
aid = json['aid'];
badge = json['badge'];
bvid = json['bvid'];
cover = json['cover'];
disablePreview = json['disable_preview'];
durationText = json['duration_text'];
jumpUrl = json['jump_url'];
stat = Stat.fromJson(json['stat']);
title = json['title'];
type = json['type'];
}
}
class DynamicDrawModel {
DynamicDrawModel({
this.id,
this.item,
});
int? id;
List<DynamicDrawItemModel>? item;
DynamicDrawModel.fromJson(Map<String, dynamic> json) {
id = json['id'];
// ignore: prefer_null_aware_operators
item = json['item'] != null
? json['item']
.map<DynamicDrawItemModel>((e) => DynamicDrawItemModel.fromJson(e))
.toList()
: null;
}
}
class DynamicOpusModel {
DynamicOpusModel({
this.jumpUrl,
this.pics,
this.summary,
this.title,
});
String? jumpUrl;
List? pics;
Map? summary;
String? title;
DynamicOpusModel.fromJson(Map<String, dynamic> json) {
jumpUrl = json['jump_url'];
pics = json['pics'];
summary = json['summary'];
title = json['title'];
}
}
class OpusPicsModel {
OpusPicsModel({
this.width,
this.height,
this.size,
this.src,
});
int? width;
int? height;
int? size;
String? src;
OpusPicsModel.fromJson(Map<String, dynamic> json) {
width = json['width'];
height = json['height'];
size = json['size'];
src = json['src'];
}
}
class DynamicDrawItemModel {
DynamicDrawItemModel({
this.height,
this.size,
this.src,
this.tags,
this.width,
});
int? height;
int? size;
String? src;
List? tags;
int? width;
DynamicDrawItemModel.fromJson(Map<String, dynamic> json) {
height = json['height'];
size = json['size'];
src = json['src'];
tags = json['tags'];
width = json['width'];
}
}
// 动态状态 转发、评论、点赞
class ModuleStatModel {
ModuleStatModel({
this.comment,
this.forward,
this.like,
});
Comment? comment;
Map? forward;
Like? like;
ModuleStatModel.fromJson(Map<String, dynamic> json) {
comment = Comment.fromJson(json['comment']);
forward = json['forward'];
like = Like.fromJson(json['like']);
}
}
// 动态状态 评论
class Comment {
Comment({
this.count,
this.forbidden,
});
String? count;
bool? forbidden;
Comment.fromJson(Map<String, dynamic> json) {
count = json['count'].toString();
forbidden = json['forbidden'];
}
}
// 动态状态 点赞
class Like {
Like({
this.count,
this.forbidden,
this.status,
});
String? count;
bool? forbidden;
bool? status;
Like.fromJson(Map<String, dynamic> json) {
count = json['count'].toString();
forbidden = json['forbidden'];
status = json['status'];
}
}
class Stat {
Stat({
this.danmaku,
this.play,
});
String? danmaku;
String? play;
Stat.fromJson(Map<String, dynamic> json) {
danmaku = json['danmaku'];
play = json['play'];
}
}

View File

@ -0,0 +1,27 @@
import 'package:get/get.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/models/dynamics/result.dart';
class DynamicsController extends GetxController {
int page = 1;
String reqType = 'all';
String? offset;
RxList<DynamicItemModel>? dynamicsList = [DynamicItemModel()].obs;
Future queryFollowDynamic({type = 'init'}) async {
var res = await DynamicsHttp.followDynamic(
page: page,
type: reqType,
offset: offset,
);
if (res['status']) {
if (type == 'init') {
dynamicsList!.value = res['data'].items;
} else {
dynamicsList!.addAll(res['data'].items);
}
offset = res['data'].offset;
}
return res;
}
}

View File

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

View File

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/models/dynamics/result.dart';
import 'controller.dart';
import 'widgets/dynamic_panel.dart';
class DynamicsPage extends StatefulWidget {
const DynamicsPage({super.key});
@override
State<DynamicsPage> createState() => _DynamicsPageState();
}
class _DynamicsPageState extends State<DynamicsPage> {
DynamicsController _dynamicsController = Get.put(DynamicsController());
Future? _futureBuilderFuture;
@override
void initState() {
super.initState();
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('动态'),
actions: [
IconButton(onPressed: () {}, icon: const Icon(Icons.more_horiz)),
const SizedBox(width: 10)
],
),
body: FutureBuilder(
future: _dynamicsController.queryFollowDynamic(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
if (data['status']) {
List<DynamicItemModel> list = _dynamicsController.dynamicsList!;
return Obx(
() => ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: (BuildContext context, index) {
return DynamicPanel(item: list[index]);
},
),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
);
}
} else {
// 骨架屏
// return SliverList(
// delegate: SliverChildBuilderDelegate((context, index) {
// return const VideoCardHSkeleton();
// }, childCount: 10),
// );
return Text('加载中');
}
},
),
);
}
}

View File

@ -0,0 +1,272 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pilipala/common/constants.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/dynamics/result.dart';
class DynamicPanel extends StatelessWidget {
DynamicItemModel? item;
DynamicPanel({this.item, Key? key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 8,
color: Theme.of(context).dividerColor.withOpacity(0.05),
),
),
),
child: Material(
elevation: 0,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
child: InkWell(
onTap: () {},
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
author(item, context),
if (item!.modules!.moduleDynamic!.desc != null)
content(item, context),
Padding(
padding: EdgeInsets.zero,
// padding: const EdgeInsets.only(left: 15, right: 15),
child: forWard(item, context),
),
action(item, context),
],
),
),
),
);
}
Widget author(item, context) {
return Container(
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
child: Row(
children: [
NetworkImgLayer(
width: 40,
height: 40,
type: 'avatar',
src: item.modules.moduleAuthor.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.modules.moduleAuthor.name),
Text(
item.modules.moduleAuthor.pubTime +
(item.modules.moduleAuthor.pubAction != ''
? ' - ${item.modules.moduleAuthor.pubAction}'
: ''),
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize),
)
],
)
],
),
);
}
// 内容
Widget content(item, context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(15, 0, 15, 10),
child: Text(
item!.modules!.moduleDynamic!.desc!.text!,
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
);
}
// 转发
Widget forWard(item, context, {floor = 1}) {
switch (item.type) {
// 图文
case 'DYNAMIC_TYPE_DRAW':
return const Text('DYNAMIC_TYPE_DRAW');
// 视频
case 'DYNAMIC_TYPE_AV':
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (floor == 2) ...[
Text('@' + item.modules.moduleAuthor.name),
const SizedBox(height: 8),
],
GestureDetector(
onTap: () {},
child: LayoutBuilder(builder: (context, box) {
double width = box.maxWidth;
return Stack(
children: [
NetworkImgLayer(
type: floor == 1 ? 'emote' : null,
width: width,
height: width / StyleString.aspectRatio,
src: item.modules.moduleDynamic.major.archive.cover,
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.fromLTRB(12, 22, 10, 15),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black87,
],
tileMode: TileMode.mirror,
),
borderRadius: floor == 1
? null
: const BorderRadius.all(Radius.circular(6))),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
children: [
Text(
item.modules.moduleDynamic.major.archive
.durationText,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Colors.white),
),
const SizedBox(width: 10),
Text(
item.modules.moduleDynamic.major.archive
.stat.play,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Colors.white),
),
const SizedBox(width: 10),
Text(
item.modules.moduleDynamic.major.archive
.stat.danmaku,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Colors.white),
)
],
),
Image.asset(
'assets/images/play.png',
width: 70,
height: 70,
),
],
),
)),
],
);
}),
),
const SizedBox(height: 6),
Padding(
padding: floor == 1
? const EdgeInsets.only(left: 12, right: 12)
: EdgeInsets.zero,
child: Text(
item.modules.moduleDynamic.major.archive.title,
maxLines: 1,
style: const TextStyle(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 4),
],
);
// 文章
case 'DYNAMIC_TYPE_ARTICLE':
return const Text('DYNAMIC_TYPE_ARTICLE');
// 转发
case 'DYNAMIC_TYPE_FORWARD':
// return const Text('DYNAMIC_TYPE_FORWARD');
switch (item.orig.type) {
// 递归
case 'DYNAMIC_TYPE_AV':
return Container(
padding: const EdgeInsets.only(
left: 15, top: 10, right: 15, bottom: 10),
color: Theme.of(context).dividerColor.withOpacity(0.08),
child: forWard(item.orig, context, floor: 2),
);
default:
return const Text('渲染出错了');
}
// 直播
case 'DYNAMIC_TYPE_LIVE_RCMD':
return const Text('DYNAMIC_TYPE_LIVE_RCMD');
// 合集
case 'DYNAMIC_TYPE_UGC_SEASON':
return const Text('DYNAMIC_TYPE_UGC_SEASON');
default:
return const Text('渲染出错了');
}
}
// 操作栏
Widget action(item, context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton.icon(
onPressed: () {},
icon: const Icon(
FontAwesomeIcons.shareFromSquare,
size: 16,
),
label: const Text('转发'),
),
TextButton.icon(
onPressed: () {},
icon: const Icon(
FontAwesomeIcons.comment,
size: 16,
),
label: Text(item.modules.moduleStat.comment.count),
),
TextButton.icon(
onPressed: () {},
icon: const Icon(
FontAwesomeIcons.thumbsUp,
size: 16,
),
label: Text(item.modules.moduleStat.like.count),
)
],
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/view.dart'; import 'package:pilipala/pages/home/view.dart';
import 'package:pilipala/pages/hot/view.dart'; import 'package:pilipala/pages/hot/view.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
@ -13,6 +14,7 @@ class MainController extends GetxController {
List<Widget> pages = <Widget>[ List<Widget> pages = <Widget>[
const HomePage(), const HomePage(),
const HotPage(), const HotPage(),
const DynamicsPage(),
const MediaPage(), const MediaPage(),
]; ];
RxList navigationBars = [ RxList navigationBars = [
@ -42,17 +44,17 @@ class MainController extends GetxController {
), ),
'label': "热门", 'label': "热门",
}, },
// { {
// 'icon': const Icon( 'icon': const Icon(
// CupertinoIcons.person, CupertinoIcons.camera_on_rectangle,
// size: 21, size: 21,
// ), ),
// 'selectedIcon': const Icon( 'selectedIcon': const Icon(
// CupertinoIcons.person_fill, CupertinoIcons.camera_on_rectangle_fill,
// size: 21, size: 21,
// ), ),
// 'label': "我的", 'label': "动态",
// }, },
{ {
// 'icon': const Icon(Icons.person_outline), // 'icon': const Icon(Icons.person_outline),
// 'selectedIcon': const Icon(Icons.person), // 'selectedIcon': const Icon(Icons.person),

View File

@ -1,4 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/pages/dynamics/view.dart';
import 'package:pilipala/pages/fav/index.dart'; import 'package:pilipala/pages/fav/index.dart';
import 'package:pilipala/pages/favDetail/index.dart'; import 'package:pilipala/pages/favDetail/index.dart';
import 'package:pilipala/pages/history/index.dart'; import 'package:pilipala/pages/history/index.dart';
@ -47,6 +48,8 @@ class Routes {
// 搜索页面 // 搜索页面
GetPage(name: '/search', page: () => const SearchPage()), GetPage(name: '/search', page: () => const SearchPage()),
// 搜索结果 // 搜索结果
GetPage(name: '/searchResult', page: () => const SearchResultPage()) GetPage(name: '/searchResult', page: () => const SearchResultPage()),
//
GetPage(name: '/dynamics', page: () => const DynamicsPage())
]; ];
} }