feat: 收藏夹

This commit is contained in:
guozhigq
2023-05-11 00:14:00 +08:00
parent e612e60361
commit ea674c4b4a
15 changed files with 481 additions and 71 deletions

View File

@ -26,4 +26,8 @@ class Api {
// 获取当前用户状态 // 获取当前用户状态
static const String userStatOwner = '/x/web-interface/nav/stat'; static const String userStatOwner = '/x/web-interface/nav/stat';
// 收藏夹
// https://api.bilibili.com/x/v3/fav/folder/created/list?pn=1&ps=10&up_mid=17340771
static const String userFavFolder = '/x/v3/fav/folder/created/list';
} }

View File

@ -1,5 +1,6 @@
import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart'; import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/models/user/stat.dart'; import 'package:pilipala/models/user/stat.dart';
@ -29,7 +30,26 @@ class UserHttp {
UserStat data = UserStat.fromJson(res.data['data']); UserStat data = UserStat.fromJson(res.data['data']);
return {'status': true, 'data': data}; return {'status': true, 'data': data};
} else { } else {
return {'status': false}; return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
// 收藏夹
static Future<dynamic> userfavFolder({
required int pn,
required int ps,
required int mid,
}) async {
var res = await Request().get(Api.userFavFolder, data: {
'pn': pn,
'ps': ps,
'up_mid': mid,
});
if (res.data['code'] == 0) {
FavFolderData data = FavFolderData.fromJson(res.data['data']);
return {'status': true, 'data': data};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
} }
} }
} }

View File

@ -0,0 +1,108 @@
class FavFolderData {
FavFolderData({
this.count,
this.list,
this.hasMore,
});
int? count;
List<FavFolderItemData>? list;
bool? hasMore;
FavFolderData.fromJson(Map<String, dynamic> json) {
count = json['count'];
list = json['list'] != null
? json['list']
.map<FavFolderItemData>((e) => FavFolderItemData.fromJson(e))
.toList()
: [FavFolderItemData()];
hasMore = json['has_more'];
}
}
class FavFolderItemData {
FavFolderItemData({
this.id,
this.fid,
this.mid,
this.attr,
this.title,
this.cover,
this.upper,
this.coverType,
this.intro,
this.ctime,
this.mtime,
this.state,
this.favState,
this.mediaCount,
this.viewCount,
this.vt,
this.playSwitch,
this.type,
this.link,
this.bvid,
});
int? id;
int? fid;
int? mid;
int? attr;
String? title;
String? cover;
Upper? upper;
int? coverType;
String? intro;
int? ctime;
int? mtime;
int? state;
int? favState;
int? mediaCount;
int? viewCount;
int? vt;
int? playSwitch;
int? type;
String? link;
String? bvid;
FavFolderItemData.fromJson(Map<String, dynamic> json) {
id = json['id'];
fid = json['fid'];
mid = json['mid'];
attr = json['attr'];
title = json['title'];
cover = json['cover'];
upper = Upper.fromJson(json['upper']);
coverType = json['cover_type'];
intro = json['intro'];
ctime = json['ctime'];
mtime = json['mtime'];
state = json['state'];
favState = json['fav_state'];
mediaCount = json['media_count'];
viewCount = json['view_count'];
vt = json['vt'];
playSwitch = json['play_switch'];
type = json['type'];
link = json['link'];
bvid = json['bvid'];
}
}
class Upper {
Upper({
this.mid,
this.name,
this.face,
});
int? mid;
String? name;
String? face;
Upper.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
name = json['name'];
face = json['face'];
}
}

View File

@ -0,0 +1,3 @@
import 'package:get/get.dart';
class FavController extends GetxController {}

4
lib/pages/fav/index.dart Normal file
View File

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

20
lib/pages/fav/view.dart Normal file
View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
class FavPage extends StatefulWidget {
const FavPage({super.key});
@override
State<FavPage> createState() => _FavPageState();
}
class _FavPageState extends State<FavPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: Text('我的收藏'),
),
);
}
}

View File

