新增:视频生成接口对接
This commit is contained in:
parent
e47e0800e5
commit
9ced4f24e7
@ -60,6 +60,11 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'com.google.android.gms:play-services-ads-identifier:18.1.0'
|
||||||
|
implementation 'com.android.installreferrer:installreferrer:2.2'
|
||||||
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
source '../..'
|
source '../..'
|
||||||
}
|
}
|
||||||
|
|||||||
10
docs/generate_video.md
Normal file
10
docs/generate_video.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# UI 开发流程
|
||||||
|
当点击中间upload an image as the base for generation 区域的时候,调用手机相册功能选择一张图片,一次只允许一张。用户选完图片后图片显示在 upload an image as the base for generation区域,并隐藏提示信息。
|
||||||
|
当点击Generate Video的时候如果用户没有选择图片择提示用户选择图片,如果已经选择了图片则开始按下面的步骤调用接口
|
||||||
|
# 接口调用
|
||||||
|
## 第一步
|
||||||
|
通过/v1/image/upload-presigned-url接口获取文件上传地址,通常是uploadUrl1或者uploadUrl2,以及对应的filePath1和filePath2用于生成视频接口的入参
|
||||||
|
## 第二步
|
||||||
|
通过上一个接口获取的upload url 用put协议将图片上传到对应的地址,注意:图片上传不需要经过代理 并且需要将requiredHeaders的参数全部设置到请求头中
|
||||||
|
## 第三步
|
||||||
|
调用/v1/image/create-task接口并传入filePath1或者filePath2传入到srcImg1Url或者srcImg2Url
|
||||||
@ -6,12 +6,24 @@
|
|||||||
|
|
||||||
#import "GeneratedPluginRegistrant.h"
|
#import "GeneratedPluginRegistrant.h"
|
||||||
|
|
||||||
|
#if __has_include(<adjust_sdk/AdjustSdk.h>)
|
||||||
|
#import <adjust_sdk/AdjustSdk.h>
|
||||||
|
#else
|
||||||
|
@import adjust_sdk;
|
||||||
|
#endif
|
||||||
|
|
||||||
#if __has_include(<device_info_plus/FPPDeviceInfoPlusPlugin.h>)
|
#if __has_include(<device_info_plus/FPPDeviceInfoPlusPlugin.h>)
|
||||||
#import <device_info_plus/FPPDeviceInfoPlusPlugin.h>
|
#import <device_info_plus/FPPDeviceInfoPlusPlugin.h>
|
||||||
#else
|
#else
|
||||||
@import device_info_plus;
|
@import device_info_plus;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<image_picker_ios/FLTImagePickerPlugin.h>)
|
||||||
|
#import <image_picker_ios/FLTImagePickerPlugin.h>
|
||||||
|
#else
|
||||||
|
@import image_picker_ios;
|
||||||
|
#endif
|
||||||
|
|
||||||
#if __has_include(<sqflite_darwin/SqflitePlugin.h>)
|
#if __has_include(<sqflite_darwin/SqflitePlugin.h>)
|
||||||
#import <sqflite_darwin/SqflitePlugin.h>
|
#import <sqflite_darwin/SqflitePlugin.h>
|
||||||
#else
|
#else
|
||||||
@ -27,7 +39,9 @@
|
|||||||
@implementation GeneratedPluginRegistrant
|
@implementation GeneratedPluginRegistrant
|
||||||
|
|
||||||
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||||
|
[AdjustSdk registerWithRegistrar:[registry registrarForPlugin:@"AdjustSdk"]];
|
||||||
[FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]];
|
[FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]];
|
||||||
|
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
|
||||||
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
|
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
|
||||||
[FVPVideoPlayerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FVPVideoPlayerPlugin"]];
|
[FVPVideoPlayerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FVPVideoPlayerPlugin"]];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,7 +50,10 @@ class _AppState extends State<App> {
|
|||||||
final task = ModalRoute.of(ctx)?.settings.arguments as TaskItem?;
|
final task = ModalRoute.of(ctx)?.settings.arguments as TaskItem?;
|
||||||
return GenerateVideoScreen(task: task);
|
return GenerateVideoScreen(task: task);
|
||||||
},
|
},
|
||||||
'/progress': (_) => const GenerateProgressScreen(),
|
'/progress': (ctx) {
|
||||||
|
final taskId = ModalRoute.of(ctx)?.settings.arguments;
|
||||||
|
return GenerateProgressScreen(taskId: taskId);
|
||||||
|
},
|
||||||
'/result': (_) => const GenerationResultScreen(),
|
'/result': (_) => const GenerationResultScreen(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@ -127,6 +127,54 @@ abstract final class ImageApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取预签名上传 URL(图片上传不经过代理,直接 PUT 到返回的 URL)
|
||||||
|
static Future<ApiResponse> getUploadPresignedUrl({
|
||||||
|
required String fileName1,
|
||||||
|
String? fileName2,
|
||||||
|
required String contentType,
|
||||||
|
required int expectedSize,
|
||||||
|
}) async {
|
||||||
|
return _client.request(
|
||||||
|
path: '/v1/image/upload-presigned-url',
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
'gateway': fileName1,
|
||||||
|
'action': fileName2 ?? '',
|
||||||
|
'pauldron': contentType,
|
||||||
|
'stronghold': expectedSize,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建生图/视频任务
|
||||||
|
static Future<ApiResponse> createTask({
|
||||||
|
required String asset,
|
||||||
|
String? guild,
|
||||||
|
String? commission,
|
||||||
|
String? ledger,
|
||||||
|
String? cipher,
|
||||||
|
String? heatmap,
|
||||||
|
String? congregation,
|
||||||
|
bool allowance = false,
|
||||||
|
String? ext,
|
||||||
|
}) async {
|
||||||
|
return _client.request(
|
||||||
|
path: '/v1/image/create-task',
|
||||||
|
method: 'POST',
|
||||||
|
queryParams: {'asset': asset},
|
||||||
|
body: {
|
||||||
|
if (guild != null) 'guild': guild,
|
||||||
|
if (commission != null) 'commission': commission,
|
||||||
|
if (ledger != null) 'ledger': ledger,
|
||||||
|
if (cipher != null) 'cipher': cipher,
|
||||||
|
if (heatmap != null) 'heatmap': heatmap,
|
||||||
|
if (congregation != null) 'congregation': congregation,
|
||||||
|
if (ext != null) 'nexus': ext,
|
||||||
|
'allowance': allowance,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取积分页面信息
|
/// 获取积分页面信息
|
||||||
static Future<ApiResponse> getCreditsPageInfo({
|
static Future<ApiResponse> getCreditsPageInfo({
|
||||||
required String sentinel,
|
required String sentinel,
|
||||||
|
|||||||
@ -8,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 '../referrer/referrer_service.dart';
|
||||||
import '../user/user_state.dart';
|
import '../user/user_state.dart';
|
||||||
|
|
||||||
/// 认证服务:APP 启动时执行快速登录
|
/// 认证服务:APP 启动时执行快速登录
|
||||||
@ -69,6 +70,11 @@ class AuthService {
|
|||||||
final sign = _computeSign(deviceId);
|
final sign = _computeSign(deviceId);
|
||||||
_log('init: sign=$sign');
|
_log('init: sign=$sign');
|
||||||
|
|
||||||
|
final crest = await ReferrerService.getReferrer();
|
||||||
|
if (crest != null && crest.isNotEmpty) {
|
||||||
|
_log('init: crest(referrer)=$crest');
|
||||||
|
}
|
||||||
|
|
||||||
ApiResponse? res;
|
ApiResponse? res;
|
||||||
for (var i = 0; i < maxRetries; i++) {
|
for (var i = 0; i < maxRetries; i++) {
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
@ -79,7 +85,8 @@ class AuthService {
|
|||||||
res = await UserApi.fastLogin(
|
res = await UserApi.fastLogin(
|
||||||
origin: deviceId,
|
origin: deviceId,
|
||||||
resolution: sign,
|
resolution: sign,
|
||||||
digest: '',
|
digest: crest ?? '',
|
||||||
|
crest: crest,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -106,6 +113,11 @@ class AuthService {
|
|||||||
UserState.setCredits(credits);
|
UserState.setCredits(credits);
|
||||||
_log('init: 已同步积分 $credits');
|
_log('init: 已同步积分 $credits');
|
||||||
}
|
}
|
||||||
|
final uid = data?['asset'] as String?;
|
||||||
|
if (uid != null && uid.isNotEmpty) {
|
||||||
|
UserState.setUserId(uid);
|
||||||
|
_log('init: 已设置 userId');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_log('init: 登录失败');
|
_log('init: 登录失败');
|
||||||
}
|
}
|
||||||
|
|||||||
38
lib/core/referrer/referrer_service.dart
Normal file
38
lib/core/referrer/referrer_service.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:play_install_referrer/play_install_referrer.dart';
|
||||||
|
|
||||||
|
/// 安装来源 referrer 服务(用于 ch/crest 渠道参数)
|
||||||
|
class ReferrerService {
|
||||||
|
ReferrerService._();
|
||||||
|
|
||||||
|
static String? _cachedReferrer;
|
||||||
|
static final Completer<String?> _completer = Completer<String?>();
|
||||||
|
|
||||||
|
/// 获取 referrer,Android 使用 Google Play Install Referrer,iOS 返回空
|
||||||
|
static Future<String?> getReferrer() async {
|
||||||
|
if (_cachedReferrer != null) return _cachedReferrer;
|
||||||
|
if (_completer.isCompleted) return _completer.future;
|
||||||
|
|
||||||
|
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||||
|
_cachedReferrer = '';
|
||||||
|
if (!_completer.isCompleted) _completer.complete('');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final details = await PlayInstallReferrer.installReferrer;
|
||||||
|
_cachedReferrer = details.installReferrer ?? '';
|
||||||
|
} catch (_) {
|
||||||
|
_cachedReferrer = '';
|
||||||
|
}
|
||||||
|
if (!_completer.isCompleted) _completer.complete(_cachedReferrer);
|
||||||
|
return _cachedReferrer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化并缓存 referrer(建议在 app 启动时调用)
|
||||||
|
static Future<void> init() async {
|
||||||
|
await getReferrer();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,11 +5,16 @@ class UserState {
|
|||||||
UserState._();
|
UserState._();
|
||||||
|
|
||||||
static final ValueNotifier<int?> credits = ValueNotifier<int?>(null);
|
static final ValueNotifier<int?> credits = ValueNotifier<int?>(null);
|
||||||
|
static final ValueNotifier<String?> userId = ValueNotifier<String?>(null);
|
||||||
|
|
||||||
static void setCredits(int? value) {
|
static void setCredits(int? value) {
|
||||||
credits.value = value;
|
credits.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void setUserId(String? value) {
|
||||||
|
userId.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
static String formatCredits(int? value) {
|
static String formatCredits(int? value) {
|
||||||
if (value == null) return '--';
|
if (value == null) return '--';
|
||||||
return value.toString().replaceAllMapped(
|
return value.toString().replaceAllMapped(
|
||||||
|
|||||||
@ -1,20 +1,91 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
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/api/api_config.dart';
|
||||||
|
import '../../core/auth/auth_service.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 '../../core/user/user_state.dart';
|
||||||
import '../../shared/widgets/top_nav_bar.dart';
|
import '../../shared/widgets/top_nav_bar.dart';
|
||||||
|
|
||||||
|
import '../../core/api/services/image_api.dart';
|
||||||
|
|
||||||
/// Generate Video Progress screen - matches Pencil qGs6n
|
/// Generate Video Progress screen - matches Pencil qGs6n
|
||||||
class GenerateProgressScreen extends StatefulWidget {
|
class GenerateProgressScreen extends StatefulWidget {
|
||||||
const GenerateProgressScreen({super.key});
|
const GenerateProgressScreen({super.key, this.taskId});
|
||||||
|
|
||||||
|
final dynamic taskId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GenerateProgressScreen> createState() => _GenerateProgressScreenState();
|
State<GenerateProgressScreen> createState() => _GenerateProgressScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
|
class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
|
||||||
double _progress = 0.45;
|
double _progress = 0;
|
||||||
|
Timer? _pollTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.taskId != null) {
|
||||||
|
_startPolling();
|
||||||
|
} else {
|
||||||
|
_progress = 0.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pollTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startPolling() async {
|
||||||
|
await AuthService.loginComplete;
|
||||||
|
|
||||||
|
_pollTimer = Timer.periodic(const Duration(seconds: 2), (_) => _fetchProgress());
|
||||||
|
_fetchProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchProgress() async {
|
||||||
|
if (widget.taskId == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final res = await ImageApi.getProgress(
|
||||||
|
sentinel: ApiConfig.appId,
|
||||||
|
tree: widget.taskId.toString(),
|
||||||
|
asset: UserState.userId.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.isSuccess || res.data == null) return;
|
||||||
|
|
||||||
|
final data = res.data as Map<String, dynamic>;
|
||||||
|
final progress = (data['dice'] as num?)?.toInt() ?? 0;
|
||||||
|
final state = (data['listing'] as num?)?.toInt();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_progress = progress / 100.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (progress >= 100 || state == 2) {
|
||||||
|
_pollTimer?.cancel();
|
||||||
|
Navigator.of(context).pushReplacementNamed('/result', arguments: widget.taskId);
|
||||||
|
} else if (state == 3) {
|
||||||
|
_pollTimer?.cancel();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Generation failed'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -133,4 +204,3 @@ class _ProgressSection extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
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:http/http.dart' as http;
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
import '../../core/auth/auth_service.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 '../../core/user/user_state.dart';
|
||||||
import '../../features/home/models/task_item.dart';
|
import '../../features/home/models/task_item.dart';
|
||||||
import '../../shared/widgets/top_nav_bar.dart';
|
import '../../shared/widgets/top_nav_bar.dart';
|
||||||
|
|
||||||
|
import '../../core/api/services/image_api.dart';
|
||||||
|
|
||||||
/// Generate Video screen - matches Pencil mmLB5
|
/// Generate Video screen - matches Pencil mmLB5
|
||||||
class GenerateVideoScreen extends StatefulWidget {
|
class GenerateVideoScreen extends StatefulWidget {
|
||||||
const GenerateVideoScreen({super.key, this.task});
|
const GenerateVideoScreen({super.key, this.task});
|
||||||
@ -20,7 +27,27 @@ class GenerateVideoScreen extends StatefulWidget {
|
|||||||
State<GenerateVideoScreen> createState() => _GenerateVideoScreenState();
|
State<GenerateVideoScreen> createState() => _GenerateVideoScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum _Resolution { p480, p720 }
|
||||||
|
|
||||||
class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
||||||
|
File? _selectedImage;
|
||||||
|
_Resolution _selectedResolution = _Resolution.p480;
|
||||||
|
bool _isGenerating = false;
|
||||||
|
|
||||||
|
int get _currentCredits {
|
||||||
|
final task = widget.task;
|
||||||
|
if (task == null) return 50;
|
||||||
|
switch (_selectedResolution) {
|
||||||
|
case _Resolution.p480:
|
||||||
|
return task.credits480p ?? 50;
|
||||||
|
case _Resolution.p720:
|
||||||
|
return task.credits720p ?? 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _heatmap =>
|
||||||
|
_selectedResolution == _Resolution.p480 ? '480p' : '720p';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -33,6 +60,164 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showImageSourcePicker() async {
|
||||||
|
final source = await showModalBottomSheet<ImageSource>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(LucideIcons.image),
|
||||||
|
title: const Text('Choose from gallery'),
|
||||||
|
onTap: () => Navigator.pop(context, ImageSource.gallery),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(LucideIcons.camera),
|
||||||
|
title: const Text('Take photo'),
|
||||||
|
onTap: () => Navigator.pop(context, ImageSource.camera),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (source != null && mounted) {
|
||||||
|
_pickImage(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickImage(ImageSource source) async {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final picked = await picker.pickImage(
|
||||||
|
source: source,
|
||||||
|
imageQuality: 85,
|
||||||
|
);
|
||||||
|
if (picked != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_selectedImage = File(picked.path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onGenerate() async {
|
||||||
|
if (_selectedImage == null) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Please select an image first'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isGenerating = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AuthService.loginComplete;
|
||||||
|
|
||||||
|
final file = _selectedImage!;
|
||||||
|
final size = await file.length();
|
||||||
|
final ext = file.path.split('.').last.toLowerCase();
|
||||||
|
final contentType = ext == 'png'
|
||||||
|
? 'image/png'
|
||||||
|
: ext == 'gif'
|
||||||
|
? 'image/gif'
|
||||||
|
: 'image/jpeg';
|
||||||
|
final fileName = 'img_${DateTime.now().millisecondsSinceEpoch}.$ext';
|
||||||
|
|
||||||
|
final presignedRes = await ImageApi.getUploadPresignedUrl(
|
||||||
|
fileName1: fileName,
|
||||||
|
contentType: contentType,
|
||||||
|
expectedSize: size,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!presignedRes.isSuccess || presignedRes.data == null) {
|
||||||
|
throw Exception(presignedRes.msg.isNotEmpty
|
||||||
|
? presignedRes.msg
|
||||||
|
: 'Failed to get upload URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = presignedRes.data as Map<String, dynamic>;
|
||||||
|
final uploadUrl =
|
||||||
|
data['bolster'] as String? ?? data['expound'] as String?;
|
||||||
|
final filePath = data['recruit'] as String? ?? data['train'] as String?;
|
||||||
|
final requiredHeaders = data['remap'] as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
if (uploadUrl == null ||
|
||||||
|
uploadUrl.isEmpty ||
|
||||||
|
filePath == null ||
|
||||||
|
filePath.isEmpty) {
|
||||||
|
throw Exception('Invalid presigned URL response');
|
||||||
|
}
|
||||||
|
|
||||||
|
final headers = <String, String>{};
|
||||||
|
if (requiredHeaders != null) {
|
||||||
|
for (final e in requiredHeaders.entries) {
|
||||||
|
headers[e.key] = e.value?.toString() ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!headers.containsKey('Content-Type')) {
|
||||||
|
headers['Content-Type'] = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
final bytes = await file.readAsBytes();
|
||||||
|
final uploadResponse = await http.put(
|
||||||
|
Uri.parse(uploadUrl),
|
||||||
|
headers: headers,
|
||||||
|
body: bytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uploadResponse.statusCode < 200 || uploadResponse.statusCode >= 300) {
|
||||||
|
throw Exception('Upload failed: ${uploadResponse.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = UserState.userId.value;
|
||||||
|
if (userId == null || userId.isEmpty) {
|
||||||
|
throw Exception('User not logged in');
|
||||||
|
}
|
||||||
|
|
||||||
|
final createRes = await ImageApi.createTask(
|
||||||
|
asset: userId,
|
||||||
|
guild: filePath,
|
||||||
|
allowance: false,
|
||||||
|
cipher: widget.task?.taskType ?? '',
|
||||||
|
congregation: widget.task?.templateName ?? '',
|
||||||
|
heatmap: _heatmap,
|
||||||
|
ext: widget.task?.ext,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!createRes.isSuccess) {
|
||||||
|
throw Exception(
|
||||||
|
createRes.msg.isNotEmpty ? createRes.msg : 'Failed to create task');
|
||||||
|
}
|
||||||
|
|
||||||
|
final taskData = createRes.data as Map<String, dynamic>?;
|
||||||
|
final taskId = taskData?['tree'];
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.of(context)
|
||||||
|
.pushReplacementNamed('/progress', arguments: taskId);
|
||||||
|
} catch (e, st) {
|
||||||
|
developer.log('Generate failed: $e', name: 'GenerateVideoScreen');
|
||||||
|
debugPrint('[GenerateVideoScreen] error: $e\n$st');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Generation failed: ${e.toString().replaceAll('Exception: ', '')}'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isGenerating = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -56,11 +241,20 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
|||||||
credits: UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
credits: UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.xxl),
|
const SizedBox(height: AppSpacing.xxl),
|
||||||
_UploadArea(onUpload: () {}),
|
_UploadArea(
|
||||||
|
selectedImage: _selectedImage,
|
||||||
|
onUpload: _showImageSourcePicker,
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppSpacing.xxl),
|
||||||
|
_ResolutionToggle(
|
||||||
|
selected: _selectedResolution,
|
||||||
|
onChanged: (r) => setState(() => _selectedResolution = r),
|
||||||
|
),
|
||||||
const SizedBox(height: AppSpacing.xxl),
|
const SizedBox(height: AppSpacing.xxl),
|
||||||
_GenerateButton(
|
_GenerateButton(
|
||||||
onGenerate: () =>
|
onGenerate: _onGenerate,
|
||||||
Navigator.of(context).pushReplacementNamed('/progress'),
|
isLoading: _isGenerating,
|
||||||
|
credits: _currentCredits.toString(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -125,8 +319,12 @@ class _CreditsCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _UploadArea extends StatelessWidget {
|
class _UploadArea extends StatelessWidget {
|
||||||
const _UploadArea({required this.onUpload});
|
const _UploadArea({
|
||||||
|
required this.selectedImage,
|
||||||
|
required this.onUpload,
|
||||||
|
});
|
||||||
|
|
||||||
|
final File? selectedImage;
|
||||||
final VoidCallback onUpload;
|
final VoidCallback onUpload;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -143,93 +341,201 @@ class _UploadArea extends StatelessWidget {
|
|||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
clipBehavior: Clip.antiAlias,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: selectedImage != null
|
||||||
children: [
|
? Image.file(
|
||||||
Icon(
|
selectedImage!,
|
||||||
LucideIcons.image_plus,
|
fit: BoxFit.cover,
|
||||||
size: 48,
|
width: double.infinity,
|
||||||
color: AppColors.textMuted,
|
height: double.infinity,
|
||||||
),
|
)
|
||||||
const SizedBox(height: AppSpacing.lg),
|
: Column(
|
||||||
SizedBox(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
width: 280,
|
children: [
|
||||||
|
Icon(
|
||||||
|
LucideIcons.image_plus,
|
||||||
|
size: 48,
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppSpacing.lg),
|
||||||
|
SizedBox(
|
||||||
|
width: 280,
|
||||||
|
child: Text(
|
||||||
|
'Please upload an image as the base for generation',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: AppTypography.bodyRegular.copyWith(
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ResolutionToggle extends StatelessWidget {
|
||||||
|
const _ResolutionToggle({
|
||||||
|
required this.selected,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final _Resolution selected;
|
||||||
|
final ValueChanged<_Resolution> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => onChanged(_Resolution.p480),
|
||||||
|
child: Container(
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected == _Resolution.p480
|
||||||
|
? AppColors.primary
|
||||||
|
: AppColors.surfaceAlt,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: selected == _Resolution.p480
|
||||||
|
? AppColors.primary.withValues(alpha: 0.5)
|
||||||
|
: AppColors.border,
|
||||||
|
width: selected == _Resolution.p480 ? 1 : 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(
|
||||||
'Please upload an image as the base for generation',
|
'480P',
|
||||||
textAlign: TextAlign.center,
|
style: AppTypography.bodyMedium.copyWith(
|
||||||
style: AppTypography.bodyRegular.copyWith(
|
color: selected == _Resolution.p480
|
||||||
color: AppColors.textSecondary,
|
? AppColors.surface
|
||||||
|
: AppColors.textSecondary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => onChanged(_Resolution.p720),
|
||||||
|
child: Container(
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected == _Resolution.p720
|
||||||
|
? AppColors.primary
|
||||||
|
: AppColors.surfaceAlt,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: selected == _Resolution.p720
|
||||||
|
? AppColors.primary.withValues(alpha: 0.5)
|
||||||
|
: AppColors.border,
|
||||||
|
width: selected == _Resolution.p720 ? 1 : 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
'720P',
|
||||||
|
style: AppTypography.bodyMedium.copyWith(
|
||||||
|
color: selected == _Resolution.p720
|
||||||
|
? AppColors.surface
|
||||||
|
: AppColors.textSecondary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GenerateButton extends StatelessWidget {
|
class _GenerateButton extends StatelessWidget {
|
||||||
const _GenerateButton({required this.onGenerate});
|
const _GenerateButton({
|
||||||
|
required this.onGenerate,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.credits = '50',
|
||||||
|
});
|
||||||
|
|
||||||
final VoidCallback onGenerate;
|
final VoidCallback onGenerate;
|
||||||
|
final bool isLoading;
|
||||||
|
final String credits;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onGenerate,
|
onTap: isLoading ? null : onGenerate,
|
||||||
child: Container(
|
child: Opacity(
|
||||||
height: 56,
|
opacity: isLoading ? 0.6 : 1,
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
color: AppColors.primary,
|
height: 56,
|
||||||
borderRadius: BorderRadius.circular(16),
|
decoration: BoxDecoration(
|
||||||
boxShadow: [
|
color: AppColors.primary,
|
||||||
BoxShadow(
|
borderRadius: BorderRadius.circular(16),
|
||||||
color: AppColors.primaryShadow.withValues(alpha: 0.25),
|
boxShadow: [
|
||||||
blurRadius: 8,
|
BoxShadow(
|
||||||
offset: const Offset(0, 2),
|
color: AppColors.primaryShadow.withValues(alpha: 0.25),
|
||||||
),
|
blurRadius: 8,
|
||||||
],
|
offset: const Offset(0, 2),
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Generate Video',
|
|
||||||
style: AppTypography.bodyMedium.copyWith(
|
|
||||||
color: AppColors.surface,
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(width: AppSpacing.md),
|
),
|
||||||
Container(
|
child: Row(
|
||||||
padding: const EdgeInsets.symmetric(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
horizontal: 10,
|
children: [
|
||||||
vertical: AppSpacing.xs,
|
if (isLoading)
|
||||||
),
|
const SizedBox(
|
||||||
decoration: BoxDecoration(
|
width: 24,
|
||||||
color: AppColors.surface.withValues(alpha: 0.2),
|
height: 24,
|
||||||
borderRadius: BorderRadius.circular(8),
|
child: CircularProgressIndicator(
|
||||||
),
|
strokeWidth: 2,
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
LucideIcons.sparkles,
|
|
||||||
size: 16,
|
|
||||||
color: AppColors.surface,
|
color: AppColors.surface,
|
||||||
),
|
),
|
||||||
const SizedBox(width: AppSpacing.xs),
|
)
|
||||||
Text(
|
else
|
||||||
'50',
|
Text(
|
||||||
style: AppTypography.bodyRegular.copyWith(
|
'Generate Video',
|
||||||
color: AppColors.surface,
|
style: AppTypography.bodyMedium.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
color: AppColors.surface,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
if (!isLoading) ...[
|
||||||
),
|
const SizedBox(width: AppSpacing.md),
|
||||||
],
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: AppSpacing.xs,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.surface.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
LucideIcons.sparkles,
|
||||||
|
size: 16,
|
||||||
|
color: AppColors.surface,
|
||||||
|
),
|
||||||
|
const SizedBox(width: AppSpacing.xs),
|
||||||
|
Text(
|
||||||
|
credits,
|
||||||
|
style: AppTypography.bodyRegular.copyWith(
|
||||||
|
color: AppColors.surface,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -133,9 +133,13 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
itemCount: _tasks.length,
|
itemCount: _tasks.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final task = _tasks[index];
|
final task = _tasks[index];
|
||||||
|
final credits = task.credits480p != null
|
||||||
|
? task.credits480p.toString()
|
||||||
|
: '50';
|
||||||
return VideoCard(
|
return VideoCard(
|
||||||
imageUrl: task.previewImageUrl ?? _placeholderImage,
|
imageUrl: task.previewImageUrl ?? _placeholderImage,
|
||||||
videoUrl: task.previewVideoUrl,
|
videoUrl: task.previewVideoUrl,
|
||||||
|
credits: credits,
|
||||||
isActive: _activeCardIndex == index,
|
isActive: _activeCardIndex == index,
|
||||||
onPlayRequested: () =>
|
onPlayRequested: () =>
|
||||||
setState(() => _activeCardIndex = index),
|
setState(() => _activeCardIndex = index),
|
||||||
|
|||||||
@ -8,6 +8,9 @@ class TaskItem {
|
|||||||
this.imageCount = 0,
|
this.imageCount = 0,
|
||||||
this.taskType,
|
this.taskType,
|
||||||
this.needopt = false,
|
this.needopt = false,
|
||||||
|
this.ext,
|
||||||
|
this.credits480p,
|
||||||
|
this.credits720p,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String templateName;
|
final String templateName;
|
||||||
@ -17,17 +20,36 @@ class TaskItem {
|
|||||||
final int imageCount;
|
final int imageCount;
|
||||||
final String? taskType;
|
final String? taskType;
|
||||||
final bool needopt;
|
final bool needopt;
|
||||||
|
final String? ext;
|
||||||
|
/// 480P 积分,来自 reverify.greaves
|
||||||
|
final int? credits480p;
|
||||||
|
/// 720P 积分,来自 preprocess.greaves
|
||||||
|
final int? credits720p;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'TaskItem(templateName: $templateName, title: $title, previewImageUrl: $previewImageUrl, '
|
'TaskItem(templateName: $templateName, title: $title, previewImageUrl: $previewImageUrl, '
|
||||||
'previewVideoUrl: $previewVideoUrl, imageCount: $imageCount, taskType: $taskType, needopt: $needopt)';
|
'previewVideoUrl: $previewVideoUrl, imageCount: $imageCount, taskType: $taskType, needopt: $needopt, credits480p: $credits480p, credits720p: $credits720p)';
|
||||||
|
|
||||||
factory TaskItem.fromJson(Map<String, dynamic> json) {
|
factory TaskItem.fromJson(Map<String, dynamic> json) {
|
||||||
final extract = json['extract'] as Map<String, dynamic>?;
|
final extract = json['extract'] as Map<String, dynamic>?;
|
||||||
final preempt = json['preempt'] as Map<String, dynamic>?;
|
final preempt = json['preempt'] as Map<String, dynamic>?;
|
||||||
|
final reverify = json['reverify'] as Map<String, dynamic>?;
|
||||||
|
final preprocess = json['preprocess'] as Map<String, dynamic>?;
|
||||||
final imgUrl = extract?['digitize'] as String?;
|
final imgUrl = extract?['digitize'] as String?;
|
||||||
final videoUrl = preempt?['digitize'] as String?;
|
final videoUrl = preempt?['digitize'] as String?;
|
||||||
|
final greaves480 = reverify?['greaves'];
|
||||||
|
final greaves720 = preprocess?['greaves'];
|
||||||
|
final credits480p = greaves480 is int
|
||||||
|
? greaves480
|
||||||
|
: greaves480 is num
|
||||||
|
? greaves480.toInt()
|
||||||
|
: null;
|
||||||
|
final credits720p = greaves720 is int
|
||||||
|
? greaves720
|
||||||
|
: greaves720 is num
|
||||||
|
? greaves720.toInt()
|
||||||
|
: null;
|
||||||
return TaskItem(
|
return TaskItem(
|
||||||
templateName: json['congregation'] as String? ?? '',
|
templateName: json['congregation'] as String? ?? '',
|
||||||
title: json['glossary'] as String? ?? '',
|
title: json['glossary'] as String? ?? '',
|
||||||
@ -36,6 +58,9 @@ class TaskItem {
|
|||||||
imageCount: json['simplify'] as int? ?? 0,
|
imageCount: json['simplify'] as int? ?? 0,
|
||||||
taskType: json['cipher'] as String?,
|
taskType: json['cipher'] as String?,
|
||||||
needopt: json['allowance'] as bool? ?? false,
|
needopt: json['allowance'] as bool? ?? false,
|
||||||
|
ext: json['nexus'] as String? ?? '',
|
||||||
|
credits480p: credits480p,
|
||||||
|
credits720p: credits720p,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
|
import 'package:adjust_sdk/adjust_config.dart';
|
||||||
|
import 'package:adjust_sdk/adjust.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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/referrer/referrer_service.dart';
|
||||||
import 'core/theme/app_colors.dart';
|
import 'core/theme/app_colors.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
_initAdjust();
|
||||||
|
ReferrerService.init();
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
const SystemUiOverlayStyle(
|
||||||
statusBarColor: AppColors.surface,
|
statusBarColor: AppColors.surface,
|
||||||
@ -18,3 +24,15 @@ void main() async {
|
|||||||
// APP 打开时后台执行快速登录
|
// APP 打开时后台执行快速登录
|
||||||
AuthService.init();
|
AuthService.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _initAdjust() {
|
||||||
|
const appToken = '2z2mly0afgqo';
|
||||||
|
final config = AdjustConfig(
|
||||||
|
appToken,
|
||||||
|
kDebugMode ? AdjustEnvironment.sandbox : AdjustEnvironment.production,
|
||||||
|
);
|
||||||
|
if (kDebugMode) {
|
||||||
|
config.logLevel = AdjustLogLevel.verbose;
|
||||||
|
}
|
||||||
|
Adjust.initSdk(config);
|
||||||
|
}
|
||||||
|
|||||||
@ -9,7 +9,9 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
adjust_sdk: ^5.5.0
|
||||||
cupertino_icons: ^1.0.6
|
cupertino_icons: ^1.0.6
|
||||||
|
play_install_referrer: ^0.5.0
|
||||||
flutter_lucide: ^1.8.2
|
flutter_lucide: ^1.8.2
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
cached_network_image: ^3.3.1
|
cached_network_image: ^3.3.1
|
||||||
@ -17,6 +19,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
|
||||||
|
image_picker: ^1.0.7
|
||||||
video_player: ^2.9.2
|
video_player: ^2.9.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user