Merge branch 'design'

This commit is contained in:
guozhigq
2023-08-06 21:28:23 +08:00
20 changed files with 691 additions and 340 deletions

View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'skeleton.dart';
class MediaBangumiSkeleton extends StatefulWidget {
const MediaBangumiSkeleton({super.key});
@override
State<MediaBangumiSkeleton> createState() => _MediaBangumiSkeletonState();
}
class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
@override
Widget build(BuildContext context) {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
return Skeleton(
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 7, StyleString.safeSpace, 7),
child: Row(
children: [
Container(
width: 111,
height: 148,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(6)),
color: bgColor),
),
const SizedBox(width: 10),
SizedBox(
height: 148,
child: Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
color: Theme.of(context).colorScheme.onInverseSurface,
width: 200,
height: 20,
margin: const EdgeInsets.only(bottom: 15),
),
Container(
color: Theme.of(context).colorScheme.onInverseSurface,
width: 150,
height: 13,
margin: const EdgeInsets.only(bottom: 5),
),
Container(
color: Theme.of(context).colorScheme.onInverseSurface,
width: 150,
height: 13,
margin: const EdgeInsets.only(bottom: 5),
),
Container(
color: Theme.of(context).colorScheme.onInverseSurface,
width: 150,
height: 13,
),
const Spacer(),
Container(
width: 90,
height: 35,
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(20)),
color: Theme.of(context).colorScheme.onInverseSurface,
),
),
],
),
),
),
],
),
),
);
}
}

View File

@ -8,103 +8,86 @@ class VideoCardHSkeleton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Skeleton( return Skeleton(
child: Column( child: Padding(
children: [ padding: const EdgeInsets.fromLTRB(
Padding( StyleString.safeSpace, 7, StyleString.safeSpace, 7),
padding: const EdgeInsets.fromLTRB( child: LayoutBuilder(
StyleString.cardSpace, 7, StyleString.cardSpace, 7), builder: (context, boxConstraints) {
child: LayoutBuilder( double width =
builder: (context, boxConstraints) { (boxConstraints.maxWidth - StyleString.cardSpace * 9) / 2;
double width = return SizedBox(
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; height: width / StyleString.aspectRatio,
return SizedBox( child: Row(
height: width / StyleString.aspectRatio, mainAxisAlignment: MainAxisAlignment.start,
child: Row( crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, children: [
crossAxisAlignment: CrossAxisAlignment.start, AspectRatio(
children: [ aspectRatio: StyleString.aspectRatio,
AspectRatio( child: LayoutBuilder(
aspectRatio: StyleString.aspectRatio, builder: (context, boxConstraints) {
child: LayoutBuilder( return Container(
builder: (context, boxConstraints) { decoration: BoxDecoration(
return Container( color:
decoration: BoxDecoration( Theme.of(context).colorScheme.onInverseSurface,
color: Theme.of(context) borderRadius:
.colorScheme BorderRadius.circular(StyleString.imgRadius.x),
.onInverseSurface, ),
borderRadius: BorderRadius.circular( );
StyleString.imgRadius.x), },
), ),
); ),
}, // VideoContent(videoItem: videoItem)
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
color: Theme.of(context).colorScheme.onInverseSurface,
width: 200,
height: 11,
margin: const EdgeInsets.only(bottom: 5),
), ),
), Container(
// VideoContent(videoItem: videoItem) color: Theme.of(context).colorScheme.onInverseSurface,
Expanded( width: 150,
child: Padding( height: 13,
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4), ),
child: Column( const Spacer(),
crossAxisAlignment: CrossAxisAlignment.start, Container(
color: Theme.of(context).colorScheme.onInverseSurface,
width: 100,
height: 13,
margin: const EdgeInsets.only(bottom: 5),
),
Row(
children: [ children: [
Container( Container(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onInverseSurface, .onInverseSurface,
width: 200, width: 40,
height: 11, height: 13,
margin: const EdgeInsets.only(bottom: 5), margin: const EdgeInsets.only(right: 8),
), ),
Container( Container(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onInverseSurface, .onInverseSurface,
width: 150, width: 40,
height: 13, height: 13,
), ),
const Spacer(),
Container(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
width: 100,
height: 13,
margin: const EdgeInsets.only(bottom: 5),
),
Row(
children: [
Container(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
width: 40,
height: 13,
margin: const EdgeInsets.only(right: 8),
),
Container(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
width: 40,
height: 13,
),
],
)
], ],
), )
)), ],
], ),
), )),
); ],
}, ),
), );
), },
Divider( ),
height: 1,
indent: 8,
endIndent: 12,
color: Theme.of(context).dividerColor.withOpacity(0.08),
)
],
), ),
); );
} }

