diff --git a/lib/http/api.dart b/lib/http/api.dart index 32abb1b64..80a3882d7 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -26,4 +26,8 @@ class Api { // 获取当前用户状态 static const String userStatOwner = '/x/web-interface/nav/stat'; + + // 收藏夹 + // https://api.bilibili.com/x/v3/fav/folder/created/list?pn=1&ps=10&up_mid=17340771 + static const String userFavFolder = '/x/v3/fav/folder/created/list'; } diff --git a/lib/http/user.dart b/lib/http/user.dart index 8f8881ab2..bf23d3eed 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -1,5 +1,6 @@ import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/stat.dart'; @@ -29,7 +30,26 @@ class UserHttp { UserStat data = UserStat.fromJson(res.data['data']); return {'status': true, 'data': data}; } else { - return {'status': false}; + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } + + // 收藏夹 + static Future userfavFolder({ + required int pn, + required int ps, + required int mid, + }) async { + var res = await Request().get(Api.userFavFolder, data: { + 'pn': pn, + 'ps': ps, + 'up_mid': mid, + }); + if (res.data['code'] == 0) { + FavFolderData data = FavFolderData.fromJson(res.data['data']); + return {'status': true, 'data': data}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; } } } diff --git a/lib/models/user/fav_folder.dart b/lib/models/user/fav_folder.dart new file mode 100644 index 000000000..0e0e61ebd --- /dev/null +++ b/lib/models/user/fav_folder.dart @@ -0,0 +1,108 @@ +class FavFolderData { + FavFolderData({ + this.count, + this.list, + this.hasMore, + }); + + int? count; + List? list; + bool? hasMore; + + FavFolderData.fromJson(Map json) { + count = json['count']; + list = json['list'] != null + ? json['list'] + .map((e) => FavFolderItemData.fromJson(e)) + .toList() + : [FavFolderItemData()]; + hasMore = json['has_more']; + } +} + +class FavFolderItemData { + FavFolderItemData({ + this.id, + this.fid, + this.mid, + this.attr, + this.title, + this.cover, + this.upper, + this.coverType, + this.intro, + this.ctime, + this.mtime, + this.state, + this.favState, + this.mediaCount, + this.viewCount, + this.vt, + this.playSwitch, + this.type, + this.link, + this.bvid, + }); + + int? id; + int? fid; + int? mid; + int? attr; + String? title; + String? cover; + Upper? upper; + int? coverType; + String? intro; + int? ctime; + int? mtime; + int? state; + int? favState; + int? mediaCount; + int? viewCount; + int? vt; + int? playSwitch; + int? type; + String? link; + String? bvid; + + FavFolderItemData.fromJson(Map json) { + id = json['id']; + fid = json['fid']; + mid = json['mid']; + attr = json['attr']; + title = json['title']; + cover = json['cover']; + upper = Upper.fromJson(json['upper']); + coverType = json['cover_type']; + intro = json['intro']; + ctime = json['ctime']; + mtime = json['mtime']; + state = json['state']; + favState = json['fav_state']; + mediaCount = json['media_count']; + viewCount = json['view_count']; + vt = json['vt']; + playSwitch = json['play_switch']; + type = json['type']; + link = json['link']; + bvid = json['bvid']; + } +} + +class Upper { + Upper({ + this.mid, + this.name, + this.face, + }); + + int? mid; + String? name; + String? face; + + Upper.fromJson(Map json) { + mid = json['mid']; + name = json['name']; + face = json['face']; + } +} diff --git a/lib/pages/fav/controller.dart b/lib/pages/fav/controller.dart new file mode 100644 index 000000000..ff14f9f9d --- /dev/null +++ b/lib/pages/fav/controller.dart @@ -0,0 +1,3 @@ +import 'package:get/get.dart'; + +class FavController extends GetxController {} diff --git a/lib/pages/fav/index.dart b/lib/pages/fav/index.dart new file mode 100644 index 000000000..84d36325d --- /dev/null +++ b/lib/pages/fav/index.dart @@ -0,0 +1,4 @@ +library fav; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart new file mode 100644 index 000000000..762fcf182 --- /dev/null +++ b/lib/pages/fav/view.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class FavPage extends StatefulWidget { + const FavPage({super.key}); + + @override + State createState() => _FavPageState(); +} + +class _FavPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + title: Text('我的收藏'), + ), + ); + } +} diff --git a/lib/pages/home/widgets/app_bar.dart b/lib/pages/home/widgets/app_bar.dart index f0b142e05..872d7eef2 100644 --- a/lib/pages/home/widgets/app_bar.dart +++ b/lib/pages/home/widgets/app_bar.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/pages/mine/view.dart'; class HomeAppBar extends StatelessWidget { const HomeAppBar({super.key}); @@ -42,6 +44,12 @@ class HomeAppBar extends StatelessWidget { // onPressed: () {}, // icon: const Icon(CupertinoIcons.bell, size: 22), // ), + IconButton( + onPressed: () { + Get.bottomSheet(const MinePage()); + }, + icon: const Icon(CupertinoIcons.person, size: 22), + ), const SizedBox(width: 10) ], elevation: 0, diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 752ff7130..25b219e70 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -5,26 +5,26 @@ import 'package:hive/hive.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/pages/home/view.dart'; import 'package:pilipala/pages/hot/view.dart'; -import 'package:pilipala/pages/mine/view.dart'; +import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/utils/storage.dart'; class MainController extends GetxController { List pages = [ const HomePage(), const HotPage(), - const MinePage(), + const MediaPage(), ]; RxList navigationBars = [ { // 'icon': const Icon(Icons.home_outlined), // 'selectedIcon': const Icon(Icons.home), 'icon': const Icon( - CupertinoIcons.house, - size: 18, + CupertinoIcons.square_favorites_alt, + size: 21, ), 'selectedIcon': const Icon( - CupertinoIcons.house_fill, - size: 18, + CupertinoIcons.square_favorites_alt_fill, + size: 21, ), 'label': "推荐", }, @@ -41,46 +41,57 @@ class MainController extends GetxController { ), 'label': "热门", }, + // { + // 'icon': const Icon( + // CupertinoIcons.person, + // size: 21, + // ), + // 'selectedIcon': const Icon( + // CupertinoIcons.person_fill, + // size: 21, + // ), + // 'label': "我的", + // }, { // 'icon': const Icon(Icons.person_outline), // 'selectedIcon': const Icon(Icons.person), 'icon': const Icon( - CupertinoIcons.person, + CupertinoIcons.tray_full, size: 21, ), 'selectedIcon': const Icon( - CupertinoIcons.person_fill, + CupertinoIcons.tray_full_fill, size: 21, ), - 'label': "我的", + 'label': "媒体库", } ].obs; @override void onInit() { super.onInit(); - readuUserFace(); + // readuUserFace(); } // 设置头像 - readuUserFace() async { - Box user = GStrorage.user; - if (user.get(UserBoxKey.userFace) != null) { - navigationBars.last['icon'] = - navigationBars.last['selectedIcon'] = NetworkImgLayer( - width: 25, - height: 25, - type: 'avatar', - src: user.get(UserBoxKey.userFace), - ); - navigationBars.last['label'] = '我'; - } - } + // readuUserFace() async { + // Box user = GStrorage.user; + // if (user.get(UserBoxKey.userFace) != null) { + // navigationBars.last['icon'] = + // navigationBars.last['selectedIcon'] = NetworkImgLayer( + // width: 25, + // height: 25, + // type: 'avatar', + // src: user.get(UserBoxKey.userFace), + // ); + // navigationBars.last['label'] = '我'; + // } + // } // 重置 - resetLast() { - navigationBars.last['icon'] = const Icon(Icons.person_outline); - navigationBars.last['selectedIcon'] = const Icon(Icons.person); - navigationBars.last['label'] = '我的'; - } + // resetLast() { + // navigationBars.last['icon'] = const Icon(Icons.person_outline); + // navigationBars.last['selectedIcon'] = const Icon(Icons.person); + // navigationBars.last['label'] = '我的'; + // } } diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index bce9bbc25..ded53199f 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -20,7 +20,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { late AnimationController? _animationController; late Animation? _fadeAnimation; late Animation? _slideAnimation; - int selectedIndex = 2; + int selectedIndex = 0; int? _lastSelectTime; //上次点击时间 @override diff --git a/lib/pages/media/controller.dart b/lib/pages/media/controller.dart new file mode 100644 index 000000000..0c0f31f80 --- /dev/null +++ b/lib/pages/media/controller.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/utils/storage.dart'; + +class MediaController extends GetxController { + Rx favFolderData = FavFolderData().obs; + List list = [ + { + 'icon': Icons.file_download_outlined, + 'title': '离线缓存', + 'onTap': () => Get.toNamed('/fav'), + }, + { + 'icon': Icons.history, + 'title': '观看记录', + 'onTap': () => Get.toNamed('/fav'), + }, + { + 'icon': Icons.star_border, + 'title': '我的收藏', + 'onTap': () => Get.toNamed('/fav'), + }, + { + 'icon': Icons.watch_later_outlined, + 'title': '稍后再看', + 'onTap': () => Get.toNamed('/fav'), + }, + ]; + + Future queryFavFolder() async { + var res = await await UserHttp.userfavFolder( + pn: 1, + ps: 5, + mid: GStrorage.user.get(UserBoxKey.userMid), + ); + favFolderData.value = res['data']; + return res; + } +} diff --git a/lib/pages/media/index.dart b/lib/pages/media/index.dart new file mode 100644 index 000000000..8fae48913 --- /dev/null +++ b/lib/pages/media/index.dart @@ -0,0 +1,4 @@ +library media; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart new file mode 100644 index 000000000..dd2b04463 --- /dev/null +++ b/lib/pages/media/view.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/pages/media/index.dart'; + +class MediaPage extends StatefulWidget { + const MediaPage({super.key}); + + @override + State createState() => _MediaPageState(); +} + +class _MediaPageState extends State + with AutomaticKeepAliveClientMixin { + final MediaController _mediaController = Get.put(MediaController()); + Future? _futureBuilderFuture; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _futureBuilderFuture = _mediaController.queryFavFolder(); + } + + @override + Widget build(BuildContext context) { + Color primary = Theme.of(context).colorScheme.primary; + return Scaffold( + appBar: AppBar(toolbarHeight: 30), + body: Column( + children: [ + ListTile( + leading: null, + title: Padding( + padding: const EdgeInsets.only(left: 20), + child: Text( + '媒体库', + style: TextStyle( + fontSize: Theme.of(context).textTheme.titleLarge!.fontSize, + ), + ), + ), + ), + for (var i in _mediaController.list) ...[ + ListTile( + onTap: () => i['onTap'](), + leading: Padding( + padding: const EdgeInsets.only(left: 15), + child: Icon( + i['icon'], + color: primary, + ), + ), + minLeadingWidth: 0, + title: Text(i['title']), + ), + ], + favFolder() + ], + ), + ); + } + + Widget favFolder() { + return Column( + children: [ + Divider( + height: 35, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + ListTile( + onTap: () {}, + leading: null, + dense: true, + title: Padding( + padding: const EdgeInsets.only(left: 10), + child: Obx( + () => Text.rich( + TextSpan( + children: [ + TextSpan( + text: '收藏夹 ', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.titleMedium!.fontSize, + ), + ), + if (_mediaController.favFolderData.value.count != null) + TextSpan( + text: _mediaController.favFolderData.value.count + .toString(), + style: TextStyle( + fontSize: + Theme.of(context).textTheme.titleSmall!.fontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ), + ), + ), + trailing: Padding( + padding: const EdgeInsets.only(right: 10), + child: Text( + '查看全部', + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline), + ), + ), + ), + // const SizedBox(height: 10), + SizedBox( + width: double.infinity, + height: 170, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + const SizedBox(width: 20), + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data; + if (data['status']) { + return Obx(() => Row( + children: [ + if (_mediaController.favFolderData.value.list != + null) ...[ + for (FavFolderItemData i in _mediaController + .favFolderData.value.list!) ...[ + FavFolderItem(item: i), + const SizedBox(width: 14) + ] + ] + ], + )); + } else { + return SizedBox( + height: 160, + child: Center(child: Text(data['msg'])), + ); + } + } else { + // 骨架屏 + return SizedBox(); + } + }), + // for (var i in [1, 2, 3]) ...[const FavFolderItem()], + const SizedBox(width: 10) + ], + ), + ), + ], + ); + } +} + +class FavFolderItem extends StatelessWidget { + FavFolderItem({super.key, this.item}); + FavFolderItemData? item; + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Container( + width: 110 * 16 / 9, + height: 110, + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + color: Theme.of(context).colorScheme.onInverseSurface, + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.onInverseSurface, + offset: const Offset(4, -12), // 阴影与容器的距离 + blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。 + spreadRadius: 0.0, // 在应用模糊之前,框应该膨胀的量。 + ), + ], + ), + child: LayoutBuilder( + builder: (context, BoxConstraints box) { + return NetworkImgLayer( + src: item!.cover, + width: box.maxWidth, + height: box.maxHeight, + ); + }, + ), + ), + Text( + ' ${item!.title}', + overflow: TextOverflow.fade, + maxLines: 1, + ), + Text( + ' 共${item!.mediaCount}条视频', + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: Theme.of(context).colorScheme.outline), + ) + ], + ); + } +} diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index 62a624380..93b0d1d0d 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -36,7 +36,7 @@ class MineController extends GetxController { user.put(UserBoxKey.userMid, res['data'].mid); user.put(UserBoxKey.userLogin, true); userLogin.value = true; - Get.find().readuUserFace(); + // Get.find().readuUserFace(); } else { resetUserInfo(); } @@ -64,6 +64,6 @@ class MineController extends GetxController { await user.delete(UserBoxKey.userMid); await user.delete(UserBoxKey.userLogin); userLogin.value = false; - Get.find().resetLast(); + // Get.find().resetLast(); } } diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index 7976659f4..85e43ad04 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -19,6 +19,11 @@ class _MinePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + automaticallyImplyLeading: false, + scrolledUnderElevation: 0, + elevation: 0, + toolbarHeight: kTextTabBarHeight + 20, + backgroundColor: Colors.transparent, title: null, actions: [ IconButton( @@ -54,6 +59,7 @@ class _MinePageState extends State { height: constraint.maxHeight, child: Column( children: [ + const SizedBox(height: 10), FutureBuilder( future: _mineController.queryUserInfo(), builder: (context, snapshot) { @@ -69,45 +75,6 @@ class _MinePageState extends State { }, ), const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.only(left: 12, right: 12), - child: LayoutBuilder( - builder: (context, constraints) { - return SizedBox( - height: constraints.maxWidth / 4 * 0.8, - child: GridView.count( - primary: false, - padding: const EdgeInsets.all(0), - crossAxisCount: 4, - childAspectRatio: 1.25, - children: [ - ActionItem( - icon: - const Icon(CupertinoIcons.cloud_download), - onTap: () => {}, - text: '离线缓存', - ), - ActionItem( - icon: const Icon(CupertinoIcons.time), - onTap: () => {}, - text: '历史记录', - ), - ActionItem( - icon: const Icon(CupertinoIcons.star), - onTap: () => {}, - text: '我的收藏', - ), - ActionItem( - icon: const Icon(CupertinoIcons.film), - onTap: () => {}, - text: '稍后再看', - ), - ], - ), - ); - }, - ), - ), ], ), ), diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 753ba8a00..9b7e385be 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,10 +1,12 @@ import 'package:get/get.dart'; +import 'package:pilipala/pages/fav/index.dart'; import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/webview/index.dart'; import 'package:pilipala/pages/setting/index.dart'; +import 'package:pilipala/pages/media/index.dart'; class Routes { static final List getPages = [ @@ -20,5 +22,9 @@ class Routes { GetPage(name: '/webview', page: () => const WebviewPage()), // 设置 GetPage(name: '/setting', page: () => const SettingPage()), + // + GetPage(name: '/media', page: () => const MediaPage()), + // + GetPage(name: '/fav', page: () => const FavPage()), ]; }