@ -2,6 +2,8 @@ import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:pilipala/pages/mine/view.dart';
class HomeAppBar extends StatelessWidget { class HomeAppBar extends StatelessWidget {
const HomeAppBar({super.key}); const HomeAppBar({super.key});
@ -42,6 +44,12 @@ class HomeAppBar extends StatelessWidget {
// onPressed: () {}, // onPressed: () {},
// icon: const Icon(CupertinoIcons.bell, size: 22), // icon: const Icon(CupertinoIcons.bell, size: 22),
// ), // ),
IconButton(
onPressed: () {
Get.bottomSheet(const MinePage());
},
icon: const Icon(CupertinoIcons.person, size: 22),
),
const SizedBox(width: 10) const SizedBox(width: 10)
], ],
elevation: 0, elevation: 0,

View File

@ -5,26 +5,26 @@ 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/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/mine/view.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class MainController extends GetxController { class MainController extends GetxController {
List<Widget> pages = <Widget>[ List<Widget> pages = <Widget>[
const HomePage(), const HomePage(),
const HotPage(), const HotPage(),
const MinePage(), const MediaPage(),
]; ];
RxList navigationBars = [ RxList navigationBars = [
{ {
// 'icon': const Icon(Icons.home_outlined), // 'icon': const Icon(Icons.home_outlined),
// 'selectedIcon': const Icon(Icons.home), // 'selectedIcon': const Icon(Icons.home),
'icon': const Icon( 'icon': const Icon(
CupertinoIcons.house, CupertinoIcons.square_favorites_alt,
size: 18, size: 21,
), ),
'selectedIcon': const Icon( 'selectedIcon': const Icon(
CupertinoIcons.house_fill, CupertinoIcons.square_favorites_alt_fill,
size: 18, size: 21,
), ),
'label': "推荐", 'label': "推荐",
}, },
@ -41,46 +41,57 @@ class MainController extends GetxController {
), ),
'label': "热门", 'label': "热门",
}, },
// {
// 'icon': const Icon(
// CupertinoIcons.person,
// size: 21,
// ),
// 'selectedIcon': const Icon(
// CupertinoIcons.person_fill,
// size: 21,
// ),
// 'label': "我的",
// },
{ {
// 'icon': const Icon(Icons.person_outline), // 'icon': const Icon(Icons.person_outline),
// 'selectedIcon': const Icon(Icons.person), // 'selectedIcon': const Icon(Icons.person),
'icon': const Icon( 'icon': const Icon(
CupertinoIcons.person, CupertinoIcons.tray_full,
size: 21, size: 21,
), ),
'selectedIcon': const Icon( 'selectedIcon': const Icon(
CupertinoIcons.person_fill, CupertinoIcons.tray_full_fill,
size: 21, size: 21,
), ),
'label': "我的", 'label': "媒体库",
} }
].obs; ].obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
readuUserFace(); // readuUserFace();
} }
// 设置头像 // 设置头像
readuUserFace() async { // readuUserFace() async {
Box user = GStrorage.user; // Box user = GStrorage.user;
if (user.get(UserBoxKey.userFace) != null) { // if (user.get(UserBoxKey.userFace) != null) {
navigationBars.last['icon'] = // navigationBars.last['icon'] =
navigationBars.last['selectedIcon'] = NetworkImgLayer( // navigationBars.last['selectedIcon'] = NetworkImgLayer(
width: 25, // width: 25,
height: 25, // height: 25,
type: 'avatar', // type: 'avatar',
src: user.get(UserBoxKey.userFace), // src: user.get(UserBoxKey.userFace),
); // );
navigationBars.last['label'] = ''; // navigationBars.last['label'] = '我';
} // }
} // }
// 重置 // 重置
resetLast() { // resetLast() {
navigationBars.last['icon'] = const Icon(Icons.person_outline); // navigationBars.last['icon'] = const Icon(Icons.person_outline);
navigationBars.last['selectedIcon'] = const Icon(Icons.person); // navigationBars.last['selectedIcon'] = const Icon(Icons.person);
navigationBars.last['label'] = '我的'; // navigationBars.last['label'] = '我的';
} // }
} }

View File