View File

@ -56,63 +56,53 @@ class VideoCardH extends StatelessWidget {
SmartDialog.showToast(err.toString()); SmartDialog.showToast(err.toString());
} }
}, },
child: Column( child: Padding(
children: [ padding: const EdgeInsets.fromLTRB(
Padding( StyleString.safeSpace, 7, StyleString.safeSpace, 7),
padding: const EdgeInsets.fromLTRB( child: LayoutBuilder(
StyleString.safeSpace, 6, StyleString.safeSpace, 6), builder: (context, boxConstraints) {
child: LayoutBuilder( double width =
builder: (context, boxConstraints) { (boxConstraints.maxWidth - StyleString.cardSpace * 9) / 2;
double width = return SizedBox(
(boxConstraints.maxWidth - StyleString.cardSpace * 9) / 2; height: width / StyleString.aspectRatio,
return SizedBox( child: Row(
height: width / StyleString.aspectRatio, mainAxisAlignment: MainAxisAlignment.start,
child: Row( crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, children: [
crossAxisAlignment: CrossAxisAlignment.start, AspectRatio(
children: [ aspectRatio: StyleString.aspectRatio,
AspectRatio( child: LayoutBuilder(
aspectRatio: StyleString.aspectRatio, builder: (context, boxConstraints) {
child: LayoutBuilder( double maxWidth = boxConstraints.maxWidth;
builder: (context, boxConstraints) { double maxHeight = boxConstraints.maxHeight;
double maxWidth = boxConstraints.maxWidth; return Stack(
double maxHeight = boxConstraints.maxHeight; children: [
return Stack( Hero(
children: [ tag: heroTag,
Hero( child: NetworkImgLayer(
tag: heroTag, src: videoItem.pic,
child: NetworkImgLayer( width: maxWidth,
src: videoItem.pic, height: maxHeight,
width: maxWidth, ),
height: maxHeight, ),
), pBadge(Utils.timeFormat(videoItem.duration!),
), context, null, 6.0, 6.0, null,
pBadge(Utils.timeFormat(videoItem.duration!), type: 'gray'),
context, null, 6.0, 6.0, null, if (videoItem.rcmdReason != null &&
type: 'gray'), videoItem.rcmdReason.content != '')
if (videoItem.rcmdReason != null && pBadge(videoItem.rcmdReason.content, context,
videoItem.rcmdReason.content != '') 6.0, 6.0, null, null),
pBadge(videoItem.rcmdReason.content, ],
context, 6.0, 6.0, null, null), );
], },
); ),
},
),
),
VideoContent(videoItem: videoItem)
],
), ),
); VideoContent(videoItem: videoItem)
}, ],
), ),
), );
// Divider( },
// height: 1, ),
// indent: 8,
// endIndent: 12,
// color: Theme.of(context).dividerColor.withOpacity(0.08),
// )
],
), ),
), ),
); );

View File

@ -167,6 +167,10 @@ class Api {
// 热搜 // 热搜
static const String hotSearchList = static const String hotSearchList =
'https://s.search.bilibili.com/main/hotword'; 'https://s.search.bilibili.com/main/hotword';
// 默认搜索词
static const String searchDefault = '/x/web-interface/wbi/search/default';
// 搜索关键词 // 搜索关键词
static const String serachSuggest = static const String serachSuggest =
'https://s.search.bilibili.com/main/suggest'; 'https://s.search.bilibili.com/main/suggest';

View File

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

View File

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

View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
class BangumiPage extends StatefulWidget {
const BangumiPage({super.key});
@override
State<BangumiPage> createState() => _BangumiPageState();
}
class _BangumiPageState extends State<BangumiPage> {
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('还在开发中'),
),
);
}
}

View File

