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:nomo/app/data/sp/ix_sp.dart'; import 'package:nomo/app/extensions/widget_extension.dart'; import 'package:nomo/utils/misc.dart'; import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart'; import '../../../../config/theme/dark_theme_colors.dart'; import '../../../../config/theme/light_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_theme_button.dart'; import '../widgets/menu_list.dart'; class HomeView extends BaseView { const HomeView({super.key}); @override bool get isPopScope => true; @override Widget buildContent(BuildContext context) { return _buildCustomScrollView(); } Widget _buildCustomScrollView() { return SafeArea( child: Padding( padding: isDesktop ? EdgeInsets.all(14.w) : 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(); } // 当前宽度 * 0.212 final height = MediaQuery.of(context).size.width * 0.212; return Stack( children: [ CarouselSlider( options: CarouselOptions( height: height, 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: height, sourceType: ImageSourceType .network, borderRadius: 14.r, ), ).withClickCursor( isDesktop, ); }, ); }).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(), if (controller.nineBannerList.isNotEmpty) 14.verticalSpaceFromWidth, ], ), ), ), ], ), ), ); }, ), ), ], ), ), ); } Widget _buildAppBar() { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Obx(() { final bgColor = controller.apiController.userLevel == 3 ? ReactiveTheme.isLightTheme ? LightThemeColors.homePremiumColor : DarkThemeColors.homePremiumColor : controller.apiController.userLevel == 9999 ? ReactiveTheme.isLightTheme ? LightThemeColors.homeTestColor : DarkThemeColors.homeTestColor : ReactiveTheme.isLightTheme ? LightThemeColors.homeFreeColor : DarkThemeColors.homeFreeColor; // 获取 vipRemainNotice 配置 final vipRemainNotice = (IXSP.getAppConfig()?.vipRemainNotice ?? 0) * 60; final remainTimeSeconds = controller.apiController.remainTimeSeconds; // 只有当剩余时间 > 0 且 < vipRemainNotice 时才显示提醒 final showReminder = remainTimeSeconds > 0 && remainTimeSeconds < vipRemainNotice; return ClickOpacity( onTap: () => Get.toNamed(Routes.SUBSCRIPTION), child: Stack( children: [ if (showReminder) Container( height: 28.w, padding: EdgeInsets.only(left: 32.w, right: 8.w), alignment: Alignment.center, margin: controller.apiController.userLevel == 3 ? EdgeInsets.only(left: 64.w) : EdgeInsets.only(left: 36.w), decoration: BoxDecoration( borderRadius: BorderRadius.circular(100.r), color: bgColor, ), child: _buildReminder(), ), Obx( () => IXImage( source: controller.apiController.userLevel == 3 ? controller.apiController.remainTimeSeconds > 0 ? Assets.premium : Assets.premiumExpired : controller.apiController.userLevel == 9999 ? Assets.test : Assets.free, width: controller.apiController.userLevel == 3 ? 92.w : 64.w, height: 28.w, sourceType: ImageSourceType.asset, ), ), ], ), ); }), ClickOpacity( child: Padding( padding: EdgeInsets.only( left: 10.w, right: 0.w, top: 10.w, bottom: 10.w, ), child: IXImage( source: Assets.menus, width: 26.w, height: 26.w, sourceType: ImageSourceType.asset, ), ), 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(() { // 只有文案变化时才更新 UI final textColor = controller.apiController.userLevel == 3 ? ReactiveTheme.isLightTheme ? LightThemeColors.homePremiumTextColor : DarkThemeColors.homePremiumTextColor : controller.apiController.userLevel == 9999 ? ReactiveTheme.isLightTheme ? LightThemeColors.homeTestTextColor : DarkThemeColors.homeTestTextColor : ReactiveTheme.isLightTheme ? LightThemeColors.homeFreeTextColor : DarkThemeColors.homeFreeTextColor; return Text( controller.apiController.remainTimeFormatted, style: TextStyle( fontSize: 13.sp, height: 1.5, fontStyle: FontStyle.italic, fontWeight: FontWeight.w500, fontFeatures: [FontFeature.tabularFigures()], color: textColor, ), ); }); } Widget _buildConnectionButton() { return Obx( () => ConnectionThemeButton( state: controller.coreController.state, onTap: () { controller.collapseRecentLocations(); controller.setDefaultAutoConnect(); }, ).withClickCursor(isDesktop), ); } /// 构建位置堆叠效果(选中位置 + 最近位置) 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(() { final isLight = ReactiveTheme.isLightTheme; 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), boxShadow: isLight ? [ BoxShadow( color: Colors.black.withOpacity(0.06), offset: const Offset(0, 4), blurRadius: 16, spreadRadius: 0, ), ] : null, ), 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: isDesktop ? TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w500, color: Get.reactiveTheme.textTheme.bodyLarge!.color, ) : 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, ), ], ), ).withClickCursor(isDesktop); }), ); } /// 构建最近位置卡片(支持展开/收缩) Widget _buildRecentLocationsCard() { return Obx(() { final isLight = ReactiveTheme.isLightTheme; 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), border: isLight ? Border.all(color: Get.reactiveTheme.dividerColor, width: 1.w) : null, ), 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(), ), ), ), ); }), ], ), ).withClickCursor(isDesktop); }); } }