@ -20,7 +20,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
late AnimationController? _animationController; late AnimationController? _animationController;
late Animation<double>? _fadeAnimation; late Animation<double>? _fadeAnimation;
late Animation<double>? _slideAnimation; late Animation<double>? _slideAnimation;
int selectedIndex = 2; int selectedIndex = 0;
int? _lastSelectTime; //上次点击时间 int? _lastSelectTime; //上次点击时间
@override @override

View File

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/utils/storage.dart';
class MediaController extends GetxController {
Rx<FavFolderData> favFolderData = FavFolderData().obs;
List list = [
{
'icon': Icons.file_download_outlined,
'title': '离线缓存',
'onTap': () => Get.toNamed('/fav'),
},
{
'icon': Icons.history,
'title': '观看记录',
'onTap': () => Get.toNamed('/fav'),
},
{
'icon': Icons.star_border,
'title': '我的收藏',
'onTap': () => Get.toNamed('/fav'),
},
{
'icon': Icons.watch_later_outlined,
'title': '稍后再看',
'onTap': () => Get.toNamed('/fav'),
},
];
Future<dynamic> queryFavFolder() async {
var res = await await UserHttp.userfavFolder(
pn: 1,
ps: 5,
mid: GStrorage.user.get(UserBoxKey.userMid),
);
favFolderData.value = res['data'];
return res;
}
}

View File

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

214
lib/pages/media/view.dart Normal file
View File

@ -0,0 +1,214 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/media/index.dart';
class MediaPage extends StatefulWidget {
const MediaPage({super.key});
@override
State<MediaPage> createState() => _MediaPageState();
}
class _MediaPageState extends State<MediaPage>
with AutomaticKeepAliveClientMixin {
final MediaController _mediaController = Get.put(MediaController());
Future? _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_futureBuilderFuture = _mediaController.queryFavFolder();
}
@override
Widget build(BuildContext context) {
Color primary = Theme.of(context).colorScheme.primary;
return Scaffold(
appBar: AppBar(toolbarHeight: 30),
body: Column(
children: [
ListTile(
leading: null,
title: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text(
'媒体库',
style: TextStyle(
fontSize: Theme.of(context).textTheme.titleLarge!.fontSize,
),
),
),
),
for (var i in _mediaController.list) ...[
ListTile(
onTap: () => i['onTap'](),
leading: Padding(
padding: const EdgeInsets.only(left: 15),
child: Icon(
i['icon'],
color: primary,
),
),
minLeadingWidth: 0,
title: Text(i['title']),
),
],
favFolder()
],
),
);
}
Widget favFolder() {
return Column(
children: [
Divider(
height: 35,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
ListTile(
onTap: () {},
leading: null,
dense: true,
title: Padding(
padding: const EdgeInsets.only(left: 10),
child: Obx(
() => Text.rich(
TextSpan(
children: [
TextSpan(
text: '收藏夹 ',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.titleMedium!.fontSize,
),
),
if (_mediaController.favFolderData.value.count != null)
TextSpan(
text: _mediaController.favFolderData.value.count
.toString(),
style: TextStyle(
fontSize:
Theme.of(context).textTheme.titleSmall!.fontSize,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
),
),
trailing: Padding(
padding: const EdgeInsets.only(right: 10),
child: Text(
'查看全部',
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline),
),
),
),
// const SizedBox(height: 10),
SizedBox(
width: double.infinity,
height: 170,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
const SizedBox(width: 20),
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
if (data['status']) {
return Obx(() => Row(
children: [
if (_mediaController.favFolderData.value.list !=
null) ...[
for (FavFolderItemData i in _mediaController
.favFolderData.value.list!) ...[
FavFolderItem(item: i),
const SizedBox(width: 14)
]
]
],
));
} else {
return SizedBox(
height: 160,
child: Center(child: Text(data['msg'])),
);
}
} else {
// 骨架屏
return SizedBox();
}
}),
// for (var i in [1, 2, 3]) ...[const FavFolderItem()],
const SizedBox(width: 10)
],
),
),
],
);
}
}
class FavFolderItem extends StatelessWidget {
FavFolderItem({super.key, this.item});
FavFolderItemData? item;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
Container(
width: 110 * 16 / 9,
height: 110,
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
color: Theme.of(context).colorScheme.onInverseSurface,
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.onInverseSurface,
offset: const Offset(4, -12), // 阴影与容器的距离
blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。
spreadRadius: 0.0, // 在应用模糊之前,框应该膨胀的量。
),
],
),
child: LayoutBuilder(
builder: (context, BoxConstraints box) {
return NetworkImgLayer(
src: item!.cover,
width: box.maxWidth,
height: box.maxHeight,
);
},
),
),
Text(
' ${item!.title}',
overflow: TextOverflow.fade,
maxLines: 1,
),
Text(
'${item!.mediaCount}条视频',
style: Theme.of(context)
.textTheme
.labelSmall!
.copyWith(color: Theme.of(context).colorScheme.outline),
)
],
);
}
}