@ -1,12 +1,16 @@
import 'dart:async';
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart'; import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/skeleton/dynamic_card.dart'; import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -36,18 +40,27 @@ class _DynamicsPageState extends State<DynamicsPage>
void initState() { void initState() {
super.initState(); super.initState();
_futureBuilderFuture = _dynamicsController.queryFollowDynamic(); _futureBuilderFuture = _dynamicsController.queryFollowDynamic();
ScrollController scrollController = _dynamicsController.scrollController;
_dynamicsController.scrollController.addListener( StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
scrollController.addListener(
() async { () async {
if (_dynamicsController.scrollController.position.pixels >= if (scrollController.position.pixels >=
_dynamicsController.scrollController.position.maxScrollExtent - scrollController.position.maxScrollExtent - 200) {
200) {
if (!_isLoadingMore) { if (!_isLoadingMore) {
_isLoadingMore = true; _isLoadingMore = true;
await _dynamicsController.queryFollowDynamic(type: 'onLoad'); await _dynamicsController.queryFollowDynamic(type: 'onLoad');
_isLoadingMore = false; _isLoadingMore = false;
} }
} }
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
}
}, },
); );
} }

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/pages/bangumi/index.dart';
import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/live/index.dart'; import 'package:pilipala/pages/live/index.dart';
import 'package:pilipala/pages/rcmd/index.dart'; import 'package:pilipala/pages/rcmd/index.dart';
@ -31,6 +33,14 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
'label': '热门', 'label': '热门',
'type': 'hot' 'type': 'hot'
}, },
{
'icon': const Icon(
Icons.play_circle_outlined,
size: 15,
),
'label': '番剧',
'type': 'bangumi'
},
]; ];
int initialIndex = 1; int initialIndex = 1;
late TabController tabController; late TabController tabController;
@ -38,7 +48,9 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
Get.find<LiveController>, Get.find<LiveController>,
Get.find<RcmdController>, Get.find<RcmdController>,
Get.find<HotController>, Get.find<HotController>,
Get.find<BangumiController>,
]; ];
RxString defaultSearch = '输入关键词搜索'.obs;
@override @override
void onInit() { void onInit() {
@ -48,6 +60,7 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
length: tabs.length, length: tabs.length,
vsync: this, vsync: this,
); );
searchDefault();
} }
void onRefresh() { void onRefresh() {
@ -61,4 +74,11 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
var ctr = ctrList[index]; var ctr = ctrList[index];
ctr().animateToTop(); ctr().animateToTop();
} }
void searchDefault() async {
var res = await Request().get(Api.searchDefault);
if (res.data['code'] == 0) {
defaultSearch.value = res.data['data']['name'];
}
}
} }

View File

