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
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;
|
|
}
|
|
}
|