View File

@ -36,7 +36,7 @@ class MineController extends GetxController {
user.put(UserBoxKey.userMid, res['data'].mid); user.put(UserBoxKey.userMid, res['data'].mid);
user.put(UserBoxKey.userLogin, true); user.put(UserBoxKey.userLogin, true);
userLogin.value = true; userLogin.value = true;
Get.find<MainController>().readuUserFace(); // Get.find<MainController>().readuUserFace();
} else { } else {
resetUserInfo(); resetUserInfo();
} }
@ -64,6 +64,6 @@ class MineController extends GetxController {
await user.delete(UserBoxKey.userMid); await user.delete(UserBoxKey.userMid);
await user.delete(UserBoxKey.userLogin); await user.delete(UserBoxKey.userLogin);
userLogin.value = false; userLogin.value = false;
Get.find<MainController>().resetLast(); // Get.find<MainController>().resetLast();
} }
} }

View File

@ -19,6 +19,11 @@ class _MinePageState extends State<MinePage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: false,
scrolledUnderElevation: 0,
elevation: 0,
toolbarHeight: kTextTabBarHeight + 20,
backgroundColor: Colors.transparent,
title: null, title: null,
actions: [ actions: [
IconButton( IconButton(
@ -54,6 +59,7 @@ class _MinePageState extends State<MinePage> {
height: constraint.maxHeight, height: constraint.maxHeight,
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 10),
FutureBuilder( FutureBuilder(
future: _mineController.queryUserInfo(), future: _mineController.queryUserInfo(),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -69,45 +75,6 @@ class _MinePageState extends State<MinePage> {
}, },
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.only(left: 12, right: 12),
child: LayoutBuilder(
builder: (context, constraints) {
return SizedBox(
height: constraints.maxWidth / 4 * 0.8,
child: GridView.count(
primary: false,
padding: const EdgeInsets.all(0),
crossAxisCount: 4,
childAspectRatio: 1.25,
children: <Widget>[
ActionItem(
icon:
const Icon(CupertinoIcons.cloud_download),
onTap: () => {},
text: '离线缓存',
),
ActionItem(
icon: const Icon(CupertinoIcons.time),
onTap: () => {},
text: '历史记录',
),
ActionItem(
icon: const Icon(CupertinoIcons.star),
onTap: () => {},
text: '我的收藏',
),
ActionItem(
icon: const Icon(CupertinoIcons.film),
onTap: () => {},
text: '稍后再看',
),
],
),
);
},
),
),
], ],
), ),
), ),

View File

@ -1,10 +1,12 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/pages/fav/index.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/webview/index.dart'; import 'package:pilipala/pages/webview/index.dart';
import 'package:pilipala/pages/setting/index.dart'; import 'package:pilipala/pages/setting/index.dart';
import 'package:pilipala/pages/media/index.dart';
class Routes { class Routes {
static final List<GetPage> getPages = [ static final List<GetPage> getPages = [
@ -20,5 +22,9 @@ class Routes {
GetPage(name: '/webview', page: () => const WebviewPage()), GetPage(name: '/webview', page: () => const WebviewPage()),
// 设置 // 设置
GetPage(name: '/setting', page: () => const SettingPage()), GetPage(name: '/setting', page: () => const SettingPage()),
//
GetPage(name: '/media', page: () => const MediaPage()),
//
GetPage(name: '/fav', page: () => const FavPage()),
]; ];
} }