优化:导航栏功能移动到顶部
This commit is contained in:
parent
01362f00a5
commit
07a047f031
@ -162,94 +162,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "NV7Kd",
|
||||
"name": "tabRow",
|
||||
"width": "fill_container",
|
||||
"fill": "#00000000",
|
||||
"gap": 14,
|
||||
"padding": [
|
||||
6,
|
||||
0,
|
||||
16,
|
||||
0
|
||||
],
|
||||
"justifyContent": "center",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "douq8",
|
||||
"name": "t1",
|
||||
"fill": "#FFFFFF",
|
||||
"content": "Dance",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 15,
|
||||
"fontWeight": "700"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "NKz0E",
|
||||
"name": "d1",
|
||||
"fill": "#FFFFFF66",
|
||||
"content": "|",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "normal"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "AZTis",
|
||||
"name": "t2",
|
||||
"fill": "#FFFFFF",
|
||||
"content": "Dress",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 15,
|
||||
"fontWeight": "700"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "Nh9EQ",
|
||||
"name": "d2",
|
||||
"fill": "#FFFFFF66",
|
||||
"content": "|",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "normal"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "H8vvk",
|
||||
"name": "t3",
|
||||
"fill": "#FFFFFF",
|
||||
"content": "Bottle",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 15,
|
||||
"fontWeight": "700"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "DvuJh",
|
||||
"name": "d3",
|
||||
"fill": "#FFFFFF66",
|
||||
"content": "|",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "normal"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "RDOkw",
|
||||
"name": "t4",
|
||||
"fill": "#FFFFFF",
|
||||
"content": "Photo Repair",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 15,
|
||||
"fontWeight": "700"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "3lNJT",
|
||||
@ -272,8 +184,9 @@
|
||||
"type": "frame",
|
||||
"id": "aHMps",
|
||||
"name": "createBtn",
|
||||
"width": 212,
|
||||
"height": 50,
|
||||
"opacity": 0.88,
|
||||
"width": 186,
|
||||
"height": 42,
|
||||
"fill": {
|
||||
"type": "gradient",
|
||||
"gradientType": "linear",
|
||||
@ -313,10 +226,10 @@
|
||||
},
|
||||
"blur": 28
|
||||
},
|
||||
"gap": 12,
|
||||
"gap": 10,
|
||||
"padding": [
|
||||
14,
|
||||
28
|
||||
11,
|
||||
22
|
||||
],
|
||||
"justifyContent": "center",
|
||||
"alignItems": "center",
|
||||
@ -325,8 +238,8 @@
|
||||
"type": "frame",
|
||||
"id": "TAocZ",
|
||||
"name": "plusCirc",
|
||||
"width": 28,
|
||||
"height": 28,
|
||||
"width": 24,
|
||||
"height": 24,
|
||||
"fill": "#FFFFFF",
|
||||
"cornerRadius": 20,
|
||||
"stroke": {
|
||||
@ -351,8 +264,8 @@
|
||||
"type": "icon_font",
|
||||
"id": "9PFVT",
|
||||
"name": "plusIc",
|
||||
"width": 14,
|
||||
"height": 14,
|
||||
"width": 12,
|
||||
"height": 12,
|
||||
"iconFontName": "plus",
|
||||
"iconFontFamily": "lucide",
|
||||
"fill": "#B45309"
|
||||
@ -366,7 +279,7 @@
|
||||
"fill": "#1C1917",
|
||||
"content": "Create Now",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 18,
|
||||
"fontSize": 16,
|
||||
"fontWeight": "800",
|
||||
"letterSpacing": 0.4
|
||||
}
|
||||
@ -487,21 +400,27 @@
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rULLj",
|
||||
"name": "backF",
|
||||
"name": "closeF",
|
||||
"width": 44,
|
||||
"height": 44,
|
||||
"fill": "#00000000",
|
||||
"fill": "#FFFFFF",
|
||||
"cornerRadius": 14,
|
||||
"stroke": {
|
||||
"align": "inside",
|
||||
"thickness": 1,
|
||||
"fill": "#E7E5E4"
|
||||
},
|
||||
"justifyContent": "center",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "byDOX",
|
||||
"width": 28,
|
||||
"height": 28,
|
||||
"iconFontName": "chevron-left",
|
||||
"width": 26,
|
||||
"height": 26,
|
||||
"iconFontName": "x",
|
||||
"iconFontFamily": "lucide",
|
||||
"fill": "#404040"
|
||||
"fill": "#374151"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -1077,21 +996,27 @@
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "dYND0",
|
||||
"name": "backCr",
|
||||
"name": "closeCr",
|
||||
"width": 44,
|
||||
"height": 44,
|
||||
"fill": "#00000000",
|
||||
"fill": "#FFFFFF",
|
||||
"cornerRadius": 14,
|
||||
"stroke": {
|
||||
"align": "inside",
|
||||
"thickness": 1,
|
||||
"fill": "#E7E5E4"
|
||||
},
|
||||
"justifyContent": "center",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "nh3Nf",
|
||||
"width": 28,
|
||||
"height": 28,
|
||||
"iconFontName": "chevron-left",
|
||||
"width": 26,
|
||||
"height": 26,
|
||||
"iconFontName": "x",
|
||||
"iconFontFamily": "lucide",
|
||||
"fill": "#404040"
|
||||
"fill": "#374151"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import 'core/auth/auth_service.dart';
|
||||
import 'core/funymee_route_observer.dart';
|
||||
import 'core/theme/app_colors.dart';
|
||||
import 'core/theme/app_theme.dart';
|
||||
import 'features/shell/main_screen.dart';
|
||||
@ -56,6 +57,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
title: widget.title,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: buildFunyMeeTheme(),
|
||||
navigatorObservers: [funymeeRouteObserver],
|
||||
home: const MainScreen(),
|
||||
builder: (context, child) {
|
||||
return Stack(
|
||||
|
||||
@ -70,7 +70,10 @@ class AppAuthCallbacks implements AuthServiceCallbacks {
|
||||
@override
|
||||
void onCommonInfoLoaded(CommonInfoResponse data) {
|
||||
if (data.credits != null) UserState.setCredits(data.credits!);
|
||||
if (data.avatar != null) UserState.setAvatar(data.avatar!);
|
||||
if (data.avatar != null) {
|
||||
final t = data.avatar!.trim();
|
||||
UserState.setAvatar(t.isEmpty ? null : t);
|
||||
}
|
||||
if (data.userName != null) UserState.setUserName(data.userName!);
|
||||
}
|
||||
|
||||
|
||||
5
lib/core/funymee_route_observer.dart
Normal file
5
lib/core/funymee_route_observer.dart
Normal file
@ -0,0 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 供 [MaterialApp.navigatorObservers] 注册;首页视频等需在「被新路由盖住」时暂停。
|
||||
final RouteObserver<ModalRoute<dynamic>> funymeeRouteObserver =
|
||||
RouteObserver<ModalRoute<dynamic>>();
|
||||
@ -24,7 +24,10 @@ class UserState {
|
||||
}) {
|
||||
if (userId != null) UserState.userId.value = userId;
|
||||
if (credits != null) UserState.credits.value = credits;
|
||||
if (avatar != null) UserState.avatar.value = avatar;
|
||||
if (avatar != null) {
|
||||
final t = avatar.trim();
|
||||
UserState.avatar.value = t.isEmpty ? null : t;
|
||||
}
|
||||
if (userName != null) UserState.userName.value = userName;
|
||||
}
|
||||
|
||||
|
||||
@ -127,11 +127,8 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.isRootTab)
|
||||
const SizedBox(width: 44)
|
||||
else
|
||||
PencilRoundBackButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
PencilRoundCloseButton(
|
||||
onPressed: () => Navigator.maybePop(context),
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
|
||||
@ -8,12 +8,15 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../core/auth/auth_service.dart';
|
||||
import '../../core/funymee_route_observer.dart';
|
||||
import '../../core/open_purchase_store.dart';
|
||||
import '../../core/user/user_state.dart';
|
||||
import '../../core/video_file_cache.dart';
|
||||
import '../../design/pencil_theme.dart';
|
||||
import '../../widgets/pencil_chrome.dart';
|
||||
import '../generate/generate_screen.dart';
|
||||
import '../history/history_screen.dart';
|
||||
import '../profile/profile_screen.dart';
|
||||
|
||||
/// 首页 Create Now 上方展示的预估积分:**480p 档**(与 [GenerateScreen] 选 480p 时一致)。
|
||||
int _homeCostDisplay480p(ExtConfigItem? t) {
|
||||
@ -64,6 +67,29 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
int _lastCategoryTabBarCount = 0;
|
||||
|
||||
Widget _topNavIconButton({
|
||||
required IconData icon,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
width: 35,
|
||||
height: 35,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x99C4C4C4),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(icon, size: 20, color: const Color(0xFF374151)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<ExtConfigItem> _visibleExtItems(ExtConfigData? ext) =>
|
||||
ext?.items.where((e) => e.isUsableOnHome).toList() ?? [];
|
||||
|
||||
@ -378,6 +404,21 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_topNavIconButton(
|
||||
icon: Icons.history_rounded,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const HistoryScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -390,6 +431,20 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
_topNavIconButton(
|
||||
icon: Icons.person_rounded,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const ProfileScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -726,9 +781,17 @@ class _HomeItemVideoBackground extends StatefulWidget {
|
||||
_HomeItemVideoBackgroundState();
|
||||
}
|
||||
|
||||
class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
||||
class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground>
|
||||
with WidgetsBindingObserver, RouteAware {
|
||||
VideoPlayerController? _controller;
|
||||
bool _failed = false;
|
||||
bool _shouldActuallyPlay = false;
|
||||
bool _appInForeground = true;
|
||||
|
||||
/// 首页被 [Navigator.push] 盖住时 [ModalRoute.isCurrent] 不会触发 rebuild,需靠 [RouteAware] 更新。
|
||||
bool _coveredByPushedRoute = false;
|
||||
|
||||
ModalRoute<dynamic>? _routeAwareSubscription;
|
||||
|
||||
/// 含 ExoPlayer 错误(416、UnrecognizedInputFormat、坏缓存等)时的自动重试次数。
|
||||
static const int _maxOpenRetries = 6;
|
||||
@ -744,6 +807,9 @@ class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
||||
ImageStream? _coverImageStream;
|
||||
ImageStreamListener? _coverImageListener;
|
||||
|
||||
/// [build] 中避免同一帧多次 [addPostFrameCallback]。
|
||||
bool _playbackGateCallbackPending = false;
|
||||
|
||||
String get _playUrl {
|
||||
final v = widget.item.videoUrl?.trim();
|
||||
if (v != null && v.isNotEmpty) return v;
|
||||
@ -753,11 +819,49 @@ class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
final lifecycle = WidgetsBinding.instance.lifecycleState;
|
||||
_appInForeground =
|
||||
lifecycle == null || lifecycle == AppLifecycleState.resumed;
|
||||
if (widget.isActive) {
|
||||
_preloadCoverThenStartVideo();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
_appInForeground = state == AppLifecycleState.resumed;
|
||||
unawaited(_syncPlaybackGate());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final route = ModalRoute.of(context);
|
||||
if (route != null && route != _routeAwareSubscription) {
|
||||
funymeeRouteObserver.unsubscribe(this);
|
||||
funymeeRouteObserver.subscribe(this, route);
|
||||
_routeAwareSubscription = route;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didPushNext() {
|
||||
_coveredByPushedRoute = true;
|
||||
unawaited(_syncPlaybackGate());
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
_coveredByPushedRoute = false;
|
||||
unawaited(_syncPlaybackGate());
|
||||
// pop 当帧里 [ModalRoute.isCurrent] 往往尚未切回 true,补一帧再对齐播放状态。
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
unawaited(_syncPlaybackGate());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_HomeItemVideoBackground oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@ -866,6 +970,31 @@ class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _computeShouldActuallyPlay() {
|
||||
if (!mounted || !widget.isActive || !_appInForeground) return false;
|
||||
if (_coveredByPushedRoute) return false;
|
||||
// 是否被新路由盖住由 [RouteAware.didPushNext]/[didPopNext] 维护;勿依赖
|
||||
// [ModalRoute.isCurrent](pop 当帧常为 false 且不触发 rebuild,会导致无法 resume)。
|
||||
final tickerEnabled = TickerMode.valuesOf(context).enabled;
|
||||
return tickerEnabled;
|
||||
}
|
||||
|
||||
Future<void> _syncPlaybackGate() async {
|
||||
final c = _controller;
|
||||
if (c == null) return;
|
||||
if (!c.value.isInitialized) return;
|
||||
final shouldPlay = _computeShouldActuallyPlay();
|
||||
if (shouldPlay == _shouldActuallyPlay) return;
|
||||
_shouldActuallyPlay = shouldPlay;
|
||||
try {
|
||||
if (shouldPlay) {
|
||||
await c.play();
|
||||
} else {
|
||||
await c.pause();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// 磁盘已有有效缓存则 [VideoPlayerController.file];否则网络流式播放。
|
||||
/// 仅在 [initialize] + [play] 成功后再 [downloadFile],避免并行下载把 HTML/错包写入缓存导致 UnrecognizedInputFormat。
|
||||
Future<void> _startPlaybackAsync() async {
|
||||
@ -926,8 +1055,13 @@ class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
||||
|
||||
_openRetries = 0;
|
||||
controller.setLooping(true);
|
||||
_shouldActuallyPlay = _computeShouldActuallyPlay();
|
||||
try {
|
||||
if (_shouldActuallyPlay) {
|
||||
await controller.play();
|
||||
} else {
|
||||
await controller.pause();
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) _scheduleRecoverFromError();
|
||||
return;
|
||||
@ -982,6 +1116,9 @@ class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
funymeeRouteObserver.unsubscribe(this);
|
||||
_routeAwareSubscription = null;
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_retryTimer?.cancel();
|
||||
_removeCoverListener();
|
||||
_disposePlayback();
|
||||
@ -1025,6 +1162,15 @@ class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shouldPlayNow = _computeShouldActuallyPlay();
|
||||
if (shouldPlayNow != _shouldActuallyPlay && !_playbackGateCallbackPending) {
|
||||
_playbackGateCallbackPending = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_playbackGateCallbackPending = false;
|
||||
if (!mounted) return;
|
||||
unawaited(_syncPlaybackGate());
|
||||
});
|
||||
}
|
||||
if (!widget.isActive) {
|
||||
return SizedBox.expand(
|
||||
child: Stack(
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:client_proxy_framework/client_proxy_framework.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@ -9,7 +10,6 @@ import '../../core/ext_config_document_urls.dart';
|
||||
import '../../core/user/user_state.dart';
|
||||
import '../../design/pencil_theme.dart';
|
||||
import '../../widgets/pencil_chrome.dart';
|
||||
import '../purchase/purchase_screen.dart';
|
||||
import '../web/app_web_view_screen.dart';
|
||||
import 'delete_account_flow.dart';
|
||||
|
||||
@ -51,21 +51,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
padding: const EdgeInsets.fromLTRB(2, 0, 14, 10),
|
||||
child: SizedBox(
|
||||
height: 56,
|
||||
child: widget.isRootTab
|
||||
? Center(
|
||||
child: Text(
|
||||
'Profile',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: PencilTheme.ink,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Align(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: PencilRoundCloseButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
onPressed: () => Navigator.maybePop(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -100,12 +89,27 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
builder: (context, url, _) {
|
||||
if (url != null && url.isNotEmpty) {
|
||||
return ClipOval(
|
||||
child: Image.network(
|
||||
url,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: url,
|
||||
fit: BoxFit.cover,
|
||||
width: 100,
|
||||
height: 100,
|
||||
errorBuilder: (_, _, _) =>
|
||||
memCacheWidth: 200,
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: Duration.zero,
|
||||
placeholder: (context, url) => Center(
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: PencilTheme
|
||||
.profileAvatarIcon
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) =>
|
||||
_avatarFallback(),
|
||||
),
|
||||
);
|
||||
|
||||
@ -1,18 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../history/history_screen.dart';
|
||||
import '../home/home_screen.dart';
|
||||
import '../profile/profile_screen.dart';
|
||||
|
||||
/// 底栏:极淡背景渐变 + 图标/标签 [Shadow],兼顾层次与可读性。
|
||||
const List<Shadow> _bottomNavItemShadows = [
|
||||
Shadow(offset: Offset(0, 1), blurRadius: 3, color: Color(0x72000000)),
|
||||
Shadow(offset: Offset(0, 0), blurRadius: 8, color: Color(0x40000000)),
|
||||
];
|
||||
|
||||
/// Root shell: bottom tabs **Home**, **History**, **Profile** (English labels).
|
||||
/// Root shell: home only (no bottom tab bar).
|
||||
class MainScreen extends StatefulWidget {
|
||||
const MainScreen({super.key});
|
||||
|
||||
@ -21,88 +11,10 @@ class MainScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MainScreenState extends State<MainScreen> {
|
||||
int _index = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final parentNav = NavigationBarTheme.of(context);
|
||||
return Scaffold(
|
||||
// All tabs: body extends under the bottom bar (faint gradient + [NavigationBar]).
|
||||
extendBody: true,
|
||||
body: IndexedStack(
|
||||
index: _index,
|
||||
children: [
|
||||
const HomeScreen(),
|
||||
HistoryScreen(isRootTab: true, isTabSelected: _index == 1),
|
||||
const ProfileScreen(isRootTab: true),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
AppColors.surface.withValues(alpha: 0.40),
|
||||
AppColors.surface.withValues(alpha: 0.15),
|
||||
AppColors.surface.withValues(alpha: 0),
|
||||
],
|
||||
stops: const [0.0, 0.48, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationBarTheme(
|
||||
data: parentNav.copyWith(
|
||||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||
final selected = states.contains(WidgetState.selected);
|
||||
return GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: selected ? AppColors.primary : AppColors.onSurface,
|
||||
shadows: _bottomNavItemShadows,
|
||||
);
|
||||
}),
|
||||
iconTheme: WidgetStateProperty.resolveWith((states) {
|
||||
final selected = states.contains(WidgetState.selected);
|
||||
return IconThemeData(
|
||||
color: selected ? AppColors.primary : AppColors.onSurface,
|
||||
shadows: _bottomNavItemShadows,
|
||||
);
|
||||
}),
|
||||
),
|
||||
child: NavigationBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
selectedIndex: _index,
|
||||
onDestinationSelected: (i) => setState(() => _index = i),
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home_rounded),
|
||||
label: 'Home',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.history_outlined),
|
||||
selectedIcon: Icon(Icons.history_rounded),
|
||||
label: 'History',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.person_outline_rounded),
|
||||
selectedIcon: Icon(Icons.person_rounded),
|
||||
label: 'Profile',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: const HomeScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,18 +100,29 @@ class PencilGlassCreditsPill extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// bi8Au Create Now:与 `desgin/funymee_home.pen` [aHMps] 一致 — 金渐变、白描边、投影。
|
||||
/// bi8Au Create Now:与 `desgin/funymee_home.pen` [aHMps] 同步(含半透明、尺寸、左加号、间距)。
|
||||
class PencilCreateNowButton extends StatelessWidget {
|
||||
const PencilCreateNowButton({super.key, required this.onPressed});
|
||||
|
||||
final VoidCallback onPressed;
|
||||
|
||||
static const double _w = 212;
|
||||
static const double _h = 50;
|
||||
/// `aHMps` createBtn
|
||||
static const double _w = 186;
|
||||
static const double _h = 42;
|
||||
|
||||
/// `aHMps` opacity
|
||||
static const double _opacity = 0.88;
|
||||
|
||||
/// `aHMps` gap;横向 padding 取 pen `padding` 中 22;竖直取 `(42-24)/2` 以垂直居中 24×24 左标。
|
||||
static const double _gap = 10;
|
||||
static const EdgeInsets _padding =
|
||||
EdgeInsets.symmetric(horizontal: 22, vertical: 9);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
return Opacity(
|
||||
opacity: _opacity,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
@ -144,16 +155,51 @@ class PencilCreateNowButton extends StatelessWidget {
|
||||
child: SizedBox(
|
||||
width: _w,
|
||||
height: _h,
|
||||
child: Center(
|
||||
child: Text(
|
||||
child: Padding(
|
||||
padding: _padding,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: Color(0x99F59E0B),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x14000000),
|
||||
offset: Offset(0, 2),
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: Icon(
|
||||
Icons.add_rounded,
|
||||
size: 12,
|
||||
color: Color(0xFFB45309),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _gap),
|
||||
Text(
|
||||
'Create Now',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: PencilTheme.stone900,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -2,7 +2,7 @@ name: funymee_ai
|
||||
description: "FunyMee AI Application."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.14+14
|
||||
version: 1.0.15+15
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.1
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user