| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 |
- import 'dart:ui';
- import 'package:carousel_slider/carousel_slider.dart';
- import 'package:flutter/material.dart' hide ConnectionState;
- import 'package:flutter_screenutil/flutter_screenutil.dart';
- import 'package:get/get.dart';
- import 'package:nomo/app/constants/iconfont/iconfont.dart';
- import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
- import '../../../../config/theme/dark_theme_colors.dart';
- import '../../../../config/theme/theme_extensions/theme_extension.dart';
- import '../../../../config/translations/strings_enum.dart';
- import '../../../base/base_view.dart';
- import '../../../constants/assets.dart';
- import '../../../routes/app_pages.dart';
- import '../../../widgets/click_opacity.dart';
- import '../../../widgets/country_icon.dart';
- import '../../../widgets/ix_image.dart';
- import '../controllers/home_controller.dart';
- import '../widgets/connection_round_button.dart';
- import '../widgets/menu_list.dart';
- class HomeView extends BaseView<HomeController> {
- const HomeView({super.key});
- @override
- bool get isPopScope => true;
- @override
- Widget buildContent(BuildContext context) {
- return _buildCustomScrollView();
- }
- Widget _buildCustomScrollView() {
- return SafeArea(
- child: Padding(
- padding: EdgeInsets.symmetric(horizontal: 14.w),
- child: Column(
- children: [
- _buildAppBar(),
- 20.verticalSpaceFromWidth,
- Expanded(
- child: LayoutBuilder(
- builder: (context, constraints) {
- // 确保 viewportHeight 不会为负数(键盘弹出时)
- final double viewportHeight = (constraints.maxHeight - 388.w)
- .clamp(0.0, double.infinity);
- return GestureDetector(
- behavior: HitTestBehavior.translucent,
- onTap: () {
- controller.collapseRecentLocations();
- },
- child: SmartRefresher(
- enablePullDown: true,
- enablePullUp: false,
- controller: controller.refreshController,
- onRefresh: controller.onRefresh,
- child: CustomScrollView(
- slivers: [
- // 1. 顶部和中间部分
- SliverList(
- delegate: SliverChildListDelegate([
- 20.verticalSpaceFromWidth,
- Stack(
- children: [
- Container(
- alignment: Alignment.center,
- margin: EdgeInsets.only(top: 138.w),
- child: _buildConnectionButton(),
- ),
- _buildLocationStack(),
- ],
- ),
- 20.verticalSpaceFromWidth,
- // // 第二部分:中间(内容可长可短)
- // Container(
- // color: Colors.green[100],
- // height: 400, // 修改这个高度来测试滚动效果
- // child: Center(child: Text("中间内容区域")),
- // ),
- ]),
- ),
- // 2. 底部部分:关键所在
- SliverFillRemaining(
- hasScrollBody: false,
- child: ConstrainedBox(
- constraints: BoxConstraints(
- minHeight: viewportHeight,
- ),
- child: Column(
- children: [
- Spacer(), // 自动撑开上方空间,将底部内容挤下去
- Padding(
- padding: EdgeInsets.symmetric(
- vertical: 14.w,
- ),
- child: Obx(() {
- if (controller.bannerList.isEmpty) {
- return SizedBox.shrink();
- }
- return Stack(
- children: [
- CarouselSlider(
- options: CarouselOptions(
- height: 80.w,
- viewportFraction: 1.0,
- autoPlay:
- controller.bannerList.length >
- 1,
- autoPlayInterval: const Duration(
- seconds: 5,
- ),
- onPageChanged: (index, reason) {
- controller.currentBannerIndex =
- index;
- },
- ),
- items: controller.bannerList.map((
- banner,
- ) {
- return Builder(
- builder:
- (BuildContext context) {
- return GestureDetector(
- onTap: () => controller
- .onBannerTap(
- banner,
- ),
- child: IXImage(
- source:
- banner.img ?? '',
- width:
- double.infinity,
- height: 80.w,
- sourceType:
- ImageSourceType
- .network,
- borderRadius: 14.r,
- ),
- );
- },
- );
- }).toList(),
- ),
- if (controller.bannerList.length > 1)
- Positioned(
- bottom: 0,
- left: 0,
- right: 0,
- child: Row(
- mainAxisAlignment:
- MainAxisAlignment.center,
- children: controller.bannerList
- .asMap()
- .entries
- .map((entry) {
- return AnimatedContainer(
- duration:
- const Duration(
- milliseconds: 300,
- ),
- curve: Curves.easeInOut,
- width:
- controller
- .currentBannerIndex ==
- entry.key
- ? 16
- : 6,
- height: 6,
- margin:
- const EdgeInsets.symmetric(
- vertical: 8.0,
- horizontal: 2.0,
- ),
- decoration: BoxDecoration(
- borderRadius:
- BorderRadius.circular(
- 6,
- ),
- color: Colors.white
- .withValues(
- alpha:
- controller
- .currentBannerIndex ==
- entry
- .key
- ? 0.9
- : 0.4,
- ),
- ),
- );
- })
- .toList(),
- ),
- ),
- ],
- );
- }),
- ),
- MenuList(),
- ],
- ),
- ),
- ),
- ],
- ),
- ),
- );
- },
- ),
- ),
- ],
- ),
- ),
- );
- }
- Widget _buildAppBar() {
- return Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Obx(() {
- final bgColor = controller.apiController.userLevel == 3
- ? DarkThemeColors.homePremiumColor
- : controller.apiController.userLevel == 9999
- ? DarkThemeColors.homeTestColor
- : DarkThemeColors.homeFreeColor;
- return ClipRRect(
- borderRadius: BorderRadius.circular(100.r),
- child: ClickOpacity(
- onTap: () => Get.toNamed(Routes.SUBSCRIPTION),
- child: Container(
- decoration: BoxDecoration(
- color: bgColor.withValues(alpha: 0.05),
- ),
- child: Stack(
- children: [
- // 左上角光晕
- Positioned(
- left: -8.w,
- top: -16.w,
- child: Container(
- width: 32.w,
- height: 32.w,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- gradient: RadialGradient(
- colors: [
- bgColor.withValues(alpha: 0.85),
- bgColor.withValues(alpha: 0.05),
- ],
- stops: const [0.0, 1.0],
- ),
- ),
- ),
- ),
- // 右下角光晕
- Positioned(
- right: 12.w,
- bottom: -12.w,
- child: ImageFiltered(
- imageFilter: ImageFilter.blur(
- sigmaX: 8,
- sigmaY: 8,
- tileMode: TileMode.decal,
- ),
- child: Container(
- width: 42.w,
- height: 16.w,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(8.w),
- color: bgColor.withValues(alpha: 0.85),
- ),
- ),
- ),
- ),
- Padding(
- padding: EdgeInsets.symmetric(
- horizontal: 6.w,
- vertical: 4.w,
- ),
- child: Row(
- children: [
- Obx(
- () => IXImage(
- source: controller.apiController.userLevel == 3
- ? Assets.homePremium
- : controller.apiController.userLevel == 9999
- ? Assets.homeTest
- : Assets.homeFree,
- width: controller.apiController.userLevel == 3
- ? 92.w
- : 64.w,
- height: 28.w,
- sourceType: ImageSourceType.asset,
- ),
- ),
- _buildReminder(),
- ],
- ),
- ),
- // 倒计时提醒
- ],
- ),
- ),
- ),
- );
- }),
- ClickOpacity(
- child: Padding(
- padding: EdgeInsets.only(
- left: 10.w,
- right: 0.w,
- top: 10.w,
- bottom: 10.w,
- ),
- child: Icon(
- IconFont.icon09,
- size: 26.w,
- color: Get.reactiveTheme.hintColor,
- ),
- ),
- onTap: () {
- controller.collapseRecentLocations();
- Get.toNamed(Routes.SETTING);
- },
- ),
- ],
- );
- }
- Widget _buildReminder() {
- // return Obx(
- // () => Text(
- // controller.apiController.isGuest &&
- // !controller.apiController.isPremium &&
- // controller.apiController.remainTimeSeconds > 0
- // ? controller.apiController.remainTimeFormatted
- // : controller.coreController.timer,
- // style: TextStyle(
- // fontSize: 13.sp,
- // height: 1.5,
- // fontStyle: FontStyle.italic,
- // fontWeight: FontWeight.w500,
- // fontFeatures: [FontFeature.tabularFigures()],
- // color:
- // controller.apiController.userLevel == 3 ||
- // controller.apiController.userLevel == 9999
- // ? Get.reactiveTheme.textTheme.bodyLarge!.color
- // : Get.reactiveTheme.hintColor,
- // ),
- // ),
- // );
- return Obx(() {
- // 只监听 shouldShowCountdown 和 remainTimeFormatted
- // 只有文案或显示状态变化时才更新 UI
- if (!controller.apiController.shouldShowCountdown) {
- return const SizedBox.shrink();
- }
- return Text(
- '${controller.apiController.remainTimeFormatted} ',
- style: TextStyle(
- fontSize: 13.sp,
- height: 1.5,
- fontStyle: FontStyle.italic,
- fontWeight: FontWeight.w500,
- fontFeatures: [FontFeature.tabularFigures()],
- color:
- controller.apiController.userLevel == 3 ||
- controller.apiController.userLevel == 9999
- ? Get.reactiveTheme.textTheme.bodyLarge!.color
- : Get.reactiveTheme.hintColor,
- ),
- );
- });
- }
- Widget _buildConnectionButton() {
- return Obx(
- () => ConnectionRoundButton(
- state: controller.coreController.state,
- onTap: () {
- controller.collapseRecentLocations();
- controller.setDefaultAutoConnect();
- },
- ),
- );
- }
- /// 构建位置堆叠效果(选中位置 + 最近位置)
- Widget _buildLocationStack() {
- return Obx(() {
- if (controller.selectedLocation == null) {
- return const SizedBox.shrink();
- }
- return Stack(
- children: [
- // 最近位置列表(背景层)
- if (controller.recentLocations.isNotEmpty)
- _buildRecentLocationsCard(),
- // 选中位置(前景层)
- _buildSelectedLocationCard(),
- ],
- );
- });
- }
- /// 构建选中位置卡片
- Widget _buildSelectedLocationCard() {
- return GestureDetector(
- onTap: () {
- controller.collapseRecentLocations();
- Get.toNamed(Routes.NODE);
- },
- child: Obx(() {
- return Container(
- height: 56.w,
- width: double.maxFinite,
- padding: EdgeInsets.only(left: 16.w, right: 10.w),
- decoration: BoxDecoration(
- color: Get.reactiveTheme.highlightColor,
- borderRadius: BorderRadius.circular(12.r),
- ),
- child: Row(
- children: [
- // 国旗图标
- CountryIcon(
- countryCode: controller.selectedLocation?.country ?? '',
- width: 32.w,
- height: 24.w,
- borderRadius: 4.r,
- ),
- 10.horizontalSpace,
- // 位置名称
- Expanded(
- child: Text(
- '${controller.selectedLocation?.code ?? ''} - ${controller.selectedLocation?.name ?? ''}',
- style: TextStyle(
- fontSize: 16.sp,
- height: 1.5,
- fontWeight: FontWeight.w500,
- color: Get.reactiveTheme.textTheme.bodyLarge!.color,
- ),
- ),
- ),
- // 箭头图标
- Icon(
- IconFont.icon02,
- size: 20.w,
- color: Get.reactiveTheme.textTheme.bodyLarge!.color,
- ),
- ],
- ),
- );
- }),
- );
- }
- /// 构建最近位置卡片(支持展开/收缩)
- Widget _buildRecentLocationsCard() {
- return Obx(() {
- return Container(
- margin: EdgeInsets.symmetric(horizontal: 10.w),
- padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 56.w, bottom: 0),
- decoration: BoxDecoration(
- color: Get.reactiveTheme.cardColor,
- borderRadius: BorderRadius.circular(12.r),
- ),
- child: Column(
- children: [
- GestureDetector(
- behavior: HitTestBehavior.opaque,
- onTap: () {
- controller.isRecentLocationsExpanded =
- !controller.isRecentLocationsExpanded;
- },
- child: SizedBox(
- height: 44.w,
- child: Row(
- children: [
- Icon(
- IconFont.icon68,
- size: 16.w,
- color: Get.reactiveTheme.hintColor,
- ),
- SizedBox(width: 4.w),
- Text(
- Strings.recent.tr,
- style: TextStyle(
- fontSize: 12.sp,
- height: 1.2,
- color: Get.reactiveTheme.hintColor,
- ),
- ),
- const Spacer(),
- // 最近三个节点的国旗图标(收缩状态)或箭头(展开状态)
- Obx(() {
- return AnimatedOpacity(
- opacity: controller.isRecentLocationsExpanded
- ? 0.0
- : 1.0,
- duration: const Duration(milliseconds: 300),
- child: IgnorePointer(
- ignoring: controller.isRecentLocationsExpanded,
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- ...controller.recentLocations.take(3).map((
- location,
- ) {
- return Container(
- margin: EdgeInsets.only(right: 4.w),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(5.r),
- border: Border.all(
- color: Get.reactiveTheme.canvasColor,
- width: 0.4.w,
- ),
- ),
- child: CountryIcon(
- countryCode: location.country ?? '',
- width: 24.w,
- height: 16.w,
- borderRadius: 4.r,
- ),
- );
- }),
- ],
- ),
- ),
- );
- }),
- Obx(() {
- return AnimatedRotation(
- turns: controller.isRecentLocationsExpanded
- ? 0.25
- : 0.0,
- duration: const Duration(milliseconds: 300),
- child: Icon(
- IconFont.icon02,
- size: 20.w,
- color: Get.reactiveTheme.hintColor,
- ),
- );
- }),
- ],
- ),
- ),
- ),
- // 最近位置列表(可折叠)
- Obx(() {
- return ClipRect(
- child: AnimatedAlign(
- duration: const Duration(milliseconds: 300),
- curve: Curves.easeInOut,
- heightFactor: controller.isRecentLocationsExpanded
- ? 1.0
- : 0.0,
- alignment: Alignment.topLeft,
- child: AnimatedOpacity(
- opacity: controller.isRecentLocationsExpanded ? 1.0 : 0.0,
- duration: const Duration(milliseconds: 300),
- child: Column(
- children: controller.recentLocations.map((location) {
- return ClickOpacity(
- onTap: () {
- controller.isRecentLocationsExpanded =
- !controller.isRecentLocationsExpanded;
- controller.selectLocation(location);
- controller.handleConnect();
- },
- child: Column(
- children: [
- Divider(
- height: 1,
- color: Get.reactiveTheme.dividerColor,
- ),
- Container(
- margin: EdgeInsets.symmetric(vertical: 12.h),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- // 国旗图标
- CountryIcon(
- countryCode: location.country ?? '',
- width: 28.w,
- height: 21.w,
- borderRadius: 4.r,
- ),
- SizedBox(width: 10.w),
- // 位置信息
- Expanded(
- child: Text(
- '${location.code} - ${location.name}',
- style: TextStyle(
- fontSize: 14.sp,
- fontWeight: FontWeight.w500,
- color: Get.reactiveTheme.hintColor,
- ),
- ),
- ),
- ],
- ),
- ),
- ],
- ),
- );
- }).toList(),
- ),
- ),
- ),
- );
- }),
- ],
- ),
- );
- });
- }
- }
|