@ -1,10 +1,16 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/bangumi/index.dart';
import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/live/index.dart'; import 'package:pilipala/pages/live/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/rcmd/index.dart'; import 'package:pilipala/pages/rcmd/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import './controller.dart'; import './controller.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
@ -18,7 +24,7 @@ class _HomePageState extends State<HomePage>
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
final HomeController _homeController = Get.put(HomeController()); final HomeController _homeController = Get.put(HomeController());
List videoList = []; List videoList = [];
// late TabController? _tabController; Stream<bool> stream = Get.find<MainController>().bottomBarStream.stream;
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@ -27,103 +33,231 @@ class _HomePageState extends State<HomePage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return Scaffold( return Scaffold(
appBar: AppBar( extendBody: true,
titleSpacing: 0, extendBodyBehindAppBar: true,
title: Padding( appBar: AppBar(toolbarHeight: 0, elevation: 0),
padding: const EdgeInsets.only(left: 12, right: 12, bottom: 0), body: Column(
child: Stack( children: [
children: [ CustomAppBar(stream: stream, ctr: _homeController),
const Align( Container(
alignment: Alignment.centerLeft, padding: const EdgeInsets.only(left: 12, right: 12, bottom: 4),
child: Padding( child: Stack(
padding: EdgeInsets.only(left: 8), children: [
child: Text( Align(
'PLPL', alignment: Alignment.center,
style: TextStyle( child: Theme(
height: 2.8, data: ThemeData(
fontSize: 17, splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
fontWeight: FontWeight.bold, highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明
letterSpacing: 1,
fontFamily: 'Jura-Bold',
), ),
), child: Padding(
), padding: const EdgeInsets.only(top: 2),
), child: TabBar(
Align( controller: _homeController.tabController,
alignment: Alignment.center, tabs: [
child: Theme( for (var i in _homeController.tabs)
data: ThemeData( // Tab(text: i['label'])
splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明 Padding(
highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明 padding: const EdgeInsets.symmetric(
), horizontal: 0, vertical: 11),
child: Padding( child: Row(
padding: const EdgeInsets.only(top: 4), children: [
child: TabBar( i['icon'],
controller: _homeController.tabController, const SizedBox(width: 4),
tabs: [ Text(i['label'])
for (var i in _homeController.tabs) ],
// Tab(text: i['label']) ),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 0, vertical: 11),
child: Row(
children: [
i['icon'],
const SizedBox(width: 4),
Text(i['label'])
],
), ),
), ],
], isScrollable: true,
isScrollable: true, indicatorWeight: 0,
indicatorWeight: 0, indicatorPadding: const EdgeInsets.symmetric(
indicatorPadding: const EdgeInsets.symmetric( horizontal: 4, vertical: 5),
horizontal: 4, vertical: 5), indicator: BoxDecoration(
indicator: BoxDecoration( color: Theme.of(context)
color: Theme.of(context) .colorScheme
.colorScheme .primaryContainer
.primaryContainer .withOpacity(0.8),
.withOpacity(0.8), borderRadius:
borderRadius: const BorderRadius.all(Radius.circular(20)),
const BorderRadius.all(Radius.circular(20)), ),
indicatorSize: TabBarIndicatorSize.tab,
labelColor: Theme.of(context).colorScheme.primary,
labelStyle: const TextStyle(fontSize: 13),
dividerColor: Colors.transparent,
unselectedLabelColor:
Theme.of(context).colorScheme.outline,
onTap: (value) =>
{feedBack(), _homeController.initialIndex = value},
), ),
indicatorSize: TabBarIndicatorSize.tab,
labelColor: Theme.of(context).colorScheme.primary,
labelStyle: const TextStyle(fontSize: 13),
dividerColor: Colors.transparent,
unselectedLabelColor:
Theme.of(context).colorScheme.outline,
onTap: (value) =>
{feedBack(), _homeController.initialIndex = value},
), ),
), ),
), ),
), ],
Align( ),
alignment: Alignment.centerRight, ),
child: Hero( Expanded(
tag: 'searchTag', child: TabBarView(
child: IconButton( controller: _homeController.tabController,
onPressed: () { children: const [
feedBack(); LivePage(),
Get.toNamed('/search'); RcmdPage(),
}, HotPage(),
icon: const Icon(CupertinoIcons.search, size: 21), BangumiPage(),
), ],
), ),
),
],
), ),
),
),
body: TabBarView(
controller: _homeController.tabController,
children: const [
LivePage(),
RcmdPage(),
HotPage(),
], ],
), ),
); );
} }
} }
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final double height;
final Stream<bool>? stream;
final ctr;
const CustomAppBar({
super.key,
this.height = kToolbarHeight,
this.stream,
this.ctr,
});
@override
Size get preferredSize => Size.fromHeight(height);
@override
Widget build(BuildContext context) {
Box user = GStrorage.user;
return StreamBuilder(
stream: stream,
initialData: true,
builder: (context, AsyncSnapshot snapshot) {
return ClipRect(
clipBehavior: Clip.hardEdge,
child: AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: AnimatedContainer(
curve: Curves.linear,
duration: const Duration(milliseconds: 300),
height: snapshot.data ? 94 : MediaQuery.of(context).padding.top,
child: Container(
padding: EdgeInsets.only(
left: 12,
right: 12,
bottom: 4,
top: MediaQuery.of(context).padding.top,
),
child: Row(
children: [
const Text(
'PLPL',
style: TextStyle(
height: 2.8,
fontSize: 17,
fontWeight: FontWeight.bold,
fontFamily: 'Jura-Bold',
),
),
const SizedBox(width: 10),
Expanded(
child: Hero(
tag: 'searchWrap',
child: GestureDetector(
onTap: () {
Get.toNamed('/search', parameters: {
'hintText': ctr.defaultSearch.value
});
},
child: Container(
width: 250,
height: 45,
clipBehavior: Clip.hardEdge,
padding: const EdgeInsets.only(left: 12, right: 22),
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(25)),
color: Theme.of(context)
.colorScheme
.onInverseSurface,
),
child: Row(
children: [
Icon(
Icons.search_outlined,
size: 23,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(width: 7),
Expanded(
child: Obx(
() => Text(
ctr.defaultSearch.value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline),
),
),
),
],
),
),
),
),
),
const SizedBox(width: 12),
if (user.get(UserBoxKey.userLogin) ?? false) ...[
GestureDetector(
onTap: () {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
},
child: NetworkImgLayer(
type: 'avatar',
width: 34,
height: 34,
src: user.get(UserBoxKey.userFace),
),
)
] else ...[
IconButton(
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
},
icon: const Icon(CupertinoIcons.person, size: 22),
)
],
],
),
),
),
),
);
},
);
}
}

