新增:视频生成接口对接

This commit is contained in:
ivan 2026-03-09 20:55:41 +08:00
parent e47e0800e5
commit 9ced4f24e7
14 changed files with 639 additions and 78 deletions

View File

@ -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
View 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

View File

@ -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"]];
} }

View File

@ -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(),
}, },
), ),

View File

@ -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,

View File

@ -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: 登录失败');
} }

View 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?>();
/// referrerAndroid 使 Google Play Install ReferreriOS
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();
}
}

View File

@ -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(

View File

@ -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 {
); );
} }
} }

View File

@ -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,7 +341,15 @@ class _UploadArea extends StatelessWidget {
width: 2, width: 2,
), ),
), ),
child: Column( clipBehavior: Clip.antiAlias,
child: selectedImage != null
? Image.file(
selectedImage!,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
)
: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
@ -169,15 +375,102 @@ class _UploadArea extends StatelessWidget {
} }
} }
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(
'480P',
style: AppTypography.bodyMedium.copyWith(
color: selected == _Resolution.p480
? 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: Opacity(
opacity: isLoading ? 0.6 : 1,
child: Container( child: Container(
height: 56, height: 56,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -194,12 +487,23 @@ class _GenerateButton extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (isLoading)
const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.surface,
),
)
else
Text( Text(
'Generate Video', 'Generate Video',
style: AppTypography.bodyMedium.copyWith( style: AppTypography.bodyMedium.copyWith(
color: AppColors.surface, color: AppColors.surface,
), ),
), ),
if (!isLoading) ...[
const SizedBox(width: AppSpacing.md), const SizedBox(width: AppSpacing.md),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -220,7 +524,7 @@ class _GenerateButton extends StatelessWidget {
), ),
const SizedBox(width: AppSpacing.xs), const SizedBox(width: AppSpacing.xs),
Text( Text(
'50', credits,
style: AppTypography.bodyRegular.copyWith( style: AppTypography.bodyRegular.copyWith(
color: AppColors.surface, color: AppColors.surface,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -230,6 +534,8 @@ class _GenerateButton extends StatelessWidget {
), ),
), ),
], ],
],
),
), ),
), ),
); );

View File

@ -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),

View File

@ -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,
); );
} }
} }

View File

@ -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);
}

View File

@ -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: