优化:屏幕适配,数据对接
This commit is contained in:
parent
0cff1e509d
commit
e47e0800e5
@ -18,11 +18,18 @@
|
|||||||
@import sqflite_darwin;
|
@import sqflite_darwin;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<video_player_avfoundation/FVPVideoPlayerPlugin.h>)
|
||||||
|
#import <video_player_avfoundation/FVPVideoPlayerPlugin.h>
|
||||||
|
#else
|
||||||
|
@import video_player_avfoundation;
|
||||||
|
#endif
|
||||||
|
|
||||||
@implementation GeneratedPluginRegistrant
|
@implementation GeneratedPluginRegistrant
|
||||||
|
|
||||||
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||||
[FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]];
|
[FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]];
|
||||||
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
|
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
|
||||||
|
[FVPVideoPlayerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FVPVideoPlayerPlugin"]];
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
21
lib/app.dart
21
lib/app.dart
@ -1,10 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
|
import 'core/user/user_state.dart';
|
||||||
import 'features/gallery/gallery_screen.dart';
|
import 'features/gallery/gallery_screen.dart';
|
||||||
import 'features/generate_video/generate_progress_screen.dart';
|
import 'features/generate_video/generate_progress_screen.dart';
|
||||||
import 'features/generate_video/generate_video_screen.dart';
|
import 'features/generate_video/generate_video_screen.dart';
|
||||||
import 'features/generate_video/generation_result_screen.dart';
|
import 'features/generate_video/generation_result_screen.dart';
|
||||||
import 'features/home/home_screen.dart';
|
import 'features/home/home_screen.dart';
|
||||||
|
import 'features/home/models/task_item.dart';
|
||||||
import 'features/profile/profile_screen.dart';
|
import 'features/profile/profile_screen.dart';
|
||||||
import 'features/recharge/recharge_screen.dart';
|
import 'features/recharge/recharge_screen.dart';
|
||||||
import 'shared/widgets/bottom_nav_bar.dart';
|
import 'shared/widgets/bottom_nav_bar.dart';
|
||||||
@ -22,21 +25,35 @@ class _AppState extends State<App> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return UserCreditsScope(
|
||||||
|
child: MaterialApp(
|
||||||
title: 'AI Video App',
|
title: 'AI Video App',
|
||||||
theme: AppTheme.light,
|
theme: AppTheme.light,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
initialRoute: '/',
|
initialRoute: '/',
|
||||||
|
builder: (context, child) {
|
||||||
|
return SafeArea(
|
||||||
|
top: true,
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
bottom: false,
|
||||||
|
child: child ?? const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
},
|
||||||
routes: {
|
routes: {
|
||||||
'/': (_) => _MainScaffold(
|
'/': (_) => _MainScaffold(
|
||||||
currentTab: _currentTab,
|
currentTab: _currentTab,
|
||||||
onTabSelected: (tab) => setState(() => _currentTab = tab),
|
onTabSelected: (tab) => setState(() => _currentTab = tab),
|
||||||
),
|
),
|
||||||
'/recharge': (_) => const RechargeScreen(),
|
'/recharge': (_) => const RechargeScreen(),
|
||||||
'/generate': (_) => const GenerateVideoScreen(),
|
'/generate': (ctx) {
|
||||||
|
final task = ModalRoute.of(ctx)?.settings.arguments as TaskItem?;
|
||||||
|
return GenerateVideoScreen(task: task);
|
||||||
|
},
|
||||||
'/progress': (_) => const GenerateProgressScreen(),
|
'/progress': (_) => const GenerateProgressScreen(),
|
||||||
'/result': (_) => const GenerationResultScreen(),
|
'/result': (_) => const GenerationResultScreen(),
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,24 @@ import '../proxy_client.dart';
|
|||||||
abstract final class ImageApi {
|
abstract final class ImageApi {
|
||||||
static final _client = ApiClient.instance.proxy;
|
static final _client = ApiClient.instance.proxy;
|
||||||
|
|
||||||
|
/// 获取图转视频分类列表
|
||||||
|
static Future<ApiResponse> getCategoryList() async {
|
||||||
|
return _client.request(
|
||||||
|
path: '/v1/image/img2video/categories',
|
||||||
|
method: 'GET',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取图转视频任务列表
|
||||||
|
/// insignia: categoryId
|
||||||
|
static Future<ApiResponse> getImg2VideoTasks({int? insignia}) async {
|
||||||
|
return _client.request(
|
||||||
|
path: '/v1/image/img2video/tasks',
|
||||||
|
method: 'GET',
|
||||||
|
queryParams: insignia != null ? {'insignia': insignia.toString()} : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取推荐提示词
|
/// 获取推荐提示词
|
||||||
static Future<ApiResponse> getPromptRecommends({
|
static Future<ApiResponse> getPromptRecommends({
|
||||||
required String sentinel,
|
required String sentinel,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
@ -7,6 +8,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import '../api/api_client.dart';
|
import '../api/api_client.dart';
|
||||||
import '../api/proxy_client.dart';
|
import '../api/proxy_client.dart';
|
||||||
import '../api/services/user_api.dart';
|
import '../api/services/user_api.dart';
|
||||||
|
import '../user/user_state.dart';
|
||||||
|
|
||||||
/// 认证服务:APP 启动时执行快速登录
|
/// 认证服务:APP 启动时执行快速登录
|
||||||
class AuthService {
|
class AuthService {
|
||||||
@ -14,6 +16,12 @@ class AuthService {
|
|||||||
|
|
||||||
static const _tag = '[AuthService]';
|
static const _tag = '[AuthService]';
|
||||||
|
|
||||||
|
static Future<void>? _loginFuture;
|
||||||
|
|
||||||
|
/// 登录完成后的 Future,需鉴权接口应 await 此 Future 再请求
|
||||||
|
static Future<void> get loginComplete =>
|
||||||
|
_loginFuture ?? Future<void>.value();
|
||||||
|
|
||||||
static void _log(String msg) {
|
static void _log(String msg) {
|
||||||
debugPrint('$_tag $msg');
|
debugPrint('$_tag $msg');
|
||||||
}
|
}
|
||||||
@ -43,6 +51,10 @@ class AuthService {
|
|||||||
/// APP 启动时调用快速登录
|
/// APP 启动时调用快速登录
|
||||||
/// 启动时网络可能未就绪,会延迟后重试
|
/// 启动时网络可能未就绪,会延迟后重试
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
|
if (_loginFuture != null) return _loginFuture!;
|
||||||
|
final completer = Completer<void>();
|
||||||
|
_loginFuture = completer.future;
|
||||||
|
|
||||||
_log('init: 开始快速登录');
|
_log('init: 开始快速登录');
|
||||||
const maxRetries = 3;
|
const maxRetries = 3;
|
||||||
const retryDelay = Duration(seconds: 2);
|
const retryDelay = Duration(seconds: 2);
|
||||||
@ -89,12 +101,19 @@ class AuthService {
|
|||||||
} else {
|
} else {
|
||||||
_log('init: 响应中无 reevaluate (userToken)');
|
_log('init: 响应中无 reevaluate (userToken)');
|
||||||
}
|
}
|
||||||
|
final credits = data?['reveal'] as int?;
|
||||||
|
if (credits != null) {
|
||||||
|
UserState.setCredits(credits);
|
||||||
|
_log('init: 已同步积分 $credits');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_log('init: 登录失败');
|
_log('init: 登录失败');
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
_log('init: 异常 $e');
|
_log('init: 异常 $e');
|
||||||
_log('init: 堆栈 $st');
|
_log('init: 堆栈 $st');
|
||||||
|
} finally {
|
||||||
|
if (!completer.isCompleted) completer.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
78
lib/core/user/user_state.dart
Normal file
78
lib/core/user/user_state.dart
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 用户积分等全局状态
|
||||||
|
class UserState {
|
||||||
|
UserState._();
|
||||||
|
|
||||||
|
static final ValueNotifier<int?> credits = ValueNotifier<int?>(null);
|
||||||
|
|
||||||
|
static void setCredits(int? value) {
|
||||||
|
credits.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String formatCredits(int? value) {
|
||||||
|
if (value == null) return '--';
|
||||||
|
return value.toString().replaceAllMapped(
|
||||||
|
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||||
|
(m) => '${m[1]},',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提供积分数据的 InheritedWidget
|
||||||
|
class UserCreditsData extends InheritedWidget {
|
||||||
|
const UserCreditsData({
|
||||||
|
super.key,
|
||||||
|
required this.credits,
|
||||||
|
required super.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int? credits;
|
||||||
|
|
||||||
|
static UserCreditsData? of(BuildContext context) {
|
||||||
|
return context.dependOnInheritedWidgetOfExactType<UserCreditsData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
String get creditsDisplay => UserState.formatCredits(credits);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(UserCreditsData oldWidget) {
|
||||||
|
return oldWidget.credits != credits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 监听 UserState.credits 并向下提供 UserCreditsData
|
||||||
|
class UserCreditsScope extends StatefulWidget {
|
||||||
|
const UserCreditsScope({super.key, required this.child});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<UserCreditsScope> createState() => _UserCreditsScopeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UserCreditsScopeState extends State<UserCreditsScope> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
UserState.credits.addListener(_onCreditsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
UserState.credits.removeListener(_onCreditsChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCreditsChanged() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UserCreditsData(
|
||||||
|
credits: UserState.credits.value,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
|
import '../../core/user/user_state.dart';
|
||||||
import '../../core/theme/app_spacing.dart';
|
import '../../core/theme/app_spacing.dart';
|
||||||
import '../../shared/widgets/top_nav_bar.dart';
|
import '../../shared/widgets/top_nav_bar.dart';
|
||||||
|
|
||||||
@ -23,7 +25,7 @@ class GalleryScreen extends StatelessWidget {
|
|||||||
preferredSize: const Size.fromHeight(56),
|
preferredSize: const Size.fromHeight(56),
|
||||||
child: TopNavBar(
|
child: TopNavBar(
|
||||||
title: 'Gallery',
|
title: 'Gallery',
|
||||||
credits: '1,280',
|
credits: UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
||||||
onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'),
|
onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,13 +1,37 @@
|
|||||||
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
|
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
|
import '../../core/user/user_state.dart';
|
||||||
import '../../core/theme/app_spacing.dart';
|
import '../../core/theme/app_spacing.dart';
|
||||||
import '../../core/theme/app_typography.dart';
|
import '../../core/theme/app_typography.dart';
|
||||||
|
import '../../features/home/models/task_item.dart';
|
||||||
import '../../shared/widgets/top_nav_bar.dart';
|
import '../../shared/widgets/top_nav_bar.dart';
|
||||||
|
|
||||||
/// Generate Video screen - matches Pencil mmLB5
|
/// Generate Video screen - matches Pencil mmLB5
|
||||||
class GenerateVideoScreen extends StatelessWidget {
|
class GenerateVideoScreen extends StatefulWidget {
|
||||||
const GenerateVideoScreen({super.key});
|
const GenerateVideoScreen({super.key, this.task});
|
||||||
|
|
||||||
|
final TaskItem? task;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GenerateVideoScreen> createState() => _GenerateVideoScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
developer.log(
|
||||||
|
'GenerateVideoScreen opened with task: ${widget.task}',
|
||||||
|
name: 'GenerateVideoScreen',
|
||||||
|
);
|
||||||
|
debugPrint('[GenerateVideoScreen] task: ${widget.task}');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -17,8 +41,10 @@ class GenerateVideoScreen extends StatelessWidget {
|
|||||||
preferredSize: const Size.fromHeight(56),
|
preferredSize: const Size.fromHeight(56),
|
||||||
child: TopNavBar(
|
child: TopNavBar(
|
||||||
title: 'Generate Video',
|
title: 'Generate Video',
|
||||||
|
credits: UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
onBack: () => Navigator.of(context).pop(),
|
onBack: () => Navigator.of(context).pop(),
|
||||||
|
onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
@ -26,7 +52,9 @@ class GenerateVideoScreen extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_CreditsCard(credits: '1,280'),
|
_CreditsCard(
|
||||||
|
credits: UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
||||||
|
),
|
||||||
const SizedBox(height: AppSpacing.xxl),
|
const SizedBox(height: AppSpacing.xxl),
|
||||||
_UploadArea(onUpload: () {}),
|
_UploadArea(onUpload: () {}),
|
||||||
const SizedBox(height: AppSpacing.xxl),
|
const SizedBox(height: AppSpacing.xxl),
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../core/api/services/image_api.dart';
|
||||||
|
import '../../core/user/user_state.dart';
|
||||||
|
import '../../core/auth/auth_service.dart';
|
||||||
import '../../core/theme/app_spacing.dart';
|
import '../../core/theme/app_spacing.dart';
|
||||||
import '../../shared/widgets/top_nav_bar.dart';
|
import '../../shared/widgets/top_nav_bar.dart';
|
||||||
|
import 'models/category_item.dart';
|
||||||
|
import 'models/task_item.dart';
|
||||||
import 'widgets/home_tab_row.dart';
|
import 'widgets/home_tab_row.dart';
|
||||||
import 'widgets/video_card.dart';
|
import 'widgets/video_card.dart';
|
||||||
|
|
||||||
/// AI Video App home screen - matches Pencil bi8Au
|
/// AI Video App home screen - tab 来自分类接口,Grid 来自任务列表接口
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
@ -13,16 +19,67 @@ class HomeScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
class _HomeScreenState extends State<HomeScreen> {
|
||||||
HomeTab _selectedTab = HomeTab.all;
|
List<CategoryItem> _categories = [];
|
||||||
|
CategoryItem? _selectedCategory;
|
||||||
|
List<TaskItem> _tasks = [];
|
||||||
|
bool _categoriesLoading = true;
|
||||||
|
bool _tasksLoading = false;
|
||||||
|
int? _activeCardIndex;
|
||||||
|
|
||||||
static const _placeholderImages = [
|
@override
|
||||||
'https://images.unsplash.com/photo-1763929272543-0df093e4f659?w=400',
|
void initState() {
|
||||||
'https://images.unsplash.com/photo-1703592819695-ea63799b7315?w=400',
|
super.initState();
|
||||||
'https://images.unsplash.com/photo-1764787435677-1321e12559e3?w=400',
|
_loadCategories();
|
||||||
'https://images.unsplash.com/photo-1759264244741-7175af0b7e75?w=400',
|
}
|
||||||
'https://images.unsplash.com/photo-1574717024653-61fd2cf4d44d?w=400',
|
|
||||||
'https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=400',
|
Future<void> _loadCategories() async {
|
||||||
];
|
setState(() => _categoriesLoading = true);
|
||||||
|
await AuthService.loginComplete;
|
||||||
|
if (!mounted) return;
|
||||||
|
final res = await ImageApi.getCategoryList();
|
||||||
|
if (mounted) {
|
||||||
|
if (res.isSuccess && res.data is List) {
|
||||||
|
final list = (res.data as List)
|
||||||
|
.map((e) => CategoryItem.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
setState(() {
|
||||||
|
_categories = list;
|
||||||
|
_selectedCategory = list.isNotEmpty ? list.first : null;
|
||||||
|
if (_selectedCategory != null) _loadTasks(_selectedCategory!.id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() => _categories = []);
|
||||||
|
}
|
||||||
|
setState(() => _categoriesLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadTasks(int categoryId) async {
|
||||||
|
setState(() => _tasksLoading = true);
|
||||||
|
final res = await ImageApi.getImg2VideoTasks(insignia: categoryId);
|
||||||
|
if (mounted) {
|
||||||
|
if (res.isSuccess && res.data is List) {
|
||||||
|
final list = (res.data as List)
|
||||||
|
.map((e) => TaskItem.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
setState(() {
|
||||||
|
_tasks = list;
|
||||||
|
_activeCardIndex = null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() => _tasks = []);
|
||||||
|
}
|
||||||
|
setState(() => _tasksLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTabChanged(CategoryItem c) {
|
||||||
|
setState(() => _selectedCategory = c);
|
||||||
|
_loadTasks(c.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const _placeholderImage =
|
||||||
|
'https://images.unsplash.com/photo-1763929272543-0df093e4f659?w=400';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -32,7 +89,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
preferredSize: const Size.fromHeight(56),
|
preferredSize: const Size.fromHeight(56),
|
||||||
child: TopNavBar(
|
child: TopNavBar(
|
||||||
title: 'AI Video',
|
title: 'AI Video',
|
||||||
credits: '1,280',
|
credits: UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
||||||
onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'),
|
onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -43,19 +100,22 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
horizontal: AppSpacing.screenPadding,
|
horizontal: AppSpacing.screenPadding,
|
||||||
vertical: AppSpacing.xs,
|
vertical: AppSpacing.xs,
|
||||||
),
|
),
|
||||||
child: HomeTabRow(
|
child: _categoriesLoading
|
||||||
selectedTab: _selectedTab,
|
? const SizedBox(height: 40, child: Center(child: CircularProgressIndicator()))
|
||||||
onTabChanged: (tab) => setState(() => _selectedTab = tab),
|
: HomeTabRow(
|
||||||
|
categories: _categories,
|
||||||
|
selectedId: _selectedCategory?.id ?? -1,
|
||||||
|
onTabChanged: _onTabChanged,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: LayoutBuilder(
|
child: _tasksLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return Center(
|
return Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: const BoxConstraints(maxWidth: 390),
|
||||||
maxWidth: 390,
|
|
||||||
),
|
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
AppSpacing.screenPadding,
|
AppSpacing.screenPadding,
|
||||||
@ -70,12 +130,24 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
mainAxisSpacing: AppSpacing.xl,
|
mainAxisSpacing: AppSpacing.xl,
|
||||||
crossAxisSpacing: AppSpacing.xl,
|
crossAxisSpacing: AppSpacing.xl,
|
||||||
),
|
),
|
||||||
itemCount: _placeholderImages.length,
|
itemCount: _tasks.length,
|
||||||
itemBuilder: (context, index) => VideoCard(
|
itemBuilder: (context, index) {
|
||||||
imageUrl: _placeholderImages[index],
|
final task = _tasks[index];
|
||||||
|
return VideoCard(
|
||||||
|
imageUrl: task.previewImageUrl ?? _placeholderImage,
|
||||||
|
videoUrl: task.previewVideoUrl,
|
||||||
|
isActive: _activeCardIndex == index,
|
||||||
|
onPlayRequested: () =>
|
||||||
|
setState(() => _activeCardIndex = index),
|
||||||
|
onStopRequested: () =>
|
||||||
|
setState(() => _activeCardIndex = null),
|
||||||
onGenerateSimilar: () =>
|
onGenerateSimilar: () =>
|
||||||
Navigator.of(context).pushNamed('/generate'),
|
Navigator.of(context).pushNamed(
|
||||||
|
'/generate',
|
||||||
|
arguments: task,
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
20
lib/features/home/models/category_item.dart
Normal file
20
lib/features/home/models/category_item.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/// 分类项(V2 字段:federation=id, brigade=name, greylist=icon)
|
||||||
|
class CategoryItem {
|
||||||
|
const CategoryItem({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final String? icon;
|
||||||
|
|
||||||
|
factory CategoryItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
return CategoryItem(
|
||||||
|
id: json['federation'] as int? ?? 0,
|
||||||
|
name: json['brigade'] as String? ?? '',
|
||||||
|
icon: json['greylist'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
lib/features/home/models/task_item.dart
Normal file
41
lib/features/home/models/task_item.dart
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/// 图转视频任务项(V2 字段映射)
|
||||||
|
class TaskItem {
|
||||||
|
const TaskItem({
|
||||||
|
required this.templateName,
|
||||||
|
required this.title,
|
||||||
|
this.previewImageUrl,
|
||||||
|
this.previewVideoUrl,
|
||||||
|
this.imageCount = 0,
|
||||||
|
this.taskType,
|
||||||
|
this.needopt = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String templateName;
|
||||||
|
final String title;
|
||||||
|
final String? previewImageUrl;
|
||||||
|
final String? previewVideoUrl;
|
||||||
|
final int imageCount;
|
||||||
|
final String? taskType;
|
||||||
|
final bool needopt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'TaskItem(templateName: $templateName, title: $title, previewImageUrl: $previewImageUrl, '
|
||||||
|
'previewVideoUrl: $previewVideoUrl, imageCount: $imageCount, taskType: $taskType, needopt: $needopt)';
|
||||||
|
|
||||||
|
factory TaskItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
final extract = json['extract'] as Map<String, dynamic>?;
|
||||||
|
final preempt = json['preempt'] as Map<String, dynamic>?;
|
||||||
|
final imgUrl = extract?['digitize'] as String?;
|
||||||
|
final videoUrl = preempt?['digitize'] as String?;
|
||||||
|
return TaskItem(
|
||||||
|
templateName: json['congregation'] as String? ?? '',
|
||||||
|
title: json['glossary'] as String? ?? '',
|
||||||
|
previewImageUrl: imgUrl,
|
||||||
|
previewVideoUrl: videoUrl,
|
||||||
|
imageCount: json['simplify'] as int? ?? 0,
|
||||||
|
taskType: json['cipher'] as String?,
|
||||||
|
needopt: json['allowance'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,47 +1,41 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../core/theme/app_spacing.dart';
|
import '../../../core/theme/app_spacing.dart';
|
||||||
import '../../../core/theme/app_typography.dart';
|
import '../../../core/theme/app_typography.dart';
|
||||||
|
import '../models/category_item.dart';
|
||||||
|
|
||||||
enum HomeTab { all, trending, newTab }
|
/// Tab row for home screen - 使用分类列表接口数据
|
||||||
|
|
||||||
/// Tab row for home screen - matches Pencil tabRow
|
|
||||||
class HomeTabRow extends StatelessWidget {
|
class HomeTabRow extends StatelessWidget {
|
||||||
const HomeTabRow({
|
const HomeTabRow({
|
||||||
super.key,
|
super.key,
|
||||||
required this.selectedTab,
|
required this.categories,
|
||||||
|
required this.selectedId,
|
||||||
required this.onTabChanged,
|
required this.onTabChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final HomeTab selectedTab;
|
final List<CategoryItem> categories;
|
||||||
final ValueChanged<HomeTab> onTabChanged;
|
final int selectedId;
|
||||||
|
final ValueChanged<CategoryItem> onTabChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
child: Padding(
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
for (var i = 0; i < categories.length; i++) ...[
|
||||||
|
if (i > 0) const SizedBox(width: AppSpacing.md),
|
||||||
_TabChip(
|
_TabChip(
|
||||||
label: 'All',
|
label: categories[i].name,
|
||||||
isSelected: selectedTab == HomeTab.all,
|
isSelected: selectedId == categories[i].id,
|
||||||
onTap: () => onTabChanged(HomeTab.all),
|
onTap: () => onTabChanged(categories[i]),
|
||||||
),
|
|
||||||
const SizedBox(width: AppSpacing.md),
|
|
||||||
_TabChip(
|
|
||||||
label: 'Trending',
|
|
||||||
isSelected: selectedTab == HomeTab.trending,
|
|
||||||
onTap: () => onTabChanged(HomeTab.trending),
|
|
||||||
),
|
|
||||||
const SizedBox(width: AppSpacing.md),
|
|
||||||
_TabChip(
|
|
||||||
label: 'New',
|
|
||||||
isSelected: selectedTab == HomeTab.newTab,
|
|
||||||
onTap: () => onTabChanged(HomeTab.newTab),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,26 +1,156 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../core/theme/app_spacing.dart';
|
import '../../../core/theme/app_spacing.dart';
|
||||||
|
|
||||||
/// Video card for home grid - matches Pencil card1
|
/// Video card for home grid - 点击播放按钮可在卡片上播放视频
|
||||||
class VideoCard extends StatelessWidget {
|
/// 同时只能一个卡片处于播放状态
|
||||||
|
class VideoCard extends StatefulWidget {
|
||||||
const VideoCard({
|
const VideoCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.imageUrl,
|
required this.imageUrl,
|
||||||
|
this.videoUrl,
|
||||||
this.credits = '50',
|
this.credits = '50',
|
||||||
this.onTap,
|
required this.isActive,
|
||||||
|
required this.onPlayRequested,
|
||||||
|
required this.onStopRequested,
|
||||||
this.onGenerateSimilar,
|
this.onGenerateSimilar,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
|
final String? videoUrl;
|
||||||
final String credits;
|
final String credits;
|
||||||
final VoidCallback? onTap;
|
final bool isActive;
|
||||||
|
final VoidCallback onPlayRequested;
|
||||||
|
final VoidCallback onStopRequested;
|
||||||
final VoidCallback? onGenerateSimilar;
|
final VoidCallback? onGenerateSimilar;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VideoCard> createState() => _VideoCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoCardState extends State<VideoCard> {
|
||||||
|
VideoPlayerController? _controller;
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _loadError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.isActive) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _loadAndPlay());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_disposeController();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant VideoCard oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.isActive && !widget.isActive) {
|
||||||
|
_stop();
|
||||||
|
} else if (!oldWidget.isActive && widget.isActive) {
|
||||||
|
_loadAndPlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stop() {
|
||||||
|
_controller?.removeListener(_onVideoUpdate);
|
||||||
|
_controller?.pause();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_loadError = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _disposeController() {
|
||||||
|
_controller?.removeListener(_onVideoUpdate);
|
||||||
|
_controller?.dispose();
|
||||||
|
_controller = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAndPlay() async {
|
||||||
|
if (widget.videoUrl == null || widget.videoUrl!.isEmpty) {
|
||||||
|
widget.onStopRequested();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_controller != null && _controller!.value.isInitialized) {
|
||||||
|
final needSeek = _controller!.value.position >= _controller!.value.duration &&
|
||||||
|
_controller!.value.duration.inMilliseconds > 0;
|
||||||
|
if (needSeek) {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
await _controller!.seekTo(Duration.zero);
|
||||||
|
}
|
||||||
|
_controller!.addListener(_onVideoUpdate);
|
||||||
|
await _controller!.play();
|
||||||
|
if (mounted) setState(() => _isLoading = false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_loadError = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
_controller = VideoPlayerController.networkUrl(
|
||||||
|
Uri.parse(widget.videoUrl!),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await _controller!.initialize();
|
||||||
|
_controller!.addListener(_onVideoUpdate);
|
||||||
|
if (mounted && widget.isActive) {
|
||||||
|
await _controller!.play();
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
_disposeController();
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_loadError = e.toString();
|
||||||
|
});
|
||||||
|
widget.onStopRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onVideoUpdate() {
|
||||||
|
if (_controller != null &&
|
||||||
|
_controller!.value.position >= _controller!.value.duration &&
|
||||||
|
_controller!.value.duration.inMilliseconds > 0) {
|
||||||
|
_controller!.removeListener(_onVideoUpdate);
|
||||||
|
_controller!.seekTo(Duration.zero);
|
||||||
|
if (mounted) widget.onStopRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPlayButtonTap() {
|
||||||
|
if (widget.isActive) {
|
||||||
|
widget.onStopRequested();
|
||||||
|
_stop();
|
||||||
|
} else {
|
||||||
|
widget.onPlayRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _isPlaying =>
|
||||||
|
widget.isActive && _controller != null && _controller!.value.isPlaying;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final showVideo = widget.isActive && _controller != null && _controller!.value.isInitialized;
|
||||||
|
final showLoading = widget.isActive && _isLoading;
|
||||||
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -43,8 +173,24 @@ class VideoCard extends StatelessWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
|
if (showVideo)
|
||||||
|
Positioned.fill(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
child: SizedBox(
|
||||||
|
width: _controller!.value.size.width > 0
|
||||||
|
? _controller!.value.size.width
|
||||||
|
: 16,
|
||||||
|
height: _controller!.value.size.height > 0
|
||||||
|
? _controller!.value.size.height
|
||||||
|
: 9,
|
||||||
|
child: VideoPlayer(_controller!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
CachedNetworkImage(
|
CachedNetworkImage(
|
||||||
imageUrl: imageUrl,
|
imageUrl: widget.imageUrl,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (_, __) => Container(
|
placeholder: (_, __) => Container(
|
||||||
color: AppColors.surfaceAlt,
|
color: AppColors.surfaceAlt,
|
||||||
@ -75,7 +221,7 @@ class VideoCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: AppSpacing.sm),
|
const SizedBox(width: AppSpacing.sm),
|
||||||
Text(
|
Text(
|
||||||
credits,
|
widget.credits,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.surface,
|
color: AppColors.surface,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
@ -90,7 +236,9 @@ class VideoCard extends StatelessWidget {
|
|||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: onTap,
|
onTap: widget.videoUrl != null && widget.videoUrl!.isNotEmpty
|
||||||
|
? _onPlayButtonTap
|
||||||
|
: null,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
@ -105,8 +253,16 @@ class VideoCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: showLoading
|
||||||
LucideIcons.play,
|
? const Padding(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
_isPlaying ? LucideIcons.pause : LucideIcons.play,
|
||||||
size: 24,
|
size: 24,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
),
|
),
|
||||||
@ -114,12 +270,35 @@ class VideoCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (_isPlaying)
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
left: 8,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
widget.onStopRequested();
|
||||||
|
_stop();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.overlayDark,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
LucideIcons.x,
|
||||||
|
size: 16,
|
||||||
|
color: AppColors.surface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 12,
|
bottom: 12,
|
||||||
left: 12,
|
left: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: onGenerateSimilar,
|
onTap: widget.onGenerateSimilar,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 44,
|
height: 44,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
|
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
|
import '../../core/user/user_state.dart';
|
||||||
import '../../core/theme/app_spacing.dart';
|
import '../../core/theme/app_spacing.dart';
|
||||||
import '../../core/theme/app_typography.dart';
|
import '../../core/theme/app_typography.dart';
|
||||||
import '../../shared/widgets/top_nav_bar.dart';
|
|
||||||
|
|
||||||
/// Profile screen - matches Pencil KXeow
|
/// Profile screen - matches Pencil KXeow
|
||||||
class ProfileScreen extends StatelessWidget {
|
class ProfileScreen extends StatelessWidget {
|
||||||
@ -13,14 +14,6 @@ class ProfileScreen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.background,
|
backgroundColor: AppColors.background,
|
||||||
appBar: PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(56),
|
|
||||||
child: TopNavBar(
|
|
||||||
title: 'Profile',
|
|
||||||
credits: '1,280',
|
|
||||||
onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
AppSpacing.screenPadding,
|
AppSpacing.screenPadding,
|
||||||
@ -36,7 +29,7 @@ class ProfileScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.xl),
|
const SizedBox(height: AppSpacing.xl),
|
||||||
_BalanceCard(
|
_BalanceCard(
|
||||||
balance: '1,280',
|
balance: UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
||||||
onRecharge: () => Navigator.of(context).pushNamed('/recharge'),
|
onRecharge: () => Navigator.of(context).pushNamed('/recharge'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.xxl),
|
const SizedBox(height: AppSpacing.xxl),
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
|
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
|
import '../../core/user/user_state.dart';
|
||||||
import '../../core/theme/app_spacing.dart';
|
import '../../core/theme/app_spacing.dart';
|
||||||
import '../../core/theme/app_typography.dart';
|
import '../../core/theme/app_typography.dart';
|
||||||
import '../../shared/widgets/top_nav_bar.dart';
|
import '../../shared/widgets/top_nav_bar.dart';
|
||||||
@ -17,8 +19,10 @@ class RechargeScreen extends StatelessWidget {
|
|||||||
preferredSize: const Size.fromHeight(56),
|
preferredSize: const Size.fromHeight(56),
|
||||||
child: TopNavBar(
|
child: TopNavBar(
|
||||||
title: 'Recharge',
|
title: 'Recharge',
|
||||||
|
credits: UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
onBack: () => Navigator.of(context).pop(),
|
onBack: () => Navigator.of(context).pop(),
|
||||||
|
onCreditsTap: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
@ -26,7 +30,10 @@ class RechargeScreen extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_CreditsSection(currentCredits: '1,280'),
|
_CreditsSection(
|
||||||
|
currentCredits:
|
||||||
|
UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: AppSpacing.screenPaddingLarge,
|
horizontal: AppSpacing.screenPaddingLarge,
|
||||||
|
|||||||
@ -1,10 +1,19 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
import 'core/auth/auth_service.dart';
|
import 'core/auth/auth_service.dart';
|
||||||
|
import 'core/theme/app_colors.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
const SystemUiOverlayStyle(
|
||||||
|
statusBarColor: AppColors.surface,
|
||||||
|
statusBarIconBrightness: Brightness.dark,
|
||||||
|
statusBarBrightness: Brightness.light,
|
||||||
|
),
|
||||||
|
);
|
||||||
runApp(const App());
|
runApp(const App());
|
||||||
// APP 打开时后台执行快速登录
|
// APP 打开时后台执行快速登录
|
||||||
AuthService.init();
|
AuthService.init();
|
||||||
|
|||||||
@ -41,7 +41,6 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
if (showBackButton)
|
if (showBackButton)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
@ -58,13 +57,23 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
else
|
Expanded(
|
||||||
const SizedBox(width: 40),
|
child: showBackButton
|
||||||
Text(
|
? Center(
|
||||||
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: AppTypography.navTitle,
|
style: AppTypography.navTitle,
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: AppTypography.navTitle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (credits != null)
|
if (credits != null)
|
||||||
CreditsBadge(credits: credits!, onTap: onCreditsTap)
|
CreditsBadge(credits: credits!, onTap: onCreditsTap)
|
||||||
else
|
else
|
||||||
|
|||||||
@ -17,6 +17,7 @@ dependencies:
|
|||||||
device_info_plus: ^11.1.0
|
device_info_plus: ^11.1.0
|
||||||
encrypt: ^5.0.3
|
encrypt: ^5.0.3
|
||||||
http: ^1.2.2
|
http: ^1.2.2
|
||||||
|
video_player: ^2.9.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user