View File

@ -1,4 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart'; import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart';
@ -6,6 +9,7 @@ import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/pages/hot/controller.dart'; import 'package:pilipala/pages/hot/controller.dart';
import 'package:pilipala/pages/main/index.dart';
class HotPage extends StatefulWidget { class HotPage extends StatefulWidget {
const HotPage({Key? key}) : super(key: key); const HotPage({Key? key}) : super(key: key);
@ -26,15 +30,26 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
void initState() { void initState() {
super.initState(); super.initState();
_futureBuilderFuture = _hotController.queryHotFeed('init'); _futureBuilderFuture = _hotController.queryHotFeed('init');
_hotController.scrollController.addListener( ScrollController scrollController = _hotController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
scrollController.addListener(
() { () {
if (_hotController.scrollController.position.pixels >= if (scrollController.position.pixels >=
_hotController.scrollController.position.maxScrollExtent - 200) { scrollController.position.maxScrollExtent - 200) {
if (!_hotController.isLoadingMore) { if (!_hotController.isLoadingMore) {
_hotController.isLoadingMore = true; _hotController.isLoadingMore = true;
_hotController.onLoad(); _hotController.onLoad();
} }
} }
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
}
}, },
); );
} }

View File

@ -1,10 +1,14 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/video_card_v.dart'; import 'package:pilipala/common/skeleton/video_card_v.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart'; import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/pages/main/index.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/live_item.dart'; import 'widgets/live_item.dart';
@ -22,15 +26,26 @@ class _LivePageState extends State<LivePage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_liveController.scrollController.addListener( ScrollController scrollController = _liveController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
scrollController.addListener(
() { () {
if (_liveController.scrollController.position.pixels >= if (scrollController.position.pixels >=
_liveController.scrollController.position.maxScrollExtent - 200) { scrollController.position.maxScrollExtent - 200) {
if (!_liveController.isLoadingMore) { if (!_liveController.isLoadingMore) {
_liveController.isLoadingMore = true; _liveController.isLoadingMore = true;
_liveController.onLoad(); _liveController.onLoad();
} }
} }
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
}
}, },
); );
} }

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/dynamics/index.dart';
@ -33,4 +35,6 @@ class MainController extends GetxController {
'label': "媒体库", 'label': "媒体库",
} }
].obs; ].obs;
final StreamController<bool> bottomBarStream =
StreamController<bool>.broadcast();
} }

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -64,7 +66,6 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) { if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {
_homeController.onRefresh(); _homeController.onRefresh();
} else { } else {
await Future.delayed(const Duration(microseconds: 300));
_homeController.animateToTop(); _homeController.animateToTop();
} }
_lastSelectTime = DateTime.now().millisecondsSinceEpoch; _lastSelectTime = DateTime.now().millisecondsSinceEpoch;
@ -80,7 +81,6 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) { if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {
_dynamicController.onRefresh(); _dynamicController.onRefresh();
} else { } else {
await Future.delayed(const Duration(microseconds: 300));
_dynamicController.animateToTop(); _dynamicController.animateToTop();
} }
_lastSelectTime = DateTime.now().millisecondsSinceEpoch; _lastSelectTime = DateTime.now().millisecondsSinceEpoch;
@ -105,6 +105,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
localCache.put('sheetHeight', sheetHeight); localCache.put('sheetHeight', sheetHeight);
localCache.put('statusBarHeight', statusBarHeight); localCache.put('statusBarHeight', statusBarHeight);
return Scaffold( return Scaffold(
extendBody: true,
body: FadeTransition( body: FadeTransition(
opacity: _fadeAnimation!, opacity: _fadeAnimation!,
child: SlideTransition( child: SlideTransition(
@ -129,21 +130,33 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
), ),
), ),
), ),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: StreamBuilder(
currentIndex: selectedIndex, stream: _mainController.bottomBarStream.stream,
// type: BottomNavigationBarType.shifting, initialData: true,
selectedItemColor: Theme.of(context).colorScheme.primary, builder: (context, AsyncSnapshot snapshot) {
unselectedItemColor: Theme.of(context).colorScheme.onSurfaceVariant, return AnimatedSlide(
selectedFontSize: 12.4, curve: Curves.linear,
onTap: (value) => setIndex(value), duration: const Duration(milliseconds: 300),
items: [ offset: Offset(0, snapshot.data ? 0 : 1),
..._mainController.navigationBars.map((e) { child: BottomNavigationBar(
return BottomNavigationBarItem( currentIndex: selectedIndex,
icon: e['icon'], // type: BottomNavigationBarType.shifting,
label: e['label'], selectedItemColor: Theme.of(context).colorScheme.primary,
); unselectedItemColor:
}).toList(), Theme.of(context).colorScheme.onSurfaceVariant,
], selectedFontSize: 12.4,
onTap: (value) => setIndex(value),
items: [
..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem(
icon: e['icon'],
label: e['label'],
);
}).toList(),
],
),
);
},
), ),
); );
} }

