You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

309 lines
9.7 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:isekai_wiki/components/isekai_nav_bar.dart';
import 'package:isekai_wiki/components/isekai_page_scaffold.dart';
import 'package:isekai_wiki/components/recent_page_list.dart';
import 'package:isekai_wiki/components/safearea_builder.dart';
import 'package:isekai_wiki/models/user.dart';
import 'package:isekai_wiki/pages/tab_page.dart';
import 'package:web_smooth_scroll/web_smooth_scroll.dart';
import '../components/collapsed_tab.dart';
import '../global.dart';
import '../styles.dart';
const Color _kDefaultTabBarBorderColor = CupertinoDynamicColor.withBrightness(
color: Color(0x4C000000),
darkColor: Color(0x29000000),
);
enum HomeTabs { newest, followed }
class HomeController extends GetxController
with GetSingleTickerProviderStateMixin {
double _navSearchButtonOffset = 90;
var showNavSearchButton = false.obs;
var isScrolling = false.obs;
StreamController? streamController;
StreamSubscription? _streamSubscription;
late TabController tabController;
late TabsPageController tabsPageController;
final ScrollController scrollController = ScrollController();
VoidFutureCallback? onRefresh;
@override
void onInit() {
tabController = TabController(length: 2, vsync: this);
_navSearchButtonOffset =
48 * MediaQuery.of(Get.context!).textScaleFactor + 48;
scrollController.addListener(() {
if (scrollController.offset >= _navSearchButtonOffset &&
!showNavSearchButton.value) {
showNavSearchButton.value = true;
} else if (scrollController.offset < _navSearchButtonOffset &&
showNavSearchButton.value) {
showNavSearchButton.value = false;
}
});
tabsPageController = Get.find<TabsPageController>();
tabsPageController.streamController.stream.listen((event) {
if (event == "tap:home") {
handleTapHomeTab();
}
});
super.onInit();
}
@override
void dispose() {
_streamSubscription?.cancel();
super.dispose();
}
Future<void> handleTapHomeTab() async {
if (isScrolling.value) return;
if (scrollController.offset != 0) {
isScrolling.value = true;
await scrollController.animateTo(
0,
duration: const Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn,
);
isScrolling.value = false;
}
}
Future<void> handleRefresh() async {
if (onRefresh != null) {
await onRefresh!();
}
}
}
class HomeTab extends StatelessWidget {
final VoidCallback? onSearchClick;
const HomeTab({super.key, this.onSearchClick});
Widget _buildNotificationIconButton() {
final uc = Get.find<UserController>();
return Obx(
() => AnimatedOpacity(
opacity: uc.isLoggedIn ? 1.0 : 0.0,
duration: const Duration(milliseconds: 100),
child: CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.bell,
size: 26, color: Styles.themeNavTitleColor),
onPressed: () {},
),
),
);
}
Widget _buildSearchIconButton() {
final c = Get.find<HomeController>();
return Obx(
() => AnimatedOpacity(
opacity: c.showNavSearchButton.value ? 1.0 : 0.0,
duration: const Duration(milliseconds: 100),
child: CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.search,
size: 26, color: Styles.themeNavTitleColor),
onPressed: () {
onSearchClick?.call();
},
),
),
);
}
@override
Widget build(BuildContext context) {
final c = Get.put(HomeController());
return IsekaiPageScaffold(
child: SafeAreaBuilder(
builder: (_, safeArea) => WebSmoothScroll(
controller: c.scrollController,
child: CustomScrollView(
controller: c.scrollController,
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
slivers: <Widget>[
IsekaiSliverNavigationBar(
leading: _buildSearchIconButton(),
backgroundColor: Styles.themeMainColor,
brightness: Brightness.dark,
largeTitle: const Text('首页',
style: TextStyle(color: Styles.themeNavTitleColor)),
trailing: _buildNotificationIconButton(),
border: Border.all(style: BorderStyle.none),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
minHeight: 48,
maxHeight: 48,
child: Container(
decoration: const BoxDecoration(
color: Styles.themeMainColor,
boxShadow: [
BoxShadow(
color: Styles.themeMainColor,
blurRadius: 0.0,
spreadRadius: 0.0,
offset: Offset(0, -2),
),
],
),
padding:
const EdgeInsets.only(bottom: 10, left: 12, right: 12),
child: CupertinoButton(
color: Colors.white,
padding: const EdgeInsets.all(0),
onPressed: () {
onSearchClick?.call();
},
child: Wrap(
spacing: 14,
alignment: WrapAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(1),
child: const Icon(CupertinoIcons.search,
color: Colors.black54),
),
const Text("搜索页面...",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black54))
],
),
),
),
),
),
if (Global.siteConfig.enableFollowing) // 仅在站点开启关注功能时显示
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
minHeight: 40.0,
maxHeight: 40.0,
child: Container(
decoration:
BoxDecoration(color: Colors.white, boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 4,
offset: const Offset(0, 2),
)
]),
child: Row(
children: [
SizedBox(
child: TabBar(
isScrollable: true,
controller: c.tabController,
indicatorColor: Styles.themeMainColor,
labelColor: Styles.themeMainColor,
unselectedLabelColor: Colors.black45,
tabs: const [
CollapsedTabText('最新'),
CollapsedTabText('关注')
],
onTap: (int selected) {},
),
),
const Expanded(child: Text('')),
],
),
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
minHeight: 1,
maxHeight: 1,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).shadowColor,
boxShadow: [
BoxShadow(
color: Theme.of(context).shadowColor,
spreadRadius: 5,
blurRadius: 4,
)
],
),
),
),
),
CupertinoSliverRefreshControl(
onRefresh: c.handleRefresh,
),
SliverSafeArea(
top: false,
minimum: const EdgeInsets.only(top: 12, bottom: 12),
sliver: RecentPageList(
scrollController: c.scrollController,
),
),
],
),
),
),
);
}
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
required this.minHeight,
required this.maxHeight,
required this.child,
});
final double minHeight;
final double maxHeight;
final Widget child;
@override
double get minExtent => minHeight;
@override
double get maxExtent => max(maxHeight, minHeight);
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(child: child);
}
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}