优化:导航栏功能移动到顶部
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",
|
"type": "frame",
|
||||||
"id": "3lNJT",
|
"id": "3lNJT",
|
||||||
@ -272,8 +184,9 @@
|
|||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "aHMps",
|
"id": "aHMps",
|
||||||
"name": "createBtn",
|
"name": "createBtn",
|
||||||
"width": 212,
|
"opacity": 0.88,
|
||||||
"height": 50,
|
"width": 186,
|
||||||
|
"height": 42,
|
||||||
"fill": {
|
"fill": {
|
||||||
"type": "gradient",
|
"type": "gradient",
|
||||||
"gradientType": "linear",
|
"gradientType": "linear",
|
||||||
@ -313,10 +226,10 @@
|
|||||||
},
|
},
|
||||||
"blur": 28
|
"blur": 28
|
||||||
},
|
},
|
||||||
"gap": 12,
|
"gap": 10,
|
||||||
"padding": [
|
"padding": [
|
||||||
14,
|
11,
|
||||||
28
|
22
|
||||||
],
|
],
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"alignItems": "center",
|
"alignItems": "center",
|
||||||
@ -325,8 +238,8 @@
|
|||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "TAocZ",
|
"id": "TAocZ",
|
||||||
"name": "plusCirc",
|
"name": "plusCirc",
|
||||||
"width": 28,
|
"width": 24,
|
||||||
"height": 28,
|
"height": 24,
|
||||||
"fill": "#FFFFFF",
|
"fill": "#FFFFFF",
|
||||||
"cornerRadius": 20,
|
"cornerRadius": 20,
|
||||||
"stroke": {
|
"stroke": {
|
||||||
@ -351,8 +264,8 @@
|
|||||||
"type": "icon_font",
|
"type": "icon_font",
|
||||||
"id": "9PFVT",
|
"id": "9PFVT",
|
||||||
"name": "plusIc",
|
"name": "plusIc",
|
||||||
"width": 14,
|
"width": 12,
|
||||||
"height": 14,
|
"height": 12,
|
||||||
"iconFontName": "plus",
|
"iconFontName": "plus",
|
||||||
"iconFontFamily": "lucide",
|
"iconFontFamily": "lucide",
|
||||||
"fill": "#B45309"
|
"fill": "#B45309"
|
||||||
@ -366,7 +279,7 @@
|
|||||||
"fill": "#1C1917",
|
"fill": "#1C1917",
|
||||||
"content": "Create Now",
|
"content": "Create Now",
|
||||||
"fontFamily": "Inter",
|
"fontFamily": "Inter",
|
||||||
"fontSize": 18,
|
"fontSize": 16,
|
||||||
"fontWeight": "800",
|
"fontWeight": "800",
|
||||||
"letterSpacing": 0.4
|
"letterSpacing": 0.4
|
||||||
}
|
}
|
||||||
@ -487,21 +400,27 @@
|
|||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "rULLj",
|
"id": "rULLj",
|
||||||
"name": "backF",
|
"name": "closeF",
|
||||||
"width": 44,
|
"width": 44,
|
||||||
"height": 44,
|
"height": 44,
|
||||||
"fill": "#00000000",
|
"fill": "#FFFFFF",
|
||||||
|
"cornerRadius": 14,
|
||||||
|
"stroke": {
|
||||||
|
"align": "inside",
|
||||||
|
"thickness": 1,
|
||||||
|
"fill": "#E7E5E4"
|
||||||
|
},
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"alignItems": "center",
|
"alignItems": "center",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "icon_font",
|
"type": "icon_font",
|
||||||
"id": "byDOX",
|
"id": "byDOX",
|
||||||
"width": 28,
|
"width": 26,
|
||||||
"height": 28,
|
"height": 26,
|
||||||
"iconFontName": "chevron-left",
|
"iconFontName": "x",
|
||||||
"iconFontFamily": "lucide",
|
"iconFontFamily": "lucide",
|
||||||
"fill": "#404040"
|
"fill": "#374151"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -1077,21 +996,27 @@
|
|||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "dYND0",
|
"id": "dYND0",
|
||||||
"name": "backCr",
|
"name": "closeCr",
|
||||||
"width": 44,
|
"width": 44,
|
||||||
"height": 44,
|
"height": 44,
|
||||||
"fill": "#00000000",
|
"fill": "#FFFFFF",
|
||||||
|
"cornerRadius": 14,
|
||||||
|
"stroke": {
|
||||||
|
"align": "inside",
|
||||||
|
"thickness": 1,
|
||||||
|
"fill": "#E7E5E4"
|
||||||
|
},
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"alignItems": "center",
|
"alignItems": "center",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "icon_font",
|
"type": "icon_font",
|
||||||
"id": "nh3Nf",
|
"id": "nh3Nf",
|
||||||
"width": 28,
|
"width": 26,
|
||||||
"height": 28,
|
"height": 26,
|
||||||
"iconFontName": "chevron-left",
|
"iconFontName": "x",
|
||||||
"iconFontFamily": "lucide",
|
"iconFontFamily": "lucide",
|
||||||
"fill": "#404040"
|
"fill": "#374151"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
import 'core/auth/auth_service.dart';
|
import 'core/auth/auth_service.dart';
|
||||||
|
import 'core/funymee_route_observer.dart';
|
||||||
import 'core/theme/app_colors.dart';
|
import 'core/theme/app_colors.dart';
|
||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
import 'features/shell/main_screen.dart';
|
import 'features/shell/main_screen.dart';
|
||||||
@ -56,6 +57,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||||||
title: widget.title,
|
title: widget.title,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: buildFunyMeeTheme(),
|
theme: buildFunyMeeTheme(),
|
||||||
|
navigatorObservers: [funymeeRouteObserver],
|
||||||
home: const MainScreen(),
|
home: const MainScreen(),
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Stack(
|
return Stack(
|
||||||
|
|||||||
@ -70,7 +70,10 @@ class AppAuthCallbacks implements AuthServiceCallbacks {
|
|||||||
@override
|
@override
|
||||||
void onCommonInfoLoaded(CommonInfoResponse data) {
|
void onCommonInfoLoaded(CommonInfoResponse data) {
|
||||||
if (data.credits != null) UserState.setCredits(data.credits!);
|
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!);
|
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 (userId != null) UserState.userId.value = userId;
|
||||||
if (credits != null) UserState.credits.value = credits;
|
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;
|
if (userName != null) UserState.userName.value = userName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -127,11 +127,8 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||||||
const EdgeInsets.symmetric(horizontal: 12),
|
const EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (widget.isRootTab)
|
PencilRoundCloseButton(
|
||||||
const SizedBox(width: 44)
|
onPressed: () => Navigator.maybePop(context),
|
||||||
else
|
|
||||||
PencilRoundBackButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|||||||
@ -8,12 +8,15 @@ import 'package:google_fonts/google_fonts.dart';
|
|||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
import '../../core/auth/auth_service.dart';
|
import '../../core/auth/auth_service.dart';
|
||||||
|
import '../../core/funymee_route_observer.dart';
|
||||||
import '../../core/open_purchase_store.dart';
|
import '../../core/open_purchase_store.dart';
|
||||||
import '../../core/user/user_state.dart';
|
import '../../core/user/user_state.dart';
|
||||||
import '../../core/video_file_cache.dart';
|
import '../../core/video_file_cache.dart';
|
||||||
import '../../design/pencil_theme.dart';
|
import '../../design/pencil_theme.dart';
|
||||||
import '../../widgets/pencil_chrome.dart';
|
import '../../widgets/pencil_chrome.dart';
|
||||||
import '../generate/generate_screen.dart';
|
import '../generate/generate_screen.dart';
|
||||||
|
import '../history/history_screen.dart';
|
||||||
|
import '../profile/profile_screen.dart';
|
||||||
|
|
||||||
/// 首页 Create Now 上方展示的预估积分:**480p 档**(与 [GenerateScreen] 选 480p 时一致)。
|
/// 首页 Create Now 上方展示的预估积分:**480p 档**(与 [GenerateScreen] 选 480p 时一致)。
|
||||||
int _homeCostDisplay480p(ExtConfigItem? t) {
|
int _homeCostDisplay480p(ExtConfigItem? t) {
|
||||||
@ -64,6 +67,29 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
|
|
||||||
int _lastCategoryTabBarCount = 0;
|
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) =>
|
List<ExtConfigItem> _visibleExtItems(ExtConfigData? ext) =>
|
||||||
ext?.items.where((e) => e.isUsableOnHome).toList() ?? [];
|
ext?.items.where((e) => e.isUsableOnHome).toList() ?? [];
|
||||||
|
|
||||||
@ -378,6 +404,21 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 56,
|
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(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
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();
|
_HomeItemVideoBackgroundState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground>
|
||||||
|
with WidgetsBindingObserver, RouteAware {
|
||||||
VideoPlayerController? _controller;
|
VideoPlayerController? _controller;
|
||||||
bool _failed = false;
|
bool _failed = false;
|
||||||
|
bool _shouldActuallyPlay = false;
|
||||||
|
bool _appInForeground = true;
|
||||||
|
|
||||||
|
/// 首页被 [Navigator.push] 盖住时 [ModalRoute.isCurrent] 不会触发 rebuild,需靠 [RouteAware] 更新。
|
||||||
|
bool _coveredByPushedRoute = false;
|
||||||
|
|
||||||
|
ModalRoute<dynamic>? _routeAwareSubscription;
|
||||||
|
|
||||||
/// 含 ExoPlayer 错误(416、UnrecognizedInputFormat、坏缓存等)时的自动重试次数。
|
/// 含 ExoPlayer 错误(416、UnrecognizedInputFormat、坏缓存等)时的自动重试次数。
|
||||||
static const int _maxOpenRetries = 6;
|
static const int _maxOpenRetries = 6;
|
||||||
@ -744,6 +807,9 @@ class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
|||||||
ImageStream? _coverImageStream;
|
ImageStream? _coverImageStream;
|
||||||
ImageStreamListener? _coverImageListener;
|
ImageStreamListener? _coverImageListener;
|
||||||
|
|
||||||
|
/// [build] 中避免同一帧多次 [addPostFrameCallback]。
|
||||||
|
bool _playbackGateCallbackPending = false;
|
||||||
|
|
||||||
String get _playUrl {
|
String get _playUrl {
|
||||||
final v = widget.item.videoUrl?.trim();
|
final v = widget.item.videoUrl?.trim();
|
||||||
if (v != null && v.isNotEmpty) return v;
|
if (v != null && v.isNotEmpty) return v;
|
||||||
@ -753,11 +819,49 @@ class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
final lifecycle = WidgetsBinding.instance.lifecycleState;
|
||||||
|
_appInForeground =
|
||||||
|
lifecycle == null || lifecycle == AppLifecycleState.resumed;
|
||||||
if (widget.isActive) {
|
if (widget.isActive) {
|
||||||
_preloadCoverThenStartVideo();
|
_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
|
@override
|
||||||
void didUpdateWidget(_HomeItemVideoBackground oldWidget) {
|
void didUpdateWidget(_HomeItemVideoBackground oldWidget) {
|
||||||
super.didUpdateWidget(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];否则网络流式播放。
|
/// 磁盘已有有效缓存则 [VideoPlayerController.file];否则网络流式播放。
|
||||||
/// 仅在 [initialize] + [play] 成功后再 [downloadFile],避免并行下载把 HTML/错包写入缓存导致 UnrecognizedInputFormat。
|
/// 仅在 [initialize] + [play] 成功后再 [downloadFile],避免并行下载把 HTML/错包写入缓存导致 UnrecognizedInputFormat。
|
||||||
Future<void> _startPlaybackAsync() async {
|
Future<void> _startPlaybackAsync() async {
|
||||||
@ -926,8 +1055,13 @@ class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
|||||||
|
|
||||||
_openRetries = 0;
|
_openRetries = 0;
|
||||||
controller.setLooping(true);
|
controller.setLooping(true);
|
||||||
|
_shouldActuallyPlay = _computeShouldActuallyPlay();
|
||||||
try {
|
try {
|
||||||
|
if (_shouldActuallyPlay) {
|
||||||
await controller.play();
|
await controller.play();
|
||||||
|
} else {
|
||||||
|
await controller.pause();
|
||||||
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (mounted) _scheduleRecoverFromError();
|
if (mounted) _scheduleRecoverFromError();
|
||||||
return;
|
return;
|
||||||
@ -982,6 +1116,9 @@ class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
funymeeRouteObserver.unsubscribe(this);
|
||||||
|
_routeAwareSubscription = null;
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_retryTimer?.cancel();
|
_retryTimer?.cancel();
|
||||||
_removeCoverListener();
|
_removeCoverListener();
|
||||||
_disposePlayback();
|
_disposePlayback();
|
||||||
@ -1025,6 +1162,15 @@ class _HomeItemVideoBackgroundState extends State<_HomeItemVideoBackground> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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) {
|
if (!widget.isActive) {
|
||||||
return SizedBox.expand(
|
return SizedBox.expand(
|
||||||
child: Stack(
|
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:client_proxy_framework/client_proxy_framework.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.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 '../../core/user/user_state.dart';
|
||||||
import '../../design/pencil_theme.dart';
|
import '../../design/pencil_theme.dart';
|
||||||
import '../../widgets/pencil_chrome.dart';
|
import '../../widgets/pencil_chrome.dart';
|
||||||
import '../purchase/purchase_screen.dart';
|
|
||||||
import '../web/app_web_view_screen.dart';
|
import '../web/app_web_view_screen.dart';
|
||||||
import 'delete_account_flow.dart';
|
import 'delete_account_flow.dart';
|
||||||
|
|
||||||
@ -51,21 +51,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
padding: const EdgeInsets.fromLTRB(2, 0, 14, 10),
|
padding: const EdgeInsets.fromLTRB(2, 0, 14, 10),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 56,
|
height: 56,
|
||||||
child: widget.isRootTab
|
child: Align(
|
||||||
? Center(
|
|
||||||
child: Text(
|
|
||||||
'Profile',
|
|
||||||
style: GoogleFonts.inter(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: PencilTheme.ink,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Align(
|
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: PencilRoundCloseButton(
|
child: PencilRoundCloseButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.maybePop(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -100,12 +89,27 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
builder: (context, url, _) {
|
builder: (context, url, _) {
|
||||||
if (url != null && url.isNotEmpty) {
|
if (url != null && url.isNotEmpty) {
|
||||||
return ClipOval(
|
return ClipOval(
|
||||||
child: Image.network(
|
child: CachedNetworkImage(
|
||||||
url,
|
imageUrl: url,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 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(),
|
_avatarFallback(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,18 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 '../home/home_screen.dart';
|
||||||
import '../profile/profile_screen.dart';
|
|
||||||
|
|
||||||
/// 底栏:极淡背景渐变 + 图标/标签 [Shadow],兼顾层次与可读性。
|
/// Root shell: home only (no bottom tab bar).
|
||||||
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).
|
|
||||||
class MainScreen extends StatefulWidget {
|
class MainScreen extends StatefulWidget {
|
||||||
const MainScreen({super.key});
|
const MainScreen({super.key});
|
||||||
|
|
||||||
@ -21,88 +11,10 @@ class MainScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MainScreenState extends State<MainScreen> {
|
class _MainScreenState extends State<MainScreen> {
|
||||||
int _index = 0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final parentNav = NavigationBarTheme.of(context);
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// All tabs: body extends under the bottom bar (faint gradient + [NavigationBar]).
|
body: const HomeScreen(),
|
||||||
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',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
class PencilCreateNowButton extends StatelessWidget {
|
||||||
const PencilCreateNowButton({super.key, required this.onPressed});
|
const PencilCreateNowButton({super.key, required this.onPressed});
|
||||||
|
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
static const double _w = 212;
|
/// `aHMps` createBtn
|
||||||
static const double _h = 50;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return Opacity(
|
||||||
|
opacity: _opacity,
|
||||||
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onPressed,
|
onTap: onPressed,
|
||||||
@ -144,16 +155,51 @@ class PencilCreateNowButton extends StatelessWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: _w,
|
width: _w,
|
||||||
height: _h,
|
height: _h,
|
||||||
child: Center(
|
child: Padding(
|
||||||
child: Text(
|
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',
|
'Create Now',
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
color: PencilTheme.stone900,
|
color: PencilTheme.stone900,
|
||||||
letterSpacing: 0.4,
|
letterSpacing: 0.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -2,7 +2,7 @@ name: funymee_ai
|
|||||||
description: "FunyMee AI Application."
|
description: "FunyMee AI Application."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.0.14+14
|
version: 1.0.15+15
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.1
|
sdk: ^3.11.1
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user