View File

@ -1,4 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/video_card_v.dart'; import 'package:pilipala/common/skeleton/video_card_v.dart';
@ -6,6 +9,7 @@ import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_v.dart'; import 'package:pilipala/common/widgets/video_card_v.dart';
import 'package:pilipala/pages/main/index.dart';
import 'controller.dart'; import 'controller.dart';
@ -26,15 +30,26 @@ class _RcmdPageState extends State<RcmdPage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_rcmdController.scrollController.addListener( ScrollController scrollController = _rcmdController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
scrollController.addListener(
() { () {
if (_rcmdController.scrollController.position.pixels >= if (scrollController.position.pixels >=
_rcmdController.scrollController.position.maxScrollExtent - 200) { scrollController.position.maxScrollExtent - 200) {
if (!_rcmdController.isLoadingMore) { if (!_rcmdController.isLoadingMore) {
_rcmdController.isLoadingMore = true; _rcmdController.isLoadingMore = true;
_rcmdController.onLoad(); _rcmdController.onLoad();
} }
} }
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
}
}, },
); );
} }

View File

@ -19,6 +19,7 @@ class SSearchController extends GetxController {
RxList<SearchSuggestItem> searchSuggestList = [SearchSuggestItem()].obs; RxList<SearchSuggestItem> searchSuggestList = [SearchSuggestItem()].obs;
final _debouncer = final _debouncer =
Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间 Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间
String hintText = '搜索';
@override @override
void onInit() { void onInit() {
@ -33,7 +34,13 @@ class SSearchController extends GetxController {
} }
// 其他页面跳转过来 // 其他页面跳转过来
if (Get.parameters.keys.isNotEmpty) { if (Get.parameters.keys.isNotEmpty) {
onClickKeyword(Get.parameters['keyword']!); if (Get.parameters['keyword'] != null) {
onClickKeyword(Get.parameters['keyword']!);
}
if (Get.parameters['hintText'] != null) {
hintText = Get.parameters['hintText']!;
searchKeyWord.value = hintText;
}
} }
historyCacheList = histiryWord.get('cacheList') ?? []; historyCacheList = histiryWord.get('cacheList') ?? [];
historyList.value = historyCacheList; historyList.value = historyCacheList;

View File

@ -17,6 +17,13 @@ class SearchPage extends StatefulWidget {
class _SearchPageState extends State<SearchPage> with RouteAware { class _SearchPageState extends State<SearchPage> with RouteAware {
final SSearchController _searchController = Get.put(SSearchController()); final SSearchController _searchController = Get.put(SSearchController());
late Future? _futureBuilderFuture;
@override
void initState() {
super.initState();
_futureBuilderFuture = _searchController.queryHotSearchList();
}
@override @override
// 返回当前页面时 // 返回当前页面时
@ -53,26 +60,29 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
), ),
const SizedBox(width: 10) const SizedBox(width: 10)
], ],
title: Obx( title: Hero(
() => TextField( tag: 'searchWrap',
autofocus: true, child: Obx(
focusNode: _searchController.searchFocusNode, () => TextField(
controller: _searchController.controller.value, autofocus: true,
textInputAction: TextInputAction.search, focusNode: _searchController.searchFocusNode,
onChanged: (value) => _searchController.onChange(value), controller: _searchController.controller.value,
decoration: InputDecoration( textInputAction: TextInputAction.search,
hintText: '搜索', onChanged: (value) => _searchController.onChange(value),
border: InputBorder.none, decoration: InputDecoration(
suffixIcon: IconButton( hintText: _searchController.hintText,
icon: Icon( border: InputBorder.none,
Icons.clear, suffixIcon: IconButton(
size: 22, icon: Icon(
color: Theme.of(context).colorScheme.outline, Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _searchController.onClear(),
), ),
onPressed: () => _searchController.onClear(),
), ),
onSubmitted: (String value) => _searchController.submit(),
), ),
onSubmitted: (String value) => _searchController.submit(),
), ),
), ),
), ),
@ -159,7 +169,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
builder: (context, boxConstraints) { builder: (context, boxConstraints) {
final double width = boxConstraints.maxWidth; final double width = boxConstraints.maxWidth;
return FutureBuilder( return FutureBuilder(
future: _searchController.queryHotSearchList(), future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map; Map data = snapshot.data as Map;

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/media_bangumi.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
@ -104,7 +105,18 @@ class _SearchPanelState extends State<SearchPanel>
addRepaintBoundaries: false, addRepaintBoundaries: false,
itemCount: 15, itemCount: 15,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return const VideoCardHSkeleton(); switch (widget.searchType) {
case SearchType.video:
return const VideoCardHSkeleton();
case SearchType.media_bangumi:
return const MediaBangumiSkeleton();
case SearchType.bili_user:
return const VideoCardHSkeleton();
case SearchType.live_room:
return const VideoCardHSkeleton();
default:
return const VideoCardHSkeleton();
}
}, },
); );
} }

View File

@ -6,7 +6,7 @@ import 'package:pilipala/utils/utils.dart';
Widget searchLivePanel(BuildContext context, ctr, list) { Widget searchLivePanel(BuildContext context, ctr, list) {
return Padding( return Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: StyleString.cardSpace, right: StyleString.cardSpace), left: StyleString.safeSpace, right: StyleString.safeSpace),
child: GridView.builder( child: GridView.builder(
primary: false, primary: false,
controller: ctr!.scrollController, controller: ctr!.scrollController,
@ -16,73 +16,82 @@ Widget searchLivePanel(BuildContext context, ctr, list) {
mainAxisSpacing: StyleString.cardSpace + 3, mainAxisSpacing: StyleString.cardSpace + 3,
mainAxisExtent: mainAxisExtent:
MediaQuery.of(context).size.width / 2 / StyleString.aspectRatio + MediaQuery.of(context).size.width / 2 / StyleString.aspectRatio +
65, 60,
), ),
itemCount: list.length, itemCount: list.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
var i = list![index]; return LiveItem(liveItem: list![index]);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: InkWell(
onTap: () {},
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: Utils.makeHeroTag(i.roomid),
child: NetworkImgLayer(
src: i.cover,
type: 'emote',
width: maxWidth,
height: maxHeight,
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: AnimatedOpacity(
opacity: 1,
duration: const Duration(milliseconds: 200),
child: LiveStat(
online: i.online,
cateName: i.cateName,
),
),
),
],
);
}),
),
),
LiveContent(liveItem: i)
],
),
),
);
}, },
), ),
); );
} }
class LiveItem extends StatelessWidget {
final dynamic liveItem;
const LiveItem({Key? key, required this.liveItem}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: InkWell(
onTap: () {},
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: Utils.makeHeroTag(liveItem.roomid),
child: NetworkImgLayer(
src: liveItem.cover,
type: 'emote',
width: maxWidth,
height: maxHeight,
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: AnimatedOpacity(
opacity: 1,
duration: const Duration(milliseconds: 200),
child: LiveStat(
online: liveItem.online,
cateName: liveItem.cateName,
),
),
),
],
);
}),
),
),
LiveContent(liveItem: liveItem)
],
),
),
);
}
}
class LiveContent extends StatelessWidget { class LiveContent extends StatelessWidget {
final dynamic liveItem; final dynamic liveItem;
const LiveContent({Key? key, required this.liveItem}) : super(key: key); const LiveContent({Key? key, required this.liveItem}) : super(key: key);

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
@ -28,7 +29,8 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
// }); // });
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 7, StyleString.safeSpace, 7),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [