88 changed files with 4938 additions and 962 deletions
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 614 KiB |
|
Before Width: | Height: | Size: 774 KiB |
|
Before Width: | Height: | Size: 904 KiB |
@ -0,0 +1,5 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict/> |
||||
|
</plist> |
||||
@ -0,0 +1,160 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:get_storage/get_storage.dart'; |
||||
|
|
||||
|
import '../../../core/utils/logger.dart'; |
||||
|
import '../db/sqflite_api.dart'; |
||||
|
import '../deapsound_ai_service.dart'; |
||||
|
import 'meeting_task_service.dart'; |
||||
|
|
||||
|
class SummaryTaskService extends GetxService { |
||||
|
final _taskService = Get.find<MeetingTaskService>(); |
||||
|
|
||||
|
final GetStorage _storage = GetStorage(); |
||||
|
|
||||
|
List _taskList = []; |
||||
|
|
||||
|
bool _isTask = false; |
||||
|
|
||||
|
String summary = ''; //AI总结内容 |
||||
|
String overview = ''; //AI概览内容 |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
_getTaskList(); |
||||
|
} |
||||
|
|
||||
|
void cancelTask() { |
||||
|
_taskList.clear(); |
||||
|
_storage.remove("summary_task_list"); |
||||
|
} |
||||
|
|
||||
|
void _getTaskList() { |
||||
|
_taskList = _storage.read("summary_task_list") ?? []; |
||||
|
_executeTask(); |
||||
|
} |
||||
|
|
||||
|
Future<void> addTaskList( |
||||
|
int id, String text, String prompt, String outline) async { |
||||
|
Logger.debug("添加总结任务列表"); |
||||
|
_taskList.add({ |
||||
|
'id': id, |
||||
|
'text': text, |
||||
|
'prompt': prompt, |
||||
|
'outline': outline, |
||||
|
}); |
||||
|
_storage.write("summary_task_list", _taskList); |
||||
|
_executeTask(); |
||||
|
} |
||||
|
|
||||
|
void _executeTask() async { |
||||
|
if (_taskList.isEmpty) return; |
||||
|
if (_isTask) return; |
||||
|
_isTask = true; |
||||
|
var task = _taskList.first; |
||||
|
_aiServiceMessage( |
||||
|
task['id'], |
||||
|
task['text'], |
||||
|
task['prompt'], |
||||
|
task['outline'], |
||||
|
); |
||||
|
Logger.debug("执行总结任务列表"); |
||||
|
} |
||||
|
|
||||
|
void removeTask(int id) { |
||||
|
_taskList.removeWhere((element) => element['id'] == id); |
||||
|
_storage.write("summary_task_list", _taskList); |
||||
|
} |
||||
|
|
||||
|
void _aiServiceMessage(int id, String text, String prompt, String outline) { |
||||
|
final ai = DeapsoundAIService(); |
||||
|
String summaryText = ''; |
||||
|
String overviewText = ''; |
||||
|
var summaryDone = false; |
||||
|
var overviewDone = false; |
||||
|
var summarySuccess = false; |
||||
|
var overviewSuccess = false; |
||||
|
|
||||
|
void tryFinish() { |
||||
|
if (summaryDone && overviewDone) { |
||||
|
if (summarySuccess && |
||||
|
overviewSuccess && |
||||
|
summaryText.isNotEmpty && |
||||
|
overviewText.isNotEmpty) { |
||||
|
_aiServiceValue(id, summaryText, overviewText); |
||||
|
} else { |
||||
|
_isTask = false; |
||||
|
_executeTask(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
final summaryStream = ai.sendMessageStream(messages: [ |
||||
|
{'role': 'system', 'content': prompt}, |
||||
|
{'role': 'user', 'content': text}, |
||||
|
]).timeout(const Duration(seconds: 120)); |
||||
|
|
||||
|
final overviewStream = ai.sendMessageStream(messages: [ |
||||
|
{'role': 'system', 'content': outline}, |
||||
|
{'role': 'user', 'content': text}, |
||||
|
]).timeout(const Duration(seconds: 120)); |
||||
|
|
||||
|
summaryStream.listen((chunk) { |
||||
|
summaryText += chunk; |
||||
|
}, onError: (error) { |
||||
|
summaryDone = true; |
||||
|
summarySuccess = false; |
||||
|
tryFinish(); |
||||
|
}, onDone: () { |
||||
|
summaryDone = true; |
||||
|
summarySuccess = true; |
||||
|
tryFinish(); |
||||
|
}, cancelOnError: true); |
||||
|
|
||||
|
overviewStream.listen((chunk) { |
||||
|
overviewText += chunk; |
||||
|
}, onError: (error) { |
||||
|
overviewDone = true; |
||||
|
overviewSuccess = false; |
||||
|
tryFinish(); |
||||
|
}, onDone: () { |
||||
|
overviewDone = true; |
||||
|
overviewSuccess = true; |
||||
|
tryFinish(); |
||||
|
}, cancelOnError: true); |
||||
|
} |
||||
|
|
||||
|
Future<void> _aiServiceValue( |
||||
|
int id, String summaryText, String overviewText) async { |
||||
|
final exists = _taskList.any((item) => item['id'] == id); |
||||
|
if (!exists) { |
||||
|
_isTask = false; |
||||
|
_executeTask(); |
||||
|
return; |
||||
|
} |
||||
|
await SqfliteApi.editMeeting(id, { |
||||
|
'tasktype': 3, |
||||
|
'summary': summaryText, |
||||
|
'overview': overviewText, |
||||
|
}); |
||||
|
if (id == _taskService.meetingId) { |
||||
|
summary = summaryText; |
||||
|
overview = overviewText; |
||||
|
_taskService.tasktype.value = 3; |
||||
|
} |
||||
|
Map taskFinishData = _taskService.taskFinishList.firstWhere( |
||||
|
(element) => element['id'] == id, |
||||
|
orElse: () => {}, |
||||
|
); |
||||
|
if (taskFinishData.isNotEmpty) { |
||||
|
taskFinishData['tasktype'] = 3; |
||||
|
_taskService.taskFinishList.refresh(); |
||||
|
} |
||||
|
_storage.write("meeting_task_finish_list", _taskService.taskFinishList); |
||||
|
_taskList.removeWhere((element) => element['id'] == id); |
||||
|
_storage.write("summary_task_list", _taskList); |
||||
|
_isTask = false; |
||||
|
_executeTask(); |
||||
|
Logger.debug("总结任务完成"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,545 @@ |
|||||
|
import 'dart:convert'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:get_storage/get_storage.dart'; |
||||
|
import '../models/translation_usage_stats.dart'; |
||||
|
import '../../core/utils/logger.dart'; |
||||
|
|
||||
|
/// 翻译使用量统计服务 |
||||
|
class TranslationUsageService extends GetxService { |
||||
|
static const String _storageKey = 'translation_usage_stats'; |
||||
|
static const String _dailyStatsKey = 'daily_translation_stats'; |
||||
|
static const String _costConfigKey = 'translation_cost_config'; |
||||
|
|
||||
|
final GetStorage _storage = GetStorage(); |
||||
|
|
||||
|
// 实时统计数据 |
||||
|
final Rx<RealtimeUsageStats> realtimeStats = RealtimeUsageStats().obs; |
||||
|
|
||||
|
// 当日统计数据 |
||||
|
final Rx<TranslationUsageStats> todayStats = |
||||
|
TranslationUsageStats.empty('').obs; |
||||
|
|
||||
|
// 成本配置 |
||||
|
final RxMap<String, double> costConfig = <String, double>{}.obs; |
||||
|
|
||||
|
// 预警阈值 |
||||
|
final RxDouble dailyCostThreshold = 50.0.obs; // 日成本预警阈值 |
||||
|
final RxDouble monthlyCostThreshold = 300.0.obs; // 月成本预警阈值 |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
_initializeService(); |
||||
|
} |
||||
|
|
||||
|
/// 初始化服务 |
||||
|
void _initializeService() { |
||||
|
_loadCostConfig(); |
||||
|
_loadTodayStats(); |
||||
|
Logger.info('翻译使用量统计服务初始化完成'); |
||||
|
} |
||||
|
|
||||
|
/// 加载成本配置 |
||||
|
void _loadCostConfig() { |
||||
|
try { |
||||
|
final configData = _storage.read(_costConfigKey); |
||||
|
if (configData != null) { |
||||
|
costConfig.value = Map<String, double>.from(configData); |
||||
|
} else { |
||||
|
// 设置默认成本配置(每千字符的成本,单位:元) |
||||
|
costConfig.value = { |
||||
|
'translation_per_1k_chars': 0.05, // 翻译:每千字符0.05元 |
||||
|
'asr_per_minute': 0.02, // ASR:每分钟0.02元 |
||||
|
'tts_per_1k_chars': 0.03, // TTS:每千字符0.03元 |
||||
|
'ai_summary_per_request': 0.01, // AI摘要:每次请求0.01元 |
||||
|
}; |
||||
|
_saveCostConfig(); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.error('加载成本配置失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 保存成本配置 |
||||
|
void _saveCostConfig() { |
||||
|
try { |
||||
|
_storage.write(_costConfigKey, costConfig.value); |
||||
|
} catch (e) { |
||||
|
Logger.error('保存成本配置失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 加载当日统计数据 |
||||
|
void _loadTodayStats() { |
||||
|
try { |
||||
|
final today = DateTime.now(); |
||||
|
final todayKey = |
||||
|
'${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}'; |
||||
|
|
||||
|
final statsData = _storage.read('${_dailyStatsKey}_$todayKey'); |
||||
|
if (statsData != null) { |
||||
|
todayStats.value = TranslationUsageStats.fromJson( |
||||
|
Map<String, dynamic>.from(statsData)); |
||||
|
} else { |
||||
|
todayStats.value = TranslationUsageStats.empty(getCurrentUserId()); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.error('加载当日统计数据失败: $e'); |
||||
|
todayStats.value = TranslationUsageStats.empty(getCurrentUserId()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取当前用户ID |
||||
|
String getCurrentUserId() { |
||||
|
try { |
||||
|
final userInfo = _storage.read('user_info'); |
||||
|
if (userInfo != null && userInfo['user'] != null) { |
||||
|
return userInfo['user']['uid'] ?? 'anonymous'; |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.warning('获取用户ID失败: $e'); |
||||
|
} |
||||
|
return 'anonymous'; |
||||
|
} |
||||
|
|
||||
|
/// 记录翻译API调用 |
||||
|
void recordTranslationApiCall({ |
||||
|
required String sourceText, |
||||
|
required String targetText, |
||||
|
required String sourceLanguage, |
||||
|
required String targetLanguage, |
||||
|
required String mode, |
||||
|
}) { |
||||
|
try { |
||||
|
// 更新实时统计 |
||||
|
realtimeStats.value.translationCount++; |
||||
|
realtimeStats.value.sourceTextCharacters += sourceText.length; |
||||
|
realtimeStats.value.ttsTextCharacters += targetText.length; |
||||
|
realtimeStats.value.currentLanguagePair = |
||||
|
'$sourceLanguage-$targetLanguage'; |
||||
|
realtimeStats.value.currentMode = mode; |
||||
|
realtimeStats.refresh(); |
||||
|
|
||||
|
// 更新当日统计 - 使用多维度统计 |
||||
|
final languagePair = '$sourceLanguage→$targetLanguage'; |
||||
|
|
||||
|
// 更新语言对统计 |
||||
|
final newLanguagePairStats = Map<String, LanguagePairStats>.from( |
||||
|
todayStats.value.languagePairStats); |
||||
|
if (newLanguagePairStats.containsKey(languagePair)) { |
||||
|
final existing = newLanguagePairStats[languagePair]!; |
||||
|
newLanguagePairStats[languagePair] = LanguagePairStats( |
||||
|
languagePair: languagePair, |
||||
|
translationCount: existing.translationCount + 1, |
||||
|
sourceCharacters: existing.sourceCharacters + sourceText.length, |
||||
|
cost: existing.cost + _calculateTranslationCost(sourceText.length), |
||||
|
); |
||||
|
} else { |
||||
|
newLanguagePairStats[languagePair] = LanguagePairStats( |
||||
|
languagePair: languagePair, |
||||
|
translationCount: 1, |
||||
|
sourceCharacters: sourceText.length, |
||||
|
cost: _calculateTranslationCost(sourceText.length), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 更新模式统计 |
||||
|
final newModeStats = |
||||
|
Map<String, TranslationModeStats>.from(todayStats.value.modeStats); |
||||
|
if (newModeStats.containsKey(mode)) { |
||||
|
final existing = newModeStats[mode]!; |
||||
|
newModeStats[mode] = existing.copyWith( |
||||
|
apiCalls: existing.apiCalls + 1, |
||||
|
sourceCharacters: existing.sourceCharacters + sourceText.length, |
||||
|
ttsCharacters: existing.ttsCharacters + targetText.length, |
||||
|
cost: existing.cost + _calculateTranslationCost(sourceText.length), |
||||
|
); |
||||
|
} else { |
||||
|
newModeStats[mode] = TranslationModeStats( |
||||
|
apiCalls: 1, |
||||
|
sourceCharacters: sourceText.length, |
||||
|
ttsCharacters: targetText.length, |
||||
|
cost: _calculateTranslationCost(sourceText.length), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
todayStats.value = todayStats.value.copyWith( |
||||
|
translationApiCalls: todayStats.value.translationApiCalls + 1, |
||||
|
sourceTextCharacters: |
||||
|
todayStats.value.sourceTextCharacters + sourceText.length, |
||||
|
ttsTextCharacters: |
||||
|
todayStats.value.ttsTextCharacters + targetText.length, |
||||
|
languagePairStats: newLanguagePairStats, |
||||
|
modeStats: newModeStats, |
||||
|
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith( |
||||
|
translationApiCalls: todayStats.value.translationApiCalls + 1, |
||||
|
sourceTextCharacters: |
||||
|
todayStats.value.sourceTextCharacters + sourceText.length, |
||||
|
ttsTextCharacters: |
||||
|
todayStats.value.ttsTextCharacters + targetText.length, |
||||
|
)), |
||||
|
); |
||||
|
|
||||
|
_saveTodayStats(); |
||||
|
_checkCostThreshold(); |
||||
|
|
||||
|
Logger.info( |
||||
|
'记录翻译API调用: 源文本${sourceText.length}字符, 目标文本${targetText.length}字符'); |
||||
|
} catch (e) { |
||||
|
Logger.error('记录翻译API调用失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 记录ASR API调用 - 优化版(精确计费参数) |
||||
|
void recordAsrApiCall({ |
||||
|
required int effectiveDurationSeconds, // 有效语音时长(VAD检测的实际语音) |
||||
|
required int totalDurationSeconds, // 总会话时长(包含静音) |
||||
|
required String language, |
||||
|
required String mode, // 翻译模式 |
||||
|
String? audioFormat, // 音频格式(可选) |
||||
|
int? sampleRate, // 采样率(可选) |
||||
|
}) { |
||||
|
try { |
||||
|
// 更新实时统计(使用有效语音时长作为主要计费依据) |
||||
|
realtimeStats.value.effectiveAudioDuration += effectiveDurationSeconds; |
||||
|
realtimeStats.value.totalSessionDuration += totalDurationSeconds; |
||||
|
realtimeStats.refresh(); |
||||
|
|
||||
|
// 更新模式统计 |
||||
|
final newModeStats = |
||||
|
Map<String, TranslationModeStats>.from(todayStats.value.modeStats); |
||||
|
if (newModeStats.containsKey(mode)) { |
||||
|
final existing = newModeStats[mode]!; |
||||
|
newModeStats[mode] = existing.copyWith( |
||||
|
effectiveAudioDuration: |
||||
|
existing.effectiveAudioDuration + effectiveDurationSeconds, |
||||
|
cost: existing.cost + _calculateAsrCost(effectiveDurationSeconds), |
||||
|
); |
||||
|
} else { |
||||
|
newModeStats[mode] = TranslationModeStats( |
||||
|
effectiveAudioDuration: effectiveDurationSeconds, |
||||
|
cost: _calculateAsrCost(effectiveDurationSeconds), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 更新当日统计 |
||||
|
todayStats.value = todayStats.value.copyWith( |
||||
|
asrApiCalls: todayStats.value.asrApiCalls + 1, |
||||
|
effectiveAudioDuration: |
||||
|
todayStats.value.effectiveAudioDuration + effectiveDurationSeconds, |
||||
|
totalSessionDuration: |
||||
|
todayStats.value.totalSessionDuration + totalDurationSeconds, |
||||
|
modeStats: newModeStats, |
||||
|
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith( |
||||
|
asrApiCalls: todayStats.value.asrApiCalls + 1, |
||||
|
effectiveAudioDuration: todayStats.value.effectiveAudioDuration + |
||||
|
effectiveDurationSeconds, |
||||
|
)), |
||||
|
); |
||||
|
|
||||
|
_saveTodayStats(); |
||||
|
|
||||
|
Logger.info( |
||||
|
'ASR统计: 有效语音${effectiveDurationSeconds}秒, 总时长${totalDurationSeconds}秒, 语言$language, 模式$mode'); |
||||
|
} catch (e) { |
||||
|
Logger.error('记录ASR API调用失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 记录TTS API调用 |
||||
|
void recordTtsApiCall({ |
||||
|
required String text, |
||||
|
required int durationSeconds, |
||||
|
required String language, |
||||
|
}) { |
||||
|
try { |
||||
|
// 更新实时统计 |
||||
|
realtimeStats.value.totalSessionDuration += durationSeconds; |
||||
|
realtimeStats.refresh(); |
||||
|
|
||||
|
// 更新当日统计(TTS按字符数计费,不是按时长) |
||||
|
todayStats.value = todayStats.value.copyWith( |
||||
|
ttsApiCalls: todayStats.value.ttsApiCalls + 1, |
||||
|
ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length, |
||||
|
totalSessionDuration: |
||||
|
todayStats.value.totalSessionDuration + durationSeconds, |
||||
|
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith( |
||||
|
ttsApiCalls: todayStats.value.ttsApiCalls + 1, |
||||
|
ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length, |
||||
|
)), |
||||
|
); |
||||
|
|
||||
|
_saveTodayStats(); |
||||
|
|
||||
|
Logger.info('记录TTS API调用: 文本${text.length}字符, 时长${durationSeconds}秒'); |
||||
|
} catch (e) { |
||||
|
Logger.error('记录TTS API调用失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 记录AI摘要API调用 |
||||
|
void recordAiSummaryApiCall() { |
||||
|
try { |
||||
|
// 更新当日统计 |
||||
|
todayStats.value = todayStats.value.copyWith( |
||||
|
aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1, |
||||
|
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith( |
||||
|
aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1, |
||||
|
)), |
||||
|
); |
||||
|
|
||||
|
_saveTodayStats(); |
||||
|
|
||||
|
Logger.info('记录AI摘要API调用'); |
||||
|
} catch (e) { |
||||
|
Logger.error('记录AI摘要API调用失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 开始新会话 |
||||
|
void startNewSession() { |
||||
|
realtimeStats.value.reset(); |
||||
|
|
||||
|
// 更新当日会话数 |
||||
|
todayStats.value = todayStats.value.copyWith( |
||||
|
sessionCount: todayStats.value.sessionCount + 1, |
||||
|
); |
||||
|
|
||||
|
_saveTodayStats(); |
||||
|
Logger.info('开始新的翻译会话'); |
||||
|
} |
||||
|
|
||||
|
/// 计算预估成本 - 优化版(基于精确计费参数) |
||||
|
double _calculateEstimatedCost(TranslationUsageStats stats) { |
||||
|
double cost = 0.0; |
||||
|
|
||||
|
// 翻译成本:基于源文本字符数 |
||||
|
cost += (stats.sourceTextCharacters / 1000) * |
||||
|
(costConfig['translation_per_1k_chars'] ?? 0.05); |
||||
|
|
||||
|
// ASR成本:基于有效音频时长 |
||||
|
cost += (stats.effectiveAudioDuration / 60) * |
||||
|
(costConfig['asr_per_minute'] ?? 0.02); |
||||
|
|
||||
|
// TTS成本:基于TTS文本字符数 |
||||
|
cost += (stats.ttsTextCharacters / 1000) * |
||||
|
(costConfig['tts_per_1k_chars'] ?? 0.03); |
||||
|
|
||||
|
// AI摘要成本(已废弃但保留兼容性) |
||||
|
cost += stats.aiSummaryApiCalls * |
||||
|
(costConfig['ai_summary_per_request'] ?? 0.01); |
||||
|
|
||||
|
return cost; |
||||
|
} |
||||
|
|
||||
|
/// 计算翻译成本(单次调用) |
||||
|
double _calculateTranslationCost(int sourceCharacters) { |
||||
|
return (sourceCharacters / 1000) * |
||||
|
(costConfig['translation_per_1k_chars'] ?? 0.05); |
||||
|
} |
||||
|
|
||||
|
/// 计算ASR成本(单次调用) |
||||
|
double _calculateAsrCost(int durationSeconds) { |
||||
|
return (durationSeconds / 60) * (costConfig['asr_per_minute'] ?? 0.02); |
||||
|
} |
||||
|
|
||||
|
/// 计算TTS成本(单次调用) |
||||
|
double _calculateTtsCost(int textCharacters) { |
||||
|
return (textCharacters / 1000) * (costConfig['tts_per_1k_chars'] ?? 0.03); |
||||
|
} |
||||
|
|
||||
|
/// 保存当日统计数据 |
||||
|
void _saveTodayStats() { |
||||
|
try { |
||||
|
final today = DateTime.now(); |
||||
|
final todayKey = |
||||
|
'${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}'; |
||||
|
_storage.write('${_dailyStatsKey}_$todayKey', todayStats.value.toJson()); |
||||
|
} catch (e) { |
||||
|
Logger.error('保存当日统计数据失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 检查成本阈值 |
||||
|
void _checkCostThreshold() { |
||||
|
final todayCost = todayStats.value.estimatedCost; |
||||
|
|
||||
|
if (todayCost >= dailyCostThreshold.value) { |
||||
|
_showCostWarning('今日翻译成本已达到预警阈值:¥${todayCost.toStringAsFixed(2)}'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 显示成本预警 |
||||
|
void _showCostWarning(String message) { |
||||
|
Get.snackbar( |
||||
|
'成本预警', |
||||
|
message, |
||||
|
snackPosition: SnackPosition.TOP, |
||||
|
backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1), |
||||
|
colorText: Get.theme.colorScheme.error, |
||||
|
duration: const Duration(seconds: 5), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/// 获取历史统计数据 |
||||
|
Future<List<TranslationUsageStats>> getHistoryStats({ |
||||
|
required DateTime startDate, |
||||
|
required DateTime endDate, |
||||
|
}) async { |
||||
|
try { |
||||
|
final List<TranslationUsageStats> historyStats = []; |
||||
|
|
||||
|
for (DateTime date = startDate; |
||||
|
date.isBefore(endDate) || date.isAtSameMomentAs(endDate); |
||||
|
date = date.add(const Duration(days: 1))) { |
||||
|
final dateKey = |
||||
|
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; |
||||
|
final statsData = _storage.read('${_dailyStatsKey}_$dateKey'); |
||||
|
|
||||
|
if (statsData != null) { |
||||
|
historyStats.add(TranslationUsageStats.fromJson( |
||||
|
Map<String, dynamic>.from(statsData))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return historyStats; |
||||
|
} catch (e) { |
||||
|
Logger.error('获取历史统计数据失败: $e'); |
||||
|
return []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 清理过期数据(保留最近90天) |
||||
|
void cleanupOldData() { |
||||
|
try { |
||||
|
final cutoffDate = DateTime.now().subtract(const Duration(days: 90)); |
||||
|
final allKeys = _storage.getKeys(); |
||||
|
|
||||
|
for (final key in allKeys) { |
||||
|
if (key.startsWith(_dailyStatsKey)) { |
||||
|
final dateStr = key.replaceFirst('${_dailyStatsKey}_', ''); |
||||
|
try { |
||||
|
final date = DateTime.parse(dateStr); |
||||
|
if (date.isBefore(cutoffDate)) { |
||||
|
_storage.remove(key); |
||||
|
Logger.info('清理过期统计数据: $key'); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
// 忽略解析错误的键 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.error('清理过期数据失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 更新成本配置 |
||||
|
void updateCostConfig(Map<String, double> newConfig) { |
||||
|
costConfig.value = newConfig; |
||||
|
_saveCostConfig(); |
||||
|
|
||||
|
// 重新计算当日成本 |
||||
|
todayStats.value = todayStats.value.copyWith( |
||||
|
estimatedCost: _calculateEstimatedCost(todayStats.value), |
||||
|
); |
||||
|
_saveTodayStats(); |
||||
|
|
||||
|
Logger.info('更新成本配置完成'); |
||||
|
} |
||||
|
|
||||
|
/// 更新预警阈值 |
||||
|
void updateThresholds({ |
||||
|
double? dailyThreshold, |
||||
|
double? monthlyThreshold, |
||||
|
}) { |
||||
|
if (dailyThreshold != null) { |
||||
|
dailyCostThreshold.value = dailyThreshold; |
||||
|
} |
||||
|
if (monthlyThreshold != null) { |
||||
|
monthlyCostThreshold.value = monthlyThreshold; |
||||
|
} |
||||
|
|
||||
|
// 保存阈值配置 |
||||
|
_storage.write('daily_cost_threshold', dailyCostThreshold.value); |
||||
|
_storage.write('monthly_cost_threshold', monthlyCostThreshold.value); |
||||
|
|
||||
|
Logger.info('更新预警阈值完成'); |
||||
|
} |
||||
|
|
||||
|
/// 获取月度统计汇总 |
||||
|
Future<TranslationUsageStats> getMonthlyStats(int year, int month) async { |
||||
|
try { |
||||
|
final startDate = DateTime(year, month, 1); |
||||
|
final endDate = DateTime(year, month + 1, 0); // 月末 |
||||
|
|
||||
|
final dailyStats = await getHistoryStats( |
||||
|
startDate: startDate, |
||||
|
endDate: endDate, |
||||
|
); |
||||
|
|
||||
|
if (dailyStats.isEmpty) { |
||||
|
return TranslationUsageStats.empty(getCurrentUserId()); |
||||
|
} |
||||
|
|
||||
|
// 汇总月度数据 |
||||
|
int totalTranslationCalls = 0; |
||||
|
int totalAsrCalls = 0; |
||||
|
int totalTtsCalls = 0; |
||||
|
int totalAiSummaryCalls = 0; |
||||
|
int totalSourceChars = 0; |
||||
|
int totalTargetChars = 0; |
||||
|
int totalRecordingDuration = 0; |
||||
|
int totalTtsPlayDuration = 0; |
||||
|
int totalSessions = 0; |
||||
|
double totalCost = 0.0; |
||||
|
|
||||
|
final Map<String, int> combinedLanguagePairUsage = {}; |
||||
|
final Map<String, int> combinedModeUsage = {}; |
||||
|
|
||||
|
for (final stats in dailyStats) { |
||||
|
totalTranslationCalls += stats.translationApiCalls; |
||||
|
totalAsrCalls += stats.asrApiCalls; |
||||
|
totalTtsCalls += stats.ttsApiCalls; |
||||
|
totalAiSummaryCalls += stats.aiSummaryApiCalls; |
||||
|
totalSourceChars += stats.sourceCharacterCount; |
||||
|
totalTargetChars += stats.targetCharacterCount; |
||||
|
totalRecordingDuration += stats.audioRecordingDuration; |
||||
|
totalTtsPlayDuration += stats.ttsPlaybackDuration; |
||||
|
totalSessions += stats.sessionCount; |
||||
|
totalCost += stats.estimatedCost; |
||||
|
|
||||
|
// 合并语言对使用统计 |
||||
|
stats.languagePairUsage.forEach((key, value) { |
||||
|
combinedLanguagePairUsage[key] = |
||||
|
(combinedLanguagePairUsage[key] ?? 0) + value; |
||||
|
}); |
||||
|
|
||||
|
// 合并模式使用统计 |
||||
|
stats.modeUsage.forEach((key, value) { |
||||
|
combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return TranslationUsageStats( |
||||
|
id: 'monthly_${year}_$month', |
||||
|
date: startDate, |
||||
|
userId: getCurrentUserId(), |
||||
|
timeGranularity: 'month', |
||||
|
translationApiCalls: totalTranslationCalls, |
||||
|
asrApiCalls: totalAsrCalls, |
||||
|
ttsApiCalls: totalTtsCalls, |
||||
|
aiSummaryApiCalls: totalAiSummaryCalls, |
||||
|
sourceTextCharacters: totalSourceChars, |
||||
|
effectiveAudioDuration: totalRecordingDuration, |
||||
|
ttsTextCharacters: totalTargetChars, |
||||
|
totalSessionDuration: totalTtsPlayDuration, |
||||
|
sessionCount: totalSessions, |
||||
|
estimatedCost: totalCost, |
||||
|
); |
||||
|
} catch (e) { |
||||
|
Logger.error('获取月度统计失败: $e'); |
||||
|
return TranslationUsageStats.empty(getCurrentUserId()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
import '../controllers/eq_settings_controller.dart'; |
||||
|
|
||||
|
class EqSettingsBinding extends Bindings { |
||||
|
@override |
||||
|
void dependencies() { |
||||
|
Get.lazyPut<EqSettingsController>(() => EqSettingsController()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,217 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:get_storage/get_storage.dart'; |
||||
|
|
||||
|
import '../../../core/utils/logger.dart'; |
||||
|
import '../../../routes/app_routes.dart'; |
||||
|
import '../views/widgets/row_confirm_widget.dart'; |
||||
|
import '../views/widgets/row_equalizer_widget.dart'; |
||||
|
import '../views/widgets/row_grade_widget.dart'; |
||||
|
import '../views/widgets/row_input_widget.dart'; |
||||
|
import '../views/widgets/row_keypress_widget.dart'; |
||||
|
import '../views/widgets/row_radio_widget.dart'; |
||||
|
import '../views/widgets/row_switch_widget.dart'; |
||||
|
import '../views/widgets/row_timer_widget.dart'; |
||||
|
|
||||
|
class EqSettingsController extends GetxController { |
||||
|
final RxList uiList = [ |
||||
|
{ |
||||
|
'title': '音效', |
||||
|
'templates': [ |
||||
|
{ |
||||
|
'type': 'equalizer', |
||||
|
'name': '均衡器', |
||||
|
'value': [], |
||||
|
'options': [ |
||||
|
{ |
||||
|
'label': '默认', |
||||
|
'value': [1, 2, 3, 4, 5, 4, 3, 2, 1, 0], |
||||
|
'default': true, |
||||
|
}, |
||||
|
{ |
||||
|
'label': '流行', |
||||
|
'value': [1, 0, 3, 4, 5, 4, 1, 2, 1, 0], |
||||
|
'default': true, |
||||
|
}, |
||||
|
{ |
||||
|
'label': '摇滚', |
||||
|
'value': [2, 3, 4, 5, 4, 3, 2, 1, 0, -1], |
||||
|
'default': true, |
||||
|
}, |
||||
|
{ |
||||
|
'label': '爵士', |
||||
|
'value': [3, 4, 0, 4, 3, 2, 1, 0, -1, -2], |
||||
|
'default': true, |
||||
|
}, |
||||
|
{ |
||||
|
'label': '经典', |
||||
|
'value': [3, 4, 2, 4, 3, 2, 1, 0, -1, -2], |
||||
|
'default': true, |
||||
|
}, |
||||
|
{ |
||||
|
'label': '乡村', |
||||
|
'value': [3, 4, 1, 4, 3, 2, 1, 0, -1, -2], |
||||
|
'default': true, |
||||
|
}, |
||||
|
], |
||||
|
'xAxis': [ |
||||
|
'8', |
||||
|
'31', |
||||
|
'62', |
||||
|
'125', |
||||
|
'250', |
||||
|
'500', |
||||
|
'1K', |
||||
|
'2K', |
||||
|
'4K', |
||||
|
'16K' |
||||
|
], |
||||
|
'onCallback': (List value) { |
||||
|
Logger.d('equalizer', '均衡器:$value'); |
||||
|
}, |
||||
|
} |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
'title': '按键功能', |
||||
|
'templates': [ |
||||
|
{ |
||||
|
'type': 'keypress', |
||||
|
'data': [ |
||||
|
{'key': 'key1', 'label': '单击', 'left': 1, 'right': 1}, |
||||
|
{'key': 'key2', 'label': '双击', 'left': 1, 'right': 1}, |
||||
|
{'key': 'key3', 'label': '三击', 'left': 1, 'right': 1}, |
||||
|
{'key': 'key4', 'label': '长按', 'left': 1, 'right': 1}, |
||||
|
], |
||||
|
'options': [ |
||||
|
{'label': '无作用', 'value': 0}, |
||||
|
{'label': '回拨电话', 'value': 1}, |
||||
|
{'label': '语音助手', 'value': 2}, |
||||
|
{'label': '上一曲', 'value': 3}, |
||||
|
{'label': '下一曲', 'value': 4}, |
||||
|
{'label': '音量加', 'value': 5}, |
||||
|
{'label': '音量减', 'value': 6}, |
||||
|
{'label': '播放暂停', 'value': 7}, |
||||
|
{'label': '游戏模式', 'value': 8}, |
||||
|
{'label': '降噪设置', 'value': 9}, |
||||
|
{'label': 'EQ切换', 'value': 10}, |
||||
|
], |
||||
|
'onCallback': (Map value) { |
||||
|
Logger.d('keypress', '按键功能:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
'title': '耳机功能', |
||||
|
'templates': [ |
||||
|
{ |
||||
|
'type': 'input', |
||||
|
'name': '修改名称', |
||||
|
'value': 'voitrans', |
||||
|
'onCallback': (String value) { |
||||
|
Logger.d('input', '修改名称:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'radio', |
||||
|
'name': '工作模式', |
||||
|
'value': 1, |
||||
|
'options': [ |
||||
|
{'label': '游戏模式', 'value': 1}, |
||||
|
{'label': '娱乐模式', 'value': 2}, |
||||
|
{'label': '睡眠模式', 'value': 3}, |
||||
|
], |
||||
|
'onCallback': (int value) { |
||||
|
Logger.d('radio', '工作模式:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'switch', |
||||
|
'name': '闪灯开关', |
||||
|
'value': false, |
||||
|
'onCallback': (bool value) { |
||||
|
Logger.d('switch', '闪灯开关:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'switch', |
||||
|
'name': '查找设备', |
||||
|
'value': false, |
||||
|
'description': '在设备上播放声音', |
||||
|
'onCallback': (bool value) { |
||||
|
Logger.d('switch', '查找设备:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'grade', |
||||
|
'name': '降噪等级', |
||||
|
'value': 0, |
||||
|
'onCallback': (int value) { |
||||
|
Logger.d('grade', '降噪等级:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'timer', |
||||
|
'name': '定时关机', |
||||
|
'value': 0, |
||||
|
'onCallback': (int value) { |
||||
|
Logger.d('timer', '定时关机:$value'); |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
'templates': [ |
||||
|
{ |
||||
|
'type': 'confirm', |
||||
|
'name': '升级耳机固件', |
||||
|
'onCallback': () => Get.toNamed(Routes.OTA_UPGRADE), |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'confirm', |
||||
|
'name': '恢复出厂设置', |
||||
|
'tips': '确定要恢复出厂设置吗?', |
||||
|
'onCallback': () { |
||||
|
Logger.d('confirm', '恢复出厂设置'); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
'type': 'confirm', |
||||
|
'name': '清除配对记录', |
||||
|
'tips': '确定要清除配对记录吗?', |
||||
|
'onCallback': () { |
||||
|
Logger.d('confirm', '清除配对记录'); |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
} |
||||
|
].obs; |
||||
|
|
||||
|
final GetStorage _storage = GetStorage(); |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
final List equalizerOptions = _storage.read('equalizer_options') ?? []; |
||||
|
if (equalizerOptions.isNotEmpty) { |
||||
|
List options = |
||||
|
equalizerOptions.map((e) => Map<String, Object>.from(e)).toList(); |
||||
|
uiList[0]['templates'][0]['options'].addAll(options); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Widget getTemplate(Map data) { |
||||
|
return switch (data['type']) { |
||||
|
'equalizer' => RowEqualizerWidget(data), |
||||
|
'keypress' => RowKeypressWidget(data), |
||||
|
'input' => RowInputWidget(data), |
||||
|
'radio' => RowRadioWidget(data), |
||||
|
'switch' => RowSwitchWidget(data), |
||||
|
'grade' => RowGradeWidget(data), |
||||
|
'timer' => RowTimerWidget(data), |
||||
|
'confirm' => RowConfirmWidget(data), |
||||
|
_ => const SizedBox.shrink(), |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
import '../controllers/eq_settings_controller.dart'; |
||||
|
|
||||
|
class EqSettingsView extends GetView<EqSettingsController> { |
||||
|
const EqSettingsView({super.key}); |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return Scaffold( |
||||
|
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], |
||||
|
appBar: _buildAppBar(isDarkMode), |
||||
|
body: _buildBody(isDarkMode), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 构建应用栏 |
||||
|
PreferredSizeWidget _buildAppBar(bool isDarkMode) { |
||||
|
return AppBar( |
||||
|
backgroundColor: isDarkMode ? Colors.black : Colors.white, |
||||
|
surfaceTintColor: Colors.transparent, |
||||
|
elevation: 0.5, |
||||
|
leading: IconButton( |
||||
|
icon: Icon( |
||||
|
Icons.arrow_back_ios, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
size: 20.sp, |
||||
|
), |
||||
|
onPressed: () => Get.back(), |
||||
|
), |
||||
|
title: Text( |
||||
|
'设备设置', |
||||
|
style: TextStyle( |
||||
|
fontSize: 17.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
centerTitle: true, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 构建主体内容 |
||||
|
Widget _buildBody(bool isDarkMode) { |
||||
|
return Obx( |
||||
|
() => ListView.builder( |
||||
|
itemCount: controller.uiList.length, |
||||
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), |
||||
|
itemBuilder: (BuildContext context, int index) { |
||||
|
Map data = controller.uiList[index]; |
||||
|
return _item(isDarkMode, data); |
||||
|
}, |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _item(bool isDarkMode, Map data) { |
||||
|
return Container( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), |
||||
|
margin: EdgeInsets.symmetric(vertical: 5.w), |
||||
|
decoration: BoxDecoration( |
||||
|
color: isDarkMode ? Colors.white.withValues(alpha: 0.05) : Colors.white, |
||||
|
borderRadius: BorderRadius.circular(8.w), |
||||
|
boxShadow: [ |
||||
|
BoxShadow( |
||||
|
color: isDarkMode |
||||
|
? Colors.black.withValues(alpha: 0.2) |
||||
|
: Colors.black.withValues(alpha: 0.05), |
||||
|
blurRadius: 10, |
||||
|
offset: const Offset(0, 4), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
child: Column( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
if (data['title'] != null) |
||||
|
Text( |
||||
|
data['title'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
...data['templates'].map((e) => controller.getTemplate(e)).toList(), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
class RowConfirmWidget extends StatefulWidget { |
||||
|
final Map templateData; |
||||
|
|
||||
|
const RowConfirmWidget(this.templateData, {super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<RowConfirmWidget> createState() => _RowConfirmWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _RowConfirmWidgetState extends State<RowConfirmWidget> { |
||||
|
Map get _templateData => widget.templateData; |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return GestureDetector( |
||||
|
onTap: () { |
||||
|
if (_templateData['tips'] != null) { |
||||
|
_alertDialog(isDarkMode); |
||||
|
} else { |
||||
|
_templateData['onCallback'](); |
||||
|
} |
||||
|
}, |
||||
|
behavior: HitTestBehavior.translucent, |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
_templateData['name'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Icon( |
||||
|
Icons.chevron_right, |
||||
|
size: 26.w, |
||||
|
color: Colors.blue, |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _alertDialog(bool isDarkMode) { |
||||
|
Get.dialog( |
||||
|
AlertDialog( |
||||
|
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, |
||||
|
title: Text( |
||||
|
_templateData['name'], |
||||
|
style: TextStyle( |
||||
|
color: isDarkMode ? Colors.white : Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
content: Text( |
||||
|
_templateData['tips'], |
||||
|
style: TextStyle( |
||||
|
color: isDarkMode ? Colors.white70 : Colors.black54, |
||||
|
), |
||||
|
), |
||||
|
actions: [ |
||||
|
TextButton( |
||||
|
onPressed: () => Get.back(), |
||||
|
child: Text( |
||||
|
'cancel'.tr, // 取消 |
||||
|
style: TextStyle( |
||||
|
color: isDarkMode ? Colors.white70 : Colors.black54, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
TextButton( |
||||
|
onPressed: () { |
||||
|
_templateData['onCallback'](); |
||||
|
Get.back(); |
||||
|
}, |
||||
|
child: Text( |
||||
|
'confirm'.tr, // 确定 |
||||
|
style: TextStyle( |
||||
|
color: isDarkMode ? Colors.red[300] : Colors.red[600], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
import 'package:flutter/foundation.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
import 'select_equalizer_widget.dart'; |
||||
|
|
||||
|
class RowEqualizerWidget extends StatefulWidget { |
||||
|
final Map templateData; |
||||
|
|
||||
|
const RowEqualizerWidget(this.templateData, {super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<RowEqualizerWidget> createState() => _RowEqualizerWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _RowEqualizerWidgetState extends State<RowEqualizerWidget> { |
||||
|
Map get _templateData => widget.templateData; |
||||
|
|
||||
|
String _label = ''; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
_label = _templateData['options'].firstWhere( |
||||
|
(option) => listEquals(_templateData['value'], option['value']), |
||||
|
orElse: () => {'label': ''}, |
||||
|
)['label']; |
||||
|
} |
||||
|
|
||||
|
void _setLabel() { |
||||
|
setState(() { |
||||
|
_label = _templateData['options'].firstWhere( |
||||
|
(option) => listEquals(_templateData['value'], option['value']), |
||||
|
orElse: () => {'label': ''}, |
||||
|
)['label']; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return GestureDetector( |
||||
|
onTap: () async { |
||||
|
var result = await Get.to(() => SelectEqualizerWidget(_templateData)); |
||||
|
_setLabel(); |
||||
|
if (result != null) { |
||||
|
_templateData['onCallback'](_templateData['value']); |
||||
|
} |
||||
|
}, |
||||
|
behavior: HitTestBehavior.translucent, |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
_templateData['name'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Text( |
||||
|
_label, |
||||
|
style: TextStyle( |
||||
|
fontSize: 12.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.grey, |
||||
|
), |
||||
|
), |
||||
|
Icon( |
||||
|
Icons.chevron_right, |
||||
|
size: 26.w, |
||||
|
color: Colors.blue, |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
|
||||
|
class RowGradeWidget extends StatefulWidget { |
||||
|
final Map templateData; |
||||
|
|
||||
|
const RowGradeWidget(this.templateData, {super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<RowGradeWidget> createState() => _RowGradeWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _RowGradeWidgetState extends State<RowGradeWidget> { |
||||
|
Map get _templateData => widget.templateData; |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return Padding( |
||||
|
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
_templateData['name'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Slider( |
||||
|
value: _templateData['value'].toDouble(), |
||||
|
min: 0, |
||||
|
max: 10, |
||||
|
divisions: 10, |
||||
|
activeColor: Colors.blue, |
||||
|
onChanged: (value) { |
||||
|
setState(() { |
||||
|
_templateData['value'] = value.toInt(); |
||||
|
}); |
||||
|
}, |
||||
|
onChangeEnd: (value) { |
||||
|
_templateData['onCallback'](_templateData['value']); |
||||
|
}, |
||||
|
), |
||||
|
SizedBox( |
||||
|
width: 15.w, |
||||
|
child: Text( |
||||
|
_templateData['value'].toString(), |
||||
|
textAlign: TextAlign.right, |
||||
|
style: TextStyle( |
||||
|
fontSize: 12.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.grey, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
import 'settings_input_widget.dart'; |
||||
|
|
||||
|
class RowInputWidget extends StatefulWidget { |
||||
|
final Map templateData; |
||||
|
|
||||
|
const RowInputWidget(this.templateData, {super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<RowInputWidget> createState() => _RowInputWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _RowInputWidgetState extends State<RowInputWidget> { |
||||
|
Map get _templateData => widget.templateData; |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return GestureDetector( |
||||
|
onTap: () => _inputBottomSheet(isDarkMode), |
||||
|
behavior: HitTestBehavior.translucent, |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
_templateData['name'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Text( |
||||
|
_templateData['value'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 12.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.grey, |
||||
|
), |
||||
|
), |
||||
|
Icon( |
||||
|
Icons.chevron_right, |
||||
|
size: 26.w, |
||||
|
color: Colors.blue, |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _inputBottomSheet(bool isDarkMode) { |
||||
|
Get.bottomSheet( |
||||
|
SettingsInputWidget(_templateData['name'], _templateData['value']), |
||||
|
isScrollControlled: true, |
||||
|
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.vertical( |
||||
|
top: Radius.circular(12.r), |
||||
|
), |
||||
|
), |
||||
|
).then((value) { |
||||
|
if (value != null) { |
||||
|
setState(() { |
||||
|
_templateData['value'] = value; |
||||
|
}); |
||||
|
_templateData['onCallback'](value); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,242 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
class RowKeypressWidget extends StatefulWidget { |
||||
|
final Map templateData; |
||||
|
|
||||
|
const RowKeypressWidget(this.templateData, {super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<RowKeypressWidget> createState() => _RowKeypressWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _RowKeypressWidgetState extends State<RowKeypressWidget> { |
||||
|
Map get _templateData => widget.templateData; |
||||
|
|
||||
|
String _getLabel(int value) { |
||||
|
return _templateData['options'].firstWhere((item) => item['value'] == value, |
||||
|
orElse: () => {'label': ''})['label']; |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return Column( |
||||
|
children: [ |
||||
|
Padding( |
||||
|
padding: EdgeInsets.symmetric(vertical: 10.w), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
const Spacer(), |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
'左', |
||||
|
textAlign: TextAlign.center, |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
'右', |
||||
|
textAlign: TextAlign.center, |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
..._templateData['data'] |
||||
|
.map((item) => _rowItem(isDarkMode, item)) |
||||
|
.toList(), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _rowItem(bool isDarkMode, Map item) { |
||||
|
return Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.symmetric(vertical: 10.w), |
||||
|
child: Text( |
||||
|
item['label'], |
||||
|
textAlign: TextAlign.center, |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Expanded( |
||||
|
child: GestureDetector( |
||||
|
onTap: () => _radioDialog(isDarkMode, item, 'left'), |
||||
|
behavior: HitTestBehavior.translucent, |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.symmetric(vertical: 10.w), |
||||
|
child: Text( |
||||
|
_getLabel(item['left']), |
||||
|
textAlign: TextAlign.center, |
||||
|
style: TextStyle( |
||||
|
fontSize: 12.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.grey, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Expanded( |
||||
|
child: GestureDetector( |
||||
|
onTap: () => _radioDialog(isDarkMode, item, 'right'), |
||||
|
behavior: HitTestBehavior.translucent, |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.symmetric(vertical: 10.w), |
||||
|
child: Text( |
||||
|
_getLabel(item['right']), |
||||
|
textAlign: TextAlign.center, |
||||
|
style: TextStyle( |
||||
|
fontSize: 12.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.grey, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _radioDialog(bool isDarkMode, Map item, String key) { |
||||
|
Get.dialog( |
||||
|
Dialog( |
||||
|
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(16.r), |
||||
|
), |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.all(12.w), |
||||
|
child: Column( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
Text( |
||||
|
'${item['label']}-${key == 'left' ? '左' : '右'}', |
||||
|
style: TextStyle( |
||||
|
fontSize: 18.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isDarkMode ? Colors.white : Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 16.h), |
||||
|
Container( |
||||
|
constraints: BoxConstraints(maxHeight: 0.5.sh), |
||||
|
child: SingleChildScrollView( |
||||
|
child: Column( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: _templateData['options'] |
||||
|
.map<Widget>((option) => |
||||
|
_radioItem(isDarkMode, option, item, key)) |
||||
|
.toList(), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 16.h), |
||||
|
Row( |
||||
|
mainAxisAlignment: MainAxisAlignment.end, |
||||
|
children: [ |
||||
|
TextButton( |
||||
|
onPressed: () => Get.back(), |
||||
|
child: Text( |
||||
|
'cancel'.tr, // 取消 |
||||
|
style: TextStyle( |
||||
|
color: isDarkMode ? Colors.white70 : Colors.black54, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _radioItem(bool isDarkMode, Map option, Map item, String key) { |
||||
|
return GestureDetector( |
||||
|
onTap: () { |
||||
|
setState(() { |
||||
|
item[key] = option['value']; |
||||
|
}); |
||||
|
Map data = { |
||||
|
'key': item['key'], |
||||
|
key: option['value'], |
||||
|
}; |
||||
|
_templateData['onCallback'](data); |
||||
|
Get.back(); |
||||
|
}, |
||||
|
child: Container( |
||||
|
margin: EdgeInsets.only(bottom: 10.h), |
||||
|
padding: EdgeInsets.all(10.w), |
||||
|
decoration: BoxDecoration( |
||||
|
color: item[key] == option['value'] |
||||
|
? (isDarkMode |
||||
|
? Colors.teal[900]!.withValues(alpha: 0.3) |
||||
|
: Colors.teal[50]!) |
||||
|
: (isDarkMode |
||||
|
? Colors.white.withValues(alpha: 0.05) |
||||
|
: Colors.grey[100]!), |
||||
|
borderRadius: BorderRadius.circular(12.r), |
||||
|
border: Border.all( |
||||
|
color: item[key] == option['value'] |
||||
|
? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!) |
||||
|
: Colors.transparent, |
||||
|
width: 1, |
||||
|
), |
||||
|
), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
option['label'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
fontWeight: FontWeight.w500, |
||||
|
color: isDarkMode ? Colors.white : Colors.black87, |
||||
|
), |
||||
|
overflow: TextOverflow.ellipsis, |
||||
|
), |
||||
|
), |
||||
|
Container( |
||||
|
width: 24.w, |
||||
|
height: 24.w, |
||||
|
decoration: BoxDecoration( |
||||
|
color: item[key] == option['value'] |
||||
|
? (isDarkMode ? Colors.teal[300] : Colors.teal[600]) |
||||
|
: (isDarkMode |
||||
|
? Colors.white.withValues(alpha: 0.1) |
||||
|
: Colors.grey[300]), |
||||
|
shape: BoxShape.circle, |
||||
|
), |
||||
|
child: item[key] == option['value'] |
||||
|
? Icon( |
||||
|
Icons.check, |
||||
|
size: 16.w, |
||||
|
color: Colors.white, |
||||
|
) |
||||
|
: null, |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,187 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
class RowRadioWidget extends StatefulWidget { |
||||
|
final Map templateData; |
||||
|
|
||||
|
const RowRadioWidget(this.templateData, {super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<RowRadioWidget> createState() => _RowRadioWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _RowRadioWidgetState extends State<RowRadioWidget> { |
||||
|
Map get _templateData => widget.templateData; |
||||
|
|
||||
|
String _label = ''; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
_label = _templateData['options'].firstWhere( |
||||
|
(option) => option['value'] == _templateData['value'], |
||||
|
orElse: () => {'label': ''}, |
||||
|
)['label']; |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return GestureDetector( |
||||
|
onTap: () => _radioDialog(isDarkMode), |
||||
|
behavior: HitTestBehavior.translucent, |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
_templateData['name'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Text( |
||||
|
_label, |
||||
|
style: TextStyle( |
||||
|
fontSize: 12.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.grey, |
||||
|
), |
||||
|
), |
||||
|
Icon( |
||||
|
Icons.chevron_right, |
||||
|
size: 26.w, |
||||
|
color: Colors.blue, |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _radioDialog(bool isDarkMode) { |
||||
|
Get.dialog( |
||||
|
Dialog( |
||||
|
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(16.r), |
||||
|
), |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.all(12.w), |
||||
|
child: Column( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
Text( |
||||
|
_templateData['name'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 18.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isDarkMode ? Colors.white : Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 16.h), |
||||
|
Container( |
||||
|
constraints: BoxConstraints(maxHeight: 0.5.sh), |
||||
|
child: SingleChildScrollView( |
||||
|
child: Column( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: _templateData['options'] |
||||
|
.map<Widget>((option) => _item(isDarkMode, option)) |
||||
|
.toList(), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 16.h), |
||||
|
Row( |
||||
|
mainAxisAlignment: MainAxisAlignment.end, |
||||
|
children: [ |
||||
|
TextButton( |
||||
|
onPressed: () => Get.back(), |
||||
|
child: Text( |
||||
|
'cancel'.tr, // 取消 |
||||
|
style: TextStyle( |
||||
|
color: isDarkMode ? Colors.white70 : Colors.black54, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _item(bool isDarkMode, Map option) { |
||||
|
return GestureDetector( |
||||
|
onTap: () { |
||||
|
setState(() { |
||||
|
_label = option['label']; |
||||
|
}); |
||||
|
_templateData['value'] = option['value']; |
||||
|
_templateData['onCallback'](option['value']); |
||||
|
Get.back(); |
||||
|
}, |
||||
|
child: Container( |
||||
|
margin: EdgeInsets.only(bottom: 10.h), |
||||
|
padding: EdgeInsets.all(10.w), |
||||
|
decoration: BoxDecoration( |
||||
|
color: _templateData['value'] == option['value'] |
||||
|
? (isDarkMode |
||||
|
? Colors.teal[900]!.withValues(alpha: 0.3) |
||||
|
: Colors.teal[50]!) |
||||
|
: (isDarkMode |
||||
|
? Colors.white.withValues(alpha: 0.05) |
||||
|
: Colors.grey[100]!), |
||||
|
borderRadius: BorderRadius.circular(12.r), |
||||
|
border: Border.all( |
||||
|
color: _templateData['value'] == option['value'] |
||||
|
? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!) |
||||
|
: Colors.transparent, |
||||
|
width: 1, |
||||
|
), |
||||
|
), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
option['label'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
fontWeight: FontWeight.w500, |
||||
|
color: isDarkMode ? Colors.white : Colors.black87, |
||||
|
), |
||||
|
overflow: TextOverflow.ellipsis, |
||||
|
), |
||||
|
), |
||||
|
Container( |
||||
|
width: 24.w, |
||||
|
height: 24.w, |
||||
|
decoration: BoxDecoration( |
||||
|
color: _templateData['value'] == option['value'] |
||||
|
? (isDarkMode ? Colors.teal[300] : Colors.teal[600]) |
||||
|
: (isDarkMode |
||||
|
? Colors.white.withValues(alpha: 0.1) |
||||
|
: Colors.grey[300]), |
||||
|
shape: BoxShape.circle, |
||||
|
), |
||||
|
child: _templateData['value'] == option['value'] |
||||
|
? Icon( |
||||
|
Icons.check, |
||||
|
size: 16.w, |
||||
|
color: Colors.white, |
||||
|
) |
||||
|
: null, |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
|
||||
|
class RowSwitchWidget extends StatefulWidget { |
||||
|
final Map templateData; |
||||
|
|
||||
|
const RowSwitchWidget(this.templateData, {super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<RowSwitchWidget> createState() => _RowSwitchWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _RowSwitchWidgetState extends State<RowSwitchWidget> { |
||||
|
Map get _templateData => widget.templateData; |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return Padding( |
||||
|
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
_templateData['name'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
if (_templateData['description'] != null) |
||||
|
Text( |
||||
|
_templateData['description'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 12.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.grey, |
||||
|
), |
||||
|
), |
||||
|
SizedBox( |
||||
|
height: 26.w, |
||||
|
child: Transform.scale( |
||||
|
scale: 0.8, |
||||
|
alignment: Alignment.centerRight, |
||||
|
child: Switch( |
||||
|
value: _templateData['value'], |
||||
|
onChanged: (bool value) { |
||||
|
setState(() { |
||||
|
_templateData['value'] = value; |
||||
|
}); |
||||
|
_templateData['onCallback'](value); |
||||
|
}, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
import 'select_timer_widget.dart'; |
||||
|
|
||||
|
class RowTimerWidget extends StatefulWidget { |
||||
|
final Map templateData; |
||||
|
|
||||
|
const RowTimerWidget(this.templateData, {super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<RowTimerWidget> createState() => _RowTimerWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _RowTimerWidgetState extends State<RowTimerWidget> { |
||||
|
Map get _templateData => widget.templateData; |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return GestureDetector( |
||||
|
onTap: () => _timerBottomSheet(isDarkMode), |
||||
|
behavior: HitTestBehavior.translucent, |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: Text( |
||||
|
_templateData['name'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
if (_templateData['value'] > 0) |
||||
|
Text( |
||||
|
'${_templateData['value']}分钟', |
||||
|
style: TextStyle( |
||||
|
fontSize: 12.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.grey, |
||||
|
), |
||||
|
), |
||||
|
Icon( |
||||
|
Icons.chevron_right, |
||||
|
size: 26.w, |
||||
|
color: Colors.blue, |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _timerBottomSheet(bool isDarkMode) { |
||||
|
Get.bottomSheet( |
||||
|
SelectTimerWidget(_templateData['name'], _templateData['value']), |
||||
|
isScrollControlled: true, |
||||
|
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.vertical( |
||||
|
top: Radius.circular(12.r), |
||||
|
), |
||||
|
), |
||||
|
).then((value) { |
||||
|
if (value != null) { |
||||
|
setState(() { |
||||
|
_templateData['value'] = value; |
||||
|
}); |
||||
|
_templateData['onCallback'](value); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,305 @@ |
|||||
|
import 'package:flutter/foundation.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:get_storage/get_storage.dart'; |
||||
|
|
||||
|
import 'settings_input_widget.dart'; |
||||
|
|
||||
|
class SelectEqualizerWidget extends StatefulWidget { |
||||
|
final Map templateData; |
||||
|
|
||||
|
const SelectEqualizerWidget(this.templateData, {super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<SelectEqualizerWidget> createState() => _SelectEqualizerWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _SelectEqualizerWidgetState extends State<SelectEqualizerWidget> { |
||||
|
Map get _templateData => widget.templateData; |
||||
|
|
||||
|
List _values = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; |
||||
|
|
||||
|
bool isEdit = false; |
||||
|
|
||||
|
final GetStorage _storage = GetStorage(); |
||||
|
List _equalizerOptions = []; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
if (_templateData['value'].isNotEmpty) { |
||||
|
_values = List.from(_templateData['value']); |
||||
|
} |
||||
|
_equalizerOptions = _storage.read('equalizer_options') ?? []; |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
return Scaffold( |
||||
|
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], |
||||
|
appBar: _buildAppBar(isDarkMode), |
||||
|
body: _buildBody(isDarkMode), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 构建应用栏 |
||||
|
PreferredSizeWidget _buildAppBar(bool isDarkMode) { |
||||
|
return AppBar( |
||||
|
backgroundColor: isDarkMode ? Colors.black : Colors.white, |
||||
|
surfaceTintColor: Colors.transparent, |
||||
|
elevation: 0.5, |
||||
|
leading: IconButton( |
||||
|
icon: Icon( |
||||
|
Icons.arrow_back_ios, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
size: 20.sp, |
||||
|
), |
||||
|
onPressed: () => Get.back(), |
||||
|
), |
||||
|
title: Text( |
||||
|
_templateData['name'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 17.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isDarkMode ? Colors.white : Colors.black, |
||||
|
), |
||||
|
), |
||||
|
centerTitle: true, |
||||
|
actions: [ |
||||
|
GestureDetector( |
||||
|
onTap: () { |
||||
|
setState(() { |
||||
|
isEdit = !isEdit; |
||||
|
}); |
||||
|
}, |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.symmetric( |
||||
|
horizontal: 12.w, |
||||
|
vertical: 6.w, |
||||
|
), |
||||
|
child: Text( |
||||
|
isEdit ? 'cancel'.tr : 'edit'.tr, |
||||
|
style: TextStyle( |
||||
|
color: isDarkMode ? Colors.white : Colors.black87, |
||||
|
fontSize: 16.sp, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 构建主体内容 |
||||
|
Widget _buildBody(bool isDarkMode) { |
||||
|
bool isAdd = true; |
||||
|
|
||||
|
return ListView( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), |
||||
|
children: [ |
||||
|
_equalizer(isDarkMode), |
||||
|
Wrap( |
||||
|
spacing: 10.w, |
||||
|
runSpacing: 10.w, |
||||
|
children: [ |
||||
|
..._templateData['options'].map((Map option) { |
||||
|
bool isSelected = listEquals(_values, option['value']); |
||||
|
if (isSelected) { |
||||
|
isAdd = false; |
||||
|
} |
||||
|
return _item(isDarkMode, option, isSelected); |
||||
|
}).toList(), |
||||
|
if (isAdd) |
||||
|
GestureDetector( |
||||
|
onTap: () => _inputBottomSheet(isDarkMode), |
||||
|
child: Container( |
||||
|
width: 110.w, |
||||
|
height: 110.w, |
||||
|
alignment: Alignment.center, |
||||
|
decoration: BoxDecoration( |
||||
|
color: isDarkMode |
||||
|
? Colors.white.withValues(alpha: 0.05) |
||||
|
: Colors.white, |
||||
|
borderRadius: BorderRadius.circular(8.w), |
||||
|
boxShadow: [ |
||||
|
BoxShadow( |
||||
|
color: isDarkMode |
||||
|
? Colors.black.withValues(alpha: 0.2) |
||||
|
: Colors.black.withValues(alpha: 0.05), |
||||
|
blurRadius: 10, |
||||
|
offset: const Offset(0, 4), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
child: Icon( |
||||
|
Icons.add, |
||||
|
size: 40.sp, |
||||
|
color: Colors.green, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _equalizer(bool isDarkMode) { |
||||
|
return Container( |
||||
|
height: 220.w, |
||||
|
margin: EdgeInsets.only(bottom: 5.w), |
||||
|
child: Row( |
||||
|
children: List.generate( |
||||
|
_templateData['xAxis'].length, |
||||
|
(index) => Expanded( |
||||
|
child: Stack( |
||||
|
children: [ |
||||
|
RotatedBox( |
||||
|
quarterTurns: 3, |
||||
|
child: StatefulBuilder( |
||||
|
builder: (BuildContext context, setStateful) { |
||||
|
return Slider( |
||||
|
value: _values[index].toDouble(), |
||||
|
min: -5, |
||||
|
max: 5, |
||||
|
divisions: 10, |
||||
|
activeColor: Colors.green, |
||||
|
onChanged: (value) { |
||||
|
setStateful(() { |
||||
|
_values[index] = value.toInt(); |
||||
|
}); |
||||
|
}, |
||||
|
onChangeEnd: (value) { |
||||
|
setState(() {}); |
||||
|
}, |
||||
|
); |
||||
|
}, |
||||
|
), |
||||
|
), |
||||
|
Align( |
||||
|
alignment: Alignment.bottomCenter, |
||||
|
child: Text( |
||||
|
_templateData['xAxis'][index], |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.sp, |
||||
|
color: isDarkMode ? Colors.white : Colors.grey, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _item(bool isDarkMode, Map option, bool isSelected) { |
||||
|
return GestureDetector( |
||||
|
onTap: () { |
||||
|
_templateData['value'] = option['value']; |
||||
|
Get.back(result: true); |
||||
|
}, |
||||
|
child: Stack( |
||||
|
children: [ |
||||
|
Container( |
||||
|
width: 110.w, |
||||
|
height: 110.w, |
||||
|
alignment: Alignment.center, |
||||
|
decoration: BoxDecoration( |
||||
|
color: isSelected |
||||
|
? Colors.green |
||||
|
: isDarkMode |
||||
|
? Colors.white.withValues(alpha: 0.05) |
||||
|
: Colors.white, |
||||
|
borderRadius: BorderRadius.circular(8.w), |
||||
|
boxShadow: [ |
||||
|
BoxShadow( |
||||
|
color: isDarkMode |
||||
|
? Colors.black.withValues(alpha: 0.2) |
||||
|
: Colors.black.withValues(alpha: 0.05), |
||||
|
blurRadius: 10, |
||||
|
offset: const Offset(0, 4), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
child: Text( |
||||
|
option['label'], |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
fontWeight: FontWeight.w600, |
||||
|
color: isSelected |
||||
|
? Colors.white |
||||
|
: isDarkMode |
||||
|
? Colors.white |
||||
|
: Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
if (isEdit && !option['default']) |
||||
|
Positioned( |
||||
|
top: 4.w, |
||||
|
right: 4.w, |
||||
|
child: GestureDetector( |
||||
|
onTap: () { |
||||
|
if (isSelected) { |
||||
|
_templateData['value'] = []; |
||||
|
_templateData['onCallback']([]); |
||||
|
} |
||||
|
_equalizerOptions.removeWhere( |
||||
|
(e) => listEquals(e['value'], option['value'])); |
||||
|
_storage.write('equalizer_options', _equalizerOptions); |
||||
|
setState(() { |
||||
|
_templateData['options'].remove(option); |
||||
|
}); |
||||
|
}, |
||||
|
child: Container( |
||||
|
width: 24.w, |
||||
|
height: 24.w, |
||||
|
alignment: Alignment.center, |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.red, |
||||
|
borderRadius: BorderRadius.circular(12.w), |
||||
|
), |
||||
|
child: Icon( |
||||
|
Icons.close, |
||||
|
size: 16.sp, |
||||
|
color: Colors.white, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void _inputBottomSheet(bool isDarkMode) { |
||||
|
Get.bottomSheet( |
||||
|
const SettingsInputWidget('添加均衡器', ''), |
||||
|
isScrollControlled: true, |
||||
|
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.vertical( |
||||
|
top: Radius.circular(12.r), |
||||
|
), |
||||
|
), |
||||
|
).then((value) { |
||||
|
if (value != null) { |
||||
|
Map<String, Object> option = { |
||||
|
'label': value, |
||||
|
'value': List.from(_values), |
||||
|
'default': false, |
||||
|
}; |
||||
|
_equalizerOptions.add(option); |
||||
|
_storage.write('equalizer_options', _equalizerOptions); |
||||
|
setState(() { |
||||
|
_templateData['options'].add(option); |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,231 @@ |
|||||
|
import 'package:flutter/cupertino.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
class SelectTimerWidget extends StatefulWidget { |
||||
|
final String title; |
||||
|
final int time; |
||||
|
|
||||
|
const SelectTimerWidget(this.title, this.time, {super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<SelectTimerWidget> createState() => _SelectTimerWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _SelectTimerWidgetState extends State<SelectTimerWidget> { |
||||
|
late FixedExtentScrollController _hourScrollController; |
||||
|
late FixedExtentScrollController _minuteScrollController; |
||||
|
|
||||
|
int _hour = 0; |
||||
|
int _minute = 0; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
_hour = widget.time ~/ 60; |
||||
|
_minute = widget.time % 60; |
||||
|
_hourScrollController = FixedExtentScrollController(initialItem: _hour); |
||||
|
_minuteScrollController = FixedExtentScrollController(initialItem: _minute); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void dispose() { |
||||
|
super.dispose(); |
||||
|
_hourScrollController.dispose(); |
||||
|
_minuteScrollController.dispose(); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Padding( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 12.w), |
||||
|
child: Column( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: [ |
||||
|
Container( |
||||
|
padding: EdgeInsets.symmetric(vertical: 10.w), |
||||
|
child: Row( |
||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
|
children: [ |
||||
|
Text( |
||||
|
widget.title, |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
color: Colors.black87, |
||||
|
fontWeight: FontWeight.bold, |
||||
|
), |
||||
|
), |
||||
|
GestureDetector( |
||||
|
onTap: () => Get.back(), |
||||
|
child: Container( |
||||
|
width: 26.w, |
||||
|
height: 26.w, |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.white, |
||||
|
borderRadius: BorderRadius.circular(26.r), |
||||
|
), |
||||
|
child: Icon( |
||||
|
Icons.close, |
||||
|
size: 18.w, |
||||
|
color: Colors.grey[400], |
||||
|
), |
||||
|
), |
||||
|
) |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
Divider( |
||||
|
height: 1, |
||||
|
color: Colors.grey[300], |
||||
|
), |
||||
|
SizedBox( |
||||
|
height: 220.w, |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: CupertinoPicker.builder( |
||||
|
itemExtent: 40.w, |
||||
|
diameterRatio: 1.1, |
||||
|
squeeze: 1.25, |
||||
|
childCount: 24, |
||||
|
selectionOverlay: _selectionOverlayWidget( |
||||
|
isRadiusLeft: true, |
||||
|
), |
||||
|
scrollController: _hourScrollController, |
||||
|
itemBuilder: (BuildContext context, int index) { |
||||
|
return Container( |
||||
|
padding: EdgeInsets.only(right: 10.w), |
||||
|
alignment: Alignment.centerRight, |
||||
|
child: Text( |
||||
|
'$index时', |
||||
|
style: _hour == index |
||||
|
? TextStyle( |
||||
|
color: const Color(0xFFFF5561), |
||||
|
fontSize: 15.sp, |
||||
|
fontWeight: FontWeight.w500, |
||||
|
) |
||||
|
: TextStyle( |
||||
|
color: const Color(0xFF9D9D9D), |
||||
|
fontSize: 15.sp, |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
}, |
||||
|
onSelectedItemChanged: (int index) { |
||||
|
setState(() { |
||||
|
_hour = index; |
||||
|
}); |
||||
|
}, |
||||
|
), |
||||
|
), |
||||
|
Expanded( |
||||
|
child: CupertinoPicker.builder( |
||||
|
itemExtent: 40.w, |
||||
|
diameterRatio: 1.1, |
||||
|
squeeze: 1.25, |
||||
|
childCount: 60, |
||||
|
selectionOverlay: _selectionOverlayWidget( |
||||
|
isRadiusRight: true, |
||||
|
), |
||||
|
scrollController: _minuteScrollController, |
||||
|
itemBuilder: (BuildContext context, int index) { |
||||
|
return Container( |
||||
|
padding: EdgeInsets.only(left: 10.w), |
||||
|
alignment: Alignment.centerLeft, |
||||
|
child: Text( |
||||
|
'$index分', |
||||
|
style: _minute == index |
||||
|
? TextStyle( |
||||
|
color: const Color(0xFFFF5561), |
||||
|
fontSize: 15.sp, |
||||
|
fontWeight: FontWeight.w500, |
||||
|
) |
||||
|
: TextStyle( |
||||
|
color: const Color(0xFF9D9D9D), |
||||
|
fontSize: 15.sp, |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
}, |
||||
|
onSelectedItemChanged: (int index) { |
||||
|
setState(() { |
||||
|
_minute = index; |
||||
|
}); |
||||
|
}, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
Padding( |
||||
|
padding: EdgeInsets.only(bottom: 20.w), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: GestureDetector( |
||||
|
onTap: () => Get.back(), |
||||
|
child: Container( |
||||
|
height: 48.w, |
||||
|
alignment: Alignment.center, |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.grey[300], |
||||
|
borderRadius: BorderRadius.circular(12.r), |
||||
|
), |
||||
|
child: Text( |
||||
|
'cancel'.tr, // 取消 |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
color: Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
10.horizontalSpace, |
||||
|
Expanded( |
||||
|
child: GestureDetector( |
||||
|
onTap: () { |
||||
|
int time = _hour * 60 + _minute; |
||||
|
Get.back(result: time); |
||||
|
}, |
||||
|
child: Container( |
||||
|
height: 48.w, |
||||
|
alignment: Alignment.center, |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.black, |
||||
|
borderRadius: BorderRadius.circular(12.r), |
||||
|
), |
||||
|
child: Text( |
||||
|
'confirm'.tr, // 确认 |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
color: Colors.white, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _selectionOverlayWidget({ |
||||
|
bool isRadiusLeft = false, |
||||
|
bool isRadiusRight = false, |
||||
|
}) { |
||||
|
return Container( |
||||
|
decoration: BoxDecoration( |
||||
|
color: const Color.fromARGB(45, 255, 85, 97), |
||||
|
borderRadius: BorderRadius.horizontal( |
||||
|
left: isRadiusLeft ? Radius.circular(8.w) : Radius.zero, |
||||
|
right: isRadiusRight ? Radius.circular(8.w) : Radius.zero, |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,162 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
class SettingsInputWidget extends StatefulWidget { |
||||
|
final String title; |
||||
|
final String inputText; |
||||
|
|
||||
|
const SettingsInputWidget(this.title, this.inputText, {super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<SettingsInputWidget> createState() => _SettingsInputWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _SettingsInputWidgetState extends State<SettingsInputWidget> { |
||||
|
final TextEditingController _inputController = TextEditingController(); |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
_inputController.text = widget.inputText; |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void dispose() { |
||||
|
_inputController.dispose(); |
||||
|
super.dispose(); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Padding( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 12.w), |
||||
|
child: Column( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: [ |
||||
|
Container( |
||||
|
padding: EdgeInsets.symmetric(vertical: 10.w), |
||||
|
child: Row( |
||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
|
children: [ |
||||
|
Text( |
||||
|
widget.title, |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
color: Colors.black87, |
||||
|
fontWeight: FontWeight.bold, |
||||
|
), |
||||
|
), |
||||
|
GestureDetector( |
||||
|
onTap: () => Get.back(), |
||||
|
child: Container( |
||||
|
width: 26.w, |
||||
|
height: 26.w, |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.white, |
||||
|
borderRadius: BorderRadius.circular(26.r), |
||||
|
), |
||||
|
child: Icon( |
||||
|
Icons.close, |
||||
|
size: 18.w, |
||||
|
color: Colors.grey[400], |
||||
|
), |
||||
|
), |
||||
|
) |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
Divider( |
||||
|
height: 1, |
||||
|
color: Colors.grey[300], |
||||
|
), |
||||
|
Container( |
||||
|
margin: EdgeInsets.symmetric(vertical: 20.w), |
||||
|
alignment: Alignment.center, |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.white, |
||||
|
borderRadius: BorderRadius.circular(12.w), |
||||
|
boxShadow: [ |
||||
|
BoxShadow( |
||||
|
color: Colors.black.withValues(alpha: 0.05), |
||||
|
blurRadius: 10, |
||||
|
offset: const Offset(0, 2), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
child: TextField( |
||||
|
controller: _inputController, |
||||
|
style: TextStyle( |
||||
|
color: Colors.black87, |
||||
|
fontSize: 16.sp, |
||||
|
), |
||||
|
decoration: InputDecoration( |
||||
|
hintText: 'maxCharacters'.tr, |
||||
|
hintStyle: TextStyle( |
||||
|
color: Colors.black54, |
||||
|
fontSize: 16.sp, |
||||
|
), |
||||
|
isDense: true, |
||||
|
fillColor: Colors.transparent, |
||||
|
border: InputBorder.none, |
||||
|
contentPadding: EdgeInsets.all(10.w), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Padding( |
||||
|
padding: EdgeInsets.only(bottom: 20.w), |
||||
|
child: Row( |
||||
|
children: [ |
||||
|
Expanded( |
||||
|
child: GestureDetector( |
||||
|
onTap: () => Get.back(), |
||||
|
child: Container( |
||||
|
height: 48.w, |
||||
|
alignment: Alignment.center, |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.grey[300], |
||||
|
borderRadius: BorderRadius.circular(12.r), |
||||
|
), |
||||
|
child: Text( |
||||
|
'cancel'.tr, // 取消 |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
color: Colors.black, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
10.horizontalSpace, |
||||
|
Expanded( |
||||
|
child: GestureDetector( |
||||
|
onTap: () { |
||||
|
if (_inputController.text.isNotEmpty) { |
||||
|
Get.back(result: _inputController.text); |
||||
|
} |
||||
|
}, |
||||
|
child: Container( |
||||
|
height: 48.w, |
||||
|
alignment: Alignment.center, |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.black, |
||||
|
borderRadius: BorderRadius.circular(12.r), |
||||
|
), |
||||
|
child: Text( |
||||
|
'confirm'.tr, // 确认 |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
color: Colors.white, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,195 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
|
||||
|
import '../../controllers/translation_controller.dart'; |
||||
|
import '../../models/translation_models.dart'; |
||||
|
import '../dialogs/translation_long_press_menu.dart'; |
||||
|
|
||||
|
class TranslationItemWidget extends GetView<TranslationController> { |
||||
|
// 传入的翻译条目 |
||||
|
final TranslationItem item; |
||||
|
|
||||
|
// 传入用于控制操作按钮显隐的时间戳状态 |
||||
|
final Rx<DateTime?> activeActionTimestamp; |
||||
|
|
||||
|
// 点击气泡时切换操作按钮显隐 |
||||
|
final void Function(TranslationItem item) onToggleActionItem; |
||||
|
|
||||
|
// 长按时弹出菜单(需要上下文、暗色模式、手势详情、条目) |
||||
|
final void Function( |
||||
|
BuildContext context, |
||||
|
bool isDarkMode, |
||||
|
LongPressStartDetails details, |
||||
|
TranslationItem item, |
||||
|
) onLongPressStart; |
||||
|
|
||||
|
// 删除按钮点击时的确认对话框 |
||||
|
final void Function(TranslationItem item) onDeleteItemConfirm; |
||||
|
|
||||
|
const TranslationItemWidget({ |
||||
|
Key? key, |
||||
|
required this.item, |
||||
|
required this.activeActionTimestamp, |
||||
|
required this.onToggleActionItem, |
||||
|
required this.onLongPressStart, |
||||
|
required this.onDeleteItemConfirm, |
||||
|
}) : super(key: key); |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
||||
|
|
||||
|
// 定义自适应颜色(与原实现保持一致) |
||||
|
final selfColor = |
||||
|
isDarkMode ? Colors.blue[800]!.withOpacity(0.15) : Colors.blue[50]!; |
||||
|
final otherColor = |
||||
|
isDarkMode ? Colors.green[800]!.withOpacity(0.15) : Colors.green[50]!; |
||||
|
|
||||
|
// 判断是否是己方的翻译项(依据控制器当前源语言) |
||||
|
final bool isSelf = |
||||
|
item.sourceLanguageCode == controller.sourceLanguageCode.value; |
||||
|
// 新增:通话模式标记,用于控制右侧显示与隐藏中间态 |
||||
|
final bool isCallMode = controller.currentMode.value == 'call'; |
||||
|
|
||||
|
return GestureDetector( |
||||
|
onTap: () => onToggleActionItem(item), // 触碰气泡时切换按钮显隐 |
||||
|
onLongPressStart: (details) => |
||||
|
onLongPressStart(context, isDarkMode, details, item), |
||||
|
child: Align( |
||||
|
alignment: isCallMode |
||||
|
? (isSelf ? Alignment.centerLeft : Alignment.centerRight) |
||||
|
: item.isIntermediate |
||||
|
? Alignment.center |
||||
|
: (isSelf ? Alignment.centerLeft : Alignment.centerRight), |
||||
|
child: Container( |
||||
|
margin: EdgeInsets.only(bottom: 8.h), |
||||
|
padding: EdgeInsets.all(12.w), |
||||
|
constraints: BoxConstraints(maxWidth: 0.8.sw, minWidth: 0.35.sw), |
||||
|
decoration: BoxDecoration( |
||||
|
color: item.isIntermediate |
||||
|
? Colors.grey.withOpacity(isDarkMode ? 0.15 : 0.1) // 中间结果 |
||||
|
: (isSelf ? selfColor : otherColor), // 自己 / 对方 |
||||
|
borderRadius: BorderRadius.circular(12.r), |
||||
|
border: Border.all( |
||||
|
color: item.isIntermediate |
||||
|
? Colors.grey.withOpacity(isDarkMode ? 0.3 : 0.2) |
||||
|
: Colors.transparent, |
||||
|
width: 1, |
||||
|
), |
||||
|
), |
||||
|
child: Column( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
// 源文本 |
||||
|
Text( |
||||
|
item.sourceText, |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
color: Theme.of(context).textTheme.bodyLarge?.color, |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 8.h), |
||||
|
// 分隔线 + 译文 |
||||
|
Container( |
||||
|
padding: EdgeInsets.only(top: 8.h), |
||||
|
decoration: BoxDecoration( |
||||
|
border: Border( |
||||
|
top: BorderSide( |
||||
|
color: item.isIntermediate |
||||
|
? (isDarkMode |
||||
|
? Colors.grey[400]!.withOpacity(0.15) |
||||
|
: Colors.grey[400]!.withOpacity(0.25)) |
||||
|
: (isSelf |
||||
|
? (isDarkMode |
||||
|
? Colors.blue[300]!.withOpacity(0.15) |
||||
|
: Colors.blue[300]!.withOpacity(0.25)) |
||||
|
: (isDarkMode |
||||
|
? Colors.green[300]!.withOpacity(0.15) |
||||
|
: Colors.green[300]!.withOpacity(0.25))), |
||||
|
width: 1, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
child: Text( |
||||
|
item.translatedText, |
||||
|
style: TextStyle( |
||||
|
fontSize: 16.sp, |
||||
|
color: Theme.of(context).textTheme.bodyMedium?.color, |
||||
|
fontWeight: FontWeight.w500, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
// 操作按钮:复制 / 播放 / 删除(仅非中间态且显隐状态为激活时显示) |
||||
|
Obx(() { |
||||
|
final bool isActive = activeActionTimestamp.value |
||||
|
?.isAtSameMomentAs(item.timestamp) ?? |
||||
|
false; |
||||
|
if (!item.isIntermediate && isActive) { |
||||
|
return Column( |
||||
|
children: [ |
||||
|
SizedBox(height: 8.h), |
||||
|
Row( |
||||
|
mainAxisSize: MainAxisSize.min, |
||||
|
children: [ |
||||
|
// 复制按钮(复用长按菜单的复制逻辑) |
||||
|
GestureDetector( |
||||
|
onTap: () => |
||||
|
TranslationLongPressMenu.copyTranslationItem( |
||||
|
item), |
||||
|
child: Container( |
||||
|
padding: EdgeInsets.all(4.w), |
||||
|
child: Tooltip( |
||||
|
message: 'copy'.tr, // 翻译键代表的中文文本内容 |
||||
|
child: Icon( |
||||
|
Icons.copy, |
||||
|
size: 20.sp, |
||||
|
color: Theme.of(context).colorScheme.primary, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
SizedBox(width: 8.w), |
||||
|
// 播放按钮(调用控制器原有方法) |
||||
|
GestureDetector( |
||||
|
onTap: () => controller.playTranslationItem(item), |
||||
|
child: Container( |
||||
|
padding: EdgeInsets.all(4.w), |
||||
|
child: Icon( |
||||
|
Icons.volume_up, |
||||
|
size: 20.sp, |
||||
|
color: Theme.of(context).colorScheme.primary, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
SizedBox(width: 8.w), |
||||
|
// 删除按钮(弹出确认) |
||||
|
GestureDetector( |
||||
|
onTap: () => onDeleteItemConfirm(item), |
||||
|
child: Container( |
||||
|
padding: EdgeInsets.all(4.w), |
||||
|
child: Tooltip( |
||||
|
message: 'delete'.tr, // 翻译键代表的中文文本内容 |
||||
|
child: Icon( |
||||
|
Icons.delete_outline, |
||||
|
size: 20.sp, |
||||
|
color: Colors.grey[500], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
) |
||||
|
], |
||||
|
); |
||||
|
} else { |
||||
|
return const SizedBox.shrink(); |
||||
|
} |
||||
|
}), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
# Miscellaneous |
||||
|
*.class |
||||
|
*.log |
||||
|
*.pyc |
||||
|
*.swp |
||||
|
.DS_Store |
||||
|
.atom/ |
||||
|
.build/ |
||||
|
.buildlog/ |
||||
|
.history |
||||
|
.svn/ |
||||
|
.swiftpm/ |
||||
|
migrate_working_dir/ |
||||
|
|
||||
|
# IntelliJ related |
||||
|
*.iml |
||||
|
*.ipr |
||||
|
*.iws |
||||
|
.idea/ |
||||
|
|
||||
|
# The .vscode folder contains launch configuration and tasks you configure in |
||||
|
# VS Code which you may wish to be included in version control, so this line |
||||
|
# is commented out by default. |
||||
|
#.vscode/ |
||||
|
|
||||
|
# Flutter/Dart/Pub related |
||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. |
||||
|
/pubspec.lock |
||||
|
**/doc/api/ |
||||
|
.dart_tool/ |
||||
|
.flutter-plugins |
||||
|
.flutter-plugins-dependencies |
||||
|
build/ |
||||
@ -0,0 +1,33 @@ |
|||||
|
# This file tracks properties of this Flutter project. |
||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc. |
||||
|
# |
||||
|
# This file should be version controlled and should not be manually edited. |
||||
|
|
||||
|
version: |
||||
|
revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1" |
||||
|
channel: "stable" |
||||
|
|
||||
|
project_type: plugin |
||||
|
|
||||
|
# Tracks metadata for the flutter migrate command |
||||
|
migration: |
||||
|
platforms: |
||||
|
- platform: root |
||||
|
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 |
||||
|
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 |
||||
|
- platform: android |
||||
|
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 |
||||
|
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 |
||||
|
- platform: ios |
||||
|
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 |
||||
|
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 |
||||
|
|
||||
|
# User provided section |
||||
|
|
||||
|
# List of Local paths (relative to this file) that should be |
||||
|
# ignored by the migrate tool. |
||||
|
# |
||||
|
# Files that are not part of the templates will be ignored by default. |
||||
|
unmanaged_files: |
||||
|
- 'lib/main.dart' |
||||
|
- 'ios/Runner.xcodeproj/project.pbxproj' |
||||
@ -0,0 +1,3 @@ |
|||||
|
## 0.0.1 |
||||
|
|
||||
|
* TODO: Describe initial release. |
||||
@ -0,0 +1 @@ |
|||||
|
TODO: Add your license here. |
||||
@ -0,0 +1,15 @@ |
|||||
|
# native_plugin |
||||
|
|
||||
|
A new Flutter plugin project. |
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
This project is a starting point for a Flutter |
||||
|
[plug-in package](https://flutter.dev/to/develop-plugins), |
||||
|
a specialized package that includes platform-specific implementation code for |
||||
|
Android and/or iOS. |
||||
|
|
||||
|
For help getting started with Flutter development, view the |
||||
|
[online documentation](https://docs.flutter.dev), which offers tutorials, |
||||
|
samples, guidance on mobile development, and a full API reference. |
||||
|
|
||||
@ -0,0 +1,4 @@ |
|||||
|
include: package:flutter_lints/flutter.yaml |
||||
|
|
||||
|
# Additional information about this file can be found at |
||||
|
# https://dart.dev/guides/language/analysis-options |
||||
@ -0,0 +1,9 @@ |
|||||
|
*.iml |
||||
|
.gradle |
||||
|
/local.properties |
||||
|
/.idea/workspace.xml |
||||
|
/.idea/libraries |
||||
|
.DS_Store |
||||
|
/build |
||||
|
/captures |
||||
|
.cxx |
||||
@ -0,0 +1,52 @@ |
|||||
|
group = "com.example.native_plugin" |
||||
|
version = "1.0" |
||||
|
|
||||
|
buildscript { |
||||
|
repositories { |
||||
|
google() |
||||
|
mavenCentral() |
||||
|
} |
||||
|
|
||||
|
dependencies { |
||||
|
classpath("com.android.tools.build:gradle:8.7.0") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
rootProject.allprojects { |
||||
|
repositories { |
||||
|
google() |
||||
|
mavenCentral() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
apply plugin: "com.android.library" |
||||
|
|
||||
|
android { |
||||
|
namespace = "com.example.native_plugin" |
||||
|
|
||||
|
compileSdk = 35 |
||||
|
|
||||
|
compileOptions { |
||||
|
sourceCompatibility = JavaVersion.VERSION_11 |
||||
|
targetCompatibility = JavaVersion.VERSION_11 |
||||
|
} |
||||
|
|
||||
|
defaultConfig { |
||||
|
minSdk = 21 |
||||
|
} |
||||
|
|
||||
|
dependencies { |
||||
|
testImplementation("junit:junit:4.13.2") |
||||
|
testImplementation("org.mockito:mockito-core:5.0.0") |
||||
|
} |
||||
|
|
||||
|
testOptions { |
||||
|
unitTests.all { |
||||
|
testLogging { |
||||
|
events "passed", "skipped", "failed", "standardOut", "standardError" |
||||
|
outputs.upToDateWhen {false} |
||||
|
showStandardStreams = true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
rootProject.name = 'native_plugin' |
||||
@ -0,0 +1,3 @@ |
|||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
package="com.example.native_plugin"> |
||||
|
</manifest> |
||||
@ -0,0 +1,38 @@ |
|||||
|
package com.example.native_plugin; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
|
||||
|
import io.flutter.embedding.engine.plugins.FlutterPlugin; |
||||
|
import io.flutter.plugin.common.MethodCall; |
||||
|
import io.flutter.plugin.common.MethodChannel; |
||||
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler; |
||||
|
import io.flutter.plugin.common.MethodChannel.Result; |
||||
|
|
||||
|
/** NativePlugin */ |
||||
|
public class NativePlugin implements FlutterPlugin, MethodCallHandler { |
||||
|
/// The MethodChannel that will the communication between Flutter and native Android
|
||||
|
///
|
||||
|
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
||||
|
/// when the Flutter Engine is detached from the Activity
|
||||
|
private MethodChannel channel; |
||||
|
|
||||
|
@Override |
||||
|
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { |
||||
|
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "native_plugin"); |
||||
|
channel.setMethodCallHandler(this); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { |
||||
|
if (call.method.equals("getPlatformVersion")) { |
||||
|
result.success("Android " + android.os.Build.VERSION.RELEASE); |
||||
|
} else { |
||||
|
result.notImplemented(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { |
||||
|
channel.setMethodCallHandler(null); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
package com.example.native_plugin; |
||||
|
|
||||
|
import static org.mockito.Mockito.mock; |
||||
|
import static org.mockito.Mockito.verify; |
||||
|
|
||||
|
import io.flutter.plugin.common.MethodCall; |
||||
|
import io.flutter.plugin.common.MethodChannel; |
||||
|
import org.junit.Test; |
||||
|
|
||||
|
/** |
||||
|
* This demonstrates a simple unit test of the Java portion of this plugin's implementation. |
||||
|
* |
||||
|
* Once you have built the plugin's example app, you can run these tests from the command |
||||
|
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or |
||||
|
* you can run them directly from IDEs that support JUnit such as Android Studio. |
||||
|
*/ |
||||
|
|
||||
|
public class NativePluginTest { |
||||
|
@Test |
||||
|
public void onMethodCall_getPlatformVersion_returnsExpectedValue() { |
||||
|
NativePlugin plugin = new NativePlugin(); |
||||
|
|
||||
|
final MethodCall call = new MethodCall("getPlatformVersion", null); |
||||
|
MethodChannel.Result mockResult = mock(MethodChannel.Result.class); |
||||
|
plugin.onMethodCall(call, mockResult); |
||||
|
|
||||
|
verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
.idea/ |
||||
|
.vagrant/ |
||||
|
.sconsign.dblite |
||||
|
.svn/ |
||||
|
|
||||
|
.DS_Store |
||||
|
*.swp |
||||
|
profile |
||||
|
|
||||
|
DerivedData/ |
||||
|
build/ |
||||
|
GeneratedPluginRegistrant.h |
||||
|
GeneratedPluginRegistrant.m |
||||
|
|
||||
|
.generated/ |
||||
|
|
||||
|
*.pbxuser |
||||
|
*.mode1v3 |
||||
|
*.mode2v3 |
||||
|
*.perspectivev3 |
||||
|
|
||||
|
!default.pbxuser |
||||
|
!default.mode1v3 |
||||
|
!default.mode2v3 |
||||
|
!default.perspectivev3 |
||||
|
|
||||
|
xcuserdata |
||||
|
|
||||
|
*.moved-aside |
||||
|
|
||||
|
*.pyc |
||||
|
*sync/ |
||||
|
Icon? |
||||
|
.tags* |
||||
|
|
||||
|
/Flutter/Generated.xcconfig |
||||
|
/Flutter/ephemeral/ |
||||
|
/Flutter/flutter_export_environment.sh |
||||
@ -0,0 +1,15 @@ |
|||||
|
#import <flutter/flutter.h> |
||||
|
|
||||
|
@interface FLNativeViewFactory : NSObject <FlutterPlatformViewFactory> |
||||
|
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger> *)messenger; |
||||
|
@end |
||||
|
|
||||
|
@interface FLNativeView : NSObject <FlutterBinaryMessenger> |
||||
|
|
||||
|
- (instancetype)initWithFrame:(CGRect)frame |
||||
|
viewIdentifier:(int64_t)viewId |
||||
|
arguments:(id _Nullable)args |
||||
|
binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger; |
||||
|
|
||||
|
- (UIView *)view; |
||||
|
@end |
||||
@ -0,0 +1,83 @@ |
|||||
|
#import "FLNativeView.h" |
||||
|
#import "PlayerView.h" |
||||
|
|
||||
|
@implementation FLNativeViewFactory { |
||||
|
NSObject<FlutterBinaryMessenger> *_messenger; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initWithMessenger: |
||||
|
(NSObject<FlutterBinaryMessenger> *)messenger { |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_messenger = messenger; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame |
||||
|
viewIdentifier:(int64_t)viewId |
||||
|
arguments:(id _Nullable)args { |
||||
|
return [[FLNativeView alloc] initWithFrame:frame |
||||
|
viewIdentifier:viewId |
||||
|
arguments:args |
||||
|
binaryMessenger:_messenger]; |
||||
|
} |
||||
|
|
||||
|
/// Implementing this method is only necessary when the `arguments` in |
||||
|
/// `createWithFrame` is not `nil`. |
||||
|
- (NSObject<FlutterMessageCodec> *)createArgsCodec { |
||||
|
return [FlutterStandardMessageCodec sharedInstance]; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation FLNativeView { |
||||
|
PlayerView *_view; |
||||
|
FlutterMethodChannel *_methodChannel; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initWithFrame:(CGRect)frame |
||||
|
viewIdentifier:(int64_t)viewId |
||||
|
arguments:(id _Nullable)args |
||||
|
binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger { |
||||
|
if (self = [super init]) { |
||||
|
_view = [[PlayerView alloc] init]; |
||||
|
_methodChannel = [FlutterMethodChannel |
||||
|
methodChannelWithName: |
||||
|
[NSString |
||||
|
stringWithFormat:@"native_plugin/native_view_%lld", viewId] |
||||
|
binaryMessenger:messenger]; |
||||
|
__weak typeof(self) weakSelf = self; |
||||
|
[_methodChannel setMethodCallHandler:^(FlutterMethodCall *_Nonnull call, |
||||
|
FlutterResult _Nonnull result) { |
||||
|
typeof(self) strongSelf = weakSelf; |
||||
|
if (strongSelf != nil) { |
||||
|
[strongSelf onMethodCall:call result:result]; |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (UIView *)view { |
||||
|
return _view; |
||||
|
} |
||||
|
|
||||
|
- (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { |
||||
|
if ([@"getInternalView" isEqualToString:call.method]) { |
||||
|
result(@((uint64_t)(_view))); |
||||
|
} else if ([@"updateContent" isEqualToString:call.method]) { |
||||
|
NSDictionary *data = call.arguments; |
||||
|
NSString *lang = data[@"lang"]; |
||||
|
NSString *cn = data[@"cn"]; |
||||
|
NSString *en = data[@"en"]; |
||||
|
dispatch_async(dispatch_get_main_queue(), ^{ |
||||
|
[_view updateWithLang:lang cn:cn en:en]; |
||||
|
}); |
||||
|
result(nil); |
||||
|
} else { |
||||
|
result(@(0)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,4 @@ |
|||||
|
#import <Flutter/Flutter.h> |
||||
|
|
||||
|
@interface NativePlugin : NSObject<FlutterPlugin> |
||||
|
@end |
||||
@ -0,0 +1,114 @@ |
|||||
|
#import "NativePlugin.h" |
||||
|
#import "FLNativeView.h" |
||||
|
#import "PlayerView.h" |
||||
|
|
||||
|
#import <AVKit/AVKit.h> |
||||
|
|
||||
|
@interface NativePlugin () |
||||
|
|
||||
|
@property(nonatomic, strong) NSMutableArray *_Nonnull pipContentViewArray; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation NativePlugin |
||||
|
|
||||
|
+ (void)updateAudioSession { |
||||
|
AVAudioSession *audioSession = [AVAudioSession sharedInstance]; |
||||
|
NSError *categoryError = nil; |
||||
|
if (@available(iOS 14.5, *)) { |
||||
|
[audioSession |
||||
|
setCategory:AVAudioSessionCategoryPlayback |
||||
|
mode:AVAudioSessionModeMoviePlayback |
||||
|
options: |
||||
|
AVAudioSessionCategoryOptionOverrideMutedMicrophoneInterruption |
||||
|
error:&categoryError]; |
||||
|
} else { |
||||
|
// Fallback on earlier versions |
||||
|
} |
||||
|
if (categoryError) { |
||||
|
NSLog(@"Set audio session category error: %@", |
||||
|
categoryError.localizedDescription); |
||||
|
} |
||||
|
NSError *activeError = nil; |
||||
|
[audioSession setActive:YES error:&activeError]; |
||||
|
if (activeError) { |
||||
|
NSLog(@"Set audio session active error: %@", |
||||
|
activeError.localizedDescription); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar { |
||||
|
// must call this, otherwise pip will not possible |
||||
|
// https://github.com/jazzychad/PiPBugDemo |
||||
|
[NativePlugin updateAudioSession]; |
||||
|
|
||||
|
FlutterMethodChannel *channel = |
||||
|
[FlutterMethodChannel methodChannelWithName:@"native_plugin" |
||||
|
binaryMessenger:[registrar messenger]]; |
||||
|
NativePlugin *instance = [[NativePlugin alloc] init]; |
||||
|
|
||||
|
FLNativeViewFactory *factory = |
||||
|
[[FLNativeViewFactory alloc] initWithMessenger:registrar.messenger]; |
||||
|
[registrar registerViewFactory:factory withId:@"native_view"]; |
||||
|
|
||||
|
[registrar addMethodCallDelegate:instance channel:channel]; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)init { |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_pipContentViewArray = [NSMutableArray array]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)handleMethodCall:(FlutterMethodCall *)call |
||||
|
result:(FlutterResult)result { |
||||
|
if ([@"getPlatformVersion" isEqualToString:call.method]) { |
||||
|
result([@"iOS " |
||||
|
stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); |
||||
|
} else if ([@"createPipContentView" isEqualToString:call.method]) { |
||||
|
PlayerView *playerView = [[PlayerView alloc] init]; |
||||
|
|
||||
|
// should control the play state by event of pip, optimize it later |
||||
|
[playerView play]; |
||||
|
|
||||
|
[self.pipContentViewArray addObject:playerView]; |
||||
|
|
||||
|
result(@((uint64_t)playerView)); |
||||
|
} else if ([@"disposePipContentView" isEqualToString:call.method]) { |
||||
|
uint64_t viewId = [call.arguments unsignedLongLongValue]; |
||||
|
for (PlayerView *playerView in self.pipContentViewArray) { |
||||
|
if ((uint64_t)playerView == viewId) { |
||||
|
[playerView pause]; |
||||
|
[self.pipContentViewArray removeObject:playerView]; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} else if ([@"updatePipContentView" isEqualToString:call.method]) { |
||||
|
NSDictionary *data = call.arguments; |
||||
|
NSString *lang = data[@"lang"]; |
||||
|
NSString *cn = data[@"cn"]; |
||||
|
NSString *en = data[@"en"]; |
||||
|
NSNumber *viewIdNum = data[@"viewId"]; |
||||
|
dispatch_async(dispatch_get_main_queue(), ^{ |
||||
|
if (viewIdNum != nil) { |
||||
|
uint64_t targetId = viewIdNum.unsignedLongLongValue; |
||||
|
for (PlayerView *playerView in self.pipContentViewArray) { |
||||
|
if ((uint64_t)playerView == targetId) { |
||||
|
[playerView updateWithLang:lang cn:cn en:en]; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
PlayerView *playerView = [self.pipContentViewArray lastObject]; |
||||
|
[playerView updateWithLang:lang cn:cn en:en]; |
||||
|
} |
||||
|
}); |
||||
|
result(nil); |
||||
|
} else { |
||||
|
result(FlutterMethodNotImplemented); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,15 @@ |
|||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
NS_ASSUME_NONNULL_BEGIN |
||||
|
|
||||
|
@interface PlayerView : UIView |
||||
|
|
||||
|
- (void)play; |
||||
|
|
||||
|
- (void)pause; |
||||
|
|
||||
|
- (void)updateWithLang:(NSString *)lang cn:(NSString *)cn en:(NSString *)en; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
NS_ASSUME_NONNULL_END |
||||
@ -0,0 +1,100 @@ |
|||||
|
#import "PlayerView.h" |
||||
|
|
||||
|
@interface PlayerView () |
||||
|
|
||||
|
@property(nonatomic, strong) UILabel *titleLabel; |
||||
|
@property(nonatomic, strong) UILabel *langLabel; |
||||
|
@property(nonatomic, strong) UILabel *cnLabel; |
||||
|
@property(nonatomic, strong) UILabel *enLabel; |
||||
|
@property(nonatomic, strong) UIStackView *stackView; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation PlayerView |
||||
|
|
||||
|
- (instancetype)init { |
||||
|
if (self = [super init]) { |
||||
|
self.backgroundColor = [UIColor whiteColor]; |
||||
|
|
||||
|
UILabel *title = [[UILabel alloc] init]; |
||||
|
title.text = @"实时翻译"; |
||||
|
title.font = [UIFont boldSystemFontOfSize:22]; |
||||
|
title.textColor = [UIColor blackColor]; |
||||
|
title.textAlignment = NSTextAlignmentCenter; |
||||
|
title.numberOfLines = 1; |
||||
|
self.titleLabel = title; |
||||
|
|
||||
|
UILabel *lang = [[UILabel alloc] init]; |
||||
|
lang.text = @"中文(简体) -> 英语"; |
||||
|
lang.font = [UIFont systemFontOfSize:14]; |
||||
|
lang.textColor = [UIColor darkGrayColor]; |
||||
|
lang.textAlignment = NSTextAlignmentLeft; |
||||
|
lang.numberOfLines = 1; |
||||
|
self.langLabel = lang; |
||||
|
|
||||
|
UILabel *cn = [[UILabel alloc] init]; |
||||
|
cn.font = [UIFont systemFontOfSize:18]; |
||||
|
cn.textColor = [UIColor blackColor]; |
||||
|
cn.textAlignment = NSTextAlignmentLeft; |
||||
|
cn.numberOfLines = 0; |
||||
|
self.cnLabel = cn; |
||||
|
|
||||
|
UILabel *en = [[UILabel alloc] init]; |
||||
|
en.font = [UIFont systemFontOfSize:18]; |
||||
|
en.textColor = [UIColor colorWithRed:0.24 green:0.54 blue:0.90 alpha:1.0]; |
||||
|
en.textAlignment = NSTextAlignmentLeft; |
||||
|
en.numberOfLines = 0; |
||||
|
self.enLabel = en; |
||||
|
|
||||
|
NSMutableParagraphStyle *para = [[NSMutableParagraphStyle alloc] init]; |
||||
|
para.lineSpacing = 4; |
||||
|
|
||||
|
self.cnLabel.attributedText = [[NSAttributedString alloc] initWithString:@"正在识别..." attributes:@{NSParagraphStyleAttributeName: para}]; |
||||
|
|
||||
|
self.enLabel.attributedText = [[NSAttributedString alloc] initWithString:@"翻译中..." attributes:@{NSParagraphStyleAttributeName: para}]; |
||||
|
|
||||
|
UIStackView *stack = [[UIStackView alloc] initWithArrangedSubviews:@[self.titleLabel, self.langLabel, self.cnLabel, self.enLabel]]; |
||||
|
stack.axis = UILayoutConstraintAxisVertical; |
||||
|
stack.spacing = 12; |
||||
|
stack.alignment = UIStackViewAlignmentFill; |
||||
|
stack.distribution = UIStackViewDistributionFill; |
||||
|
stack.translatesAutoresizingMaskIntoConstraints = NO; |
||||
|
self.stackView = stack; |
||||
|
|
||||
|
self.layoutMargins = UIEdgeInsetsMake(16, 16, 16, 16); |
||||
|
[self addSubview:self.stackView]; |
||||
|
|
||||
|
UILayoutGuide *guide = self.layoutMarginsGuide; |
||||
|
[NSLayoutConstraint activateConstraints:@[ |
||||
|
[self.stackView.topAnchor constraintEqualToAnchor:guide.topAnchor], |
||||
|
[self.stackView.leadingAnchor constraintEqualToAnchor:guide.leadingAnchor], |
||||
|
[self.stackView.trailingAnchor constraintEqualToAnchor:guide.trailingAnchor], |
||||
|
[self.stackView.bottomAnchor constraintLessThanOrEqualToAnchor:guide.bottomAnchor] |
||||
|
]]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)play { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
- (void)pause { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
- (void)updateWithLang:(NSString *)lang cn:(NSString *)cn en:(NSString *)en { |
||||
|
if (lang != nil) { |
||||
|
self.langLabel.text = lang; |
||||
|
} |
||||
|
NSMutableParagraphStyle *para = [[NSMutableParagraphStyle alloc] init]; |
||||
|
para.lineSpacing = 4; |
||||
|
if (cn != nil) { |
||||
|
self.cnLabel.attributedText = [[NSAttributedString alloc] initWithString:cn attributes:@{NSParagraphStyleAttributeName: para}]; |
||||
|
} |
||||
|
if (en != nil) { |
||||
|
self.enLabel.attributedText = [[NSAttributedString alloc] initWithString:en attributes:@{NSParagraphStyleAttributeName: para}]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,14 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict> |
||||
|
<key>NSPrivacyTrackingDomains</key> |
||||
|
<array/> |
||||
|
<key>NSPrivacyAccessedAPITypes</key> |
||||
|
<array/> |
||||
|
<key>NSPrivacyCollectedDataTypes</key> |
||||
|
<array/> |
||||
|
<key>NSPrivacyTracking</key> |
||||
|
<false/> |
||||
|
</dict> |
||||
|
</plist> |
||||
@ -0,0 +1,29 @@ |
|||||
|
# |
||||
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
||||
|
# Run `pod lib lint native_plugin.podspec` to validate before publishing. |
||||
|
# |
||||
|
Pod::Spec.new do |s| |
||||
|
s.name = 'native_plugin' |
||||
|
s.version = '0.0.1' |
||||
|
s.summary = 'A new Flutter plugin project.' |
||||
|
s.description = <<-DESC |
||||
|
A new Flutter plugin project. |
||||
|
DESC |
||||
|
s.homepage = 'http://example.com' |
||||
|
s.license = { :file => '../LICENSE' } |
||||
|
s.author = { 'Your Company' => 'email@example.com' } |
||||
|
s.source = { :path => '.' } |
||||
|
s.source_files = 'Classes/**/*' |
||||
|
s.public_header_files = 'Classes/**/*.h' |
||||
|
s.dependency 'Flutter' |
||||
|
s.platform = :ios, '12.0' |
||||
|
|
||||
|
# Flutter.framework does not contain a i386 slice. |
||||
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } |
||||
|
|
||||
|
# If your plugin requires a privacy manifest, for example if it uses any |
||||
|
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your |
||||
|
# plugin's privacy impact, and then uncomment this line. For more information, |
||||
|
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files |
||||
|
# s.resource_bundles = {'native_plugin_privacy' => ['Resources/PrivacyInfo.xcprivacy']} |
||||
|
end |
||||
@ -0,0 +1,19 @@ |
|||||
|
import 'native_plugin_platform_interface.dart'; |
||||
|
|
||||
|
class NativePlugin { |
||||
|
Future<String?> getPlatformVersion() { |
||||
|
return NativePluginPlatform.instance.getPlatformVersion(); |
||||
|
} |
||||
|
|
||||
|
Future<int> createPipContentView() { |
||||
|
return NativePluginPlatform.instance.createPipContentView(); |
||||
|
} |
||||
|
|
||||
|
Future<void> updatePipContentView(Map data) { |
||||
|
return NativePluginPlatform.instance.updatePipContentView(data); |
||||
|
} |
||||
|
|
||||
|
Future<void> disposePipContentView(int viewId) { |
||||
|
return NativePluginPlatform.instance.disposePipContentView(viewId); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
import 'package:flutter/foundation.dart'; |
||||
|
import 'package:flutter/services.dart'; |
||||
|
|
||||
|
import 'native_plugin_platform_interface.dart'; |
||||
|
|
||||
|
/// An implementation of [NativePluginPlatform] that uses method channels. |
||||
|
class MethodChannelNativePlugin extends NativePluginPlatform { |
||||
|
/// The method channel used to interact with the native platform. |
||||
|
@visibleForTesting |
||||
|
final methodChannel = const MethodChannel('native_plugin'); |
||||
|
|
||||
|
@override |
||||
|
Future<String?> getPlatformVersion() async { |
||||
|
final version = await methodChannel.invokeMethod<String>( |
||||
|
'getPlatformVersion', |
||||
|
); |
||||
|
return version; |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Future<int> createPipContentView() async { |
||||
|
final viewId = await methodChannel.invokeMethod<int>( |
||||
|
'createPipContentView', |
||||
|
); |
||||
|
return viewId ?? 0; |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Future<void> updatePipContentView(Map data) async { |
||||
|
await methodChannel.invokeMethod<void>('updatePipContentView', data); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Future<void> disposePipContentView(int viewId) async { |
||||
|
await methodChannel.invokeMethod<void>('disposePipContentView', viewId); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; |
||||
|
|
||||
|
import 'native_plugin_method_channel.dart'; |
||||
|
|
||||
|
abstract class NativePluginPlatform extends PlatformInterface { |
||||
|
/// Constructs a NativePluginPlatform. |
||||
|
NativePluginPlatform() : super(token: _token); |
||||
|
|
||||
|
static final Object _token = Object(); |
||||
|
|
||||
|
static NativePluginPlatform _instance = MethodChannelNativePlugin(); |
||||
|
|
||||
|
/// The default instance of [NativePluginPlatform] to use. |
||||
|
/// |
||||
|
/// Defaults to [MethodChannelNativePlugin]. |
||||
|
static NativePluginPlatform get instance => _instance; |
||||
|
|
||||
|
/// Platform-specific implementations should set this with their own |
||||
|
/// platform-specific class that extends [NativePluginPlatform] when |
||||
|
/// they register themselves. |
||||
|
static set instance(NativePluginPlatform instance) { |
||||
|
PlatformInterface.verifyToken(instance, _token); |
||||
|
_instance = instance; |
||||
|
} |
||||
|
|
||||
|
Future<String?> getPlatformVersion() { |
||||
|
throw UnimplementedError('platformVersion() has not been implemented.'); |
||||
|
} |
||||
|
|
||||
|
Future<int> createPipContentView() { |
||||
|
throw UnimplementedError( |
||||
|
'createPipContentView() has not been implemented.', |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Future<void> updatePipContentView(Map data) { |
||||
|
throw UnimplementedError( |
||||
|
'updatePipContentView() has not been implemented.', |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Future<void> disposePipContentView(int viewId) { |
||||
|
throw UnimplementedError( |
||||
|
'disposePipContentView() has not been implemented.', |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
name: native_plugin |
||||
|
description: "A new Flutter plugin project." |
||||
|
version: 0.0.1 |
||||
|
homepage: |
||||
|
|
||||
|
environment: |
||||
|
sdk: ^3.7.0 |
||||
|
flutter: '>=3.3.0' |
||||
|
|
||||
|
dependencies: |
||||
|
flutter: |
||||
|
sdk: flutter |
||||
|
plugin_platform_interface: ^2.0.2 |
||||
|
|
||||
|
dev_dependencies: |
||||
|
flutter_test: |
||||
|
sdk: flutter |
||||
|
flutter_lints: ^5.0.0 |
||||
|
|
||||
|
# For information on the generic Dart part of this file, see the |
||||
|
# following page: https://dart.dev/tools/pub/pubspec |
||||
|
|
||||
|
# The following section is specific to Flutter packages. |
||||
|
flutter: |
||||
|
# This section identifies this Flutter project as a plugin project. |
||||
|
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) |
||||
|
# which should be registered in the plugin registry. This is required for |
||||
|
# using method channels. |
||||
|
# The Android 'package' specifies package in which the registered class is. |
||||
|
# This is required for using method channels on Android. |
||||
|
# The 'ffiPlugin' specifies that native code should be built and bundled. |
||||
|
# This is required for using `dart:ffi`. |
||||
|
# All these are used by the tooling to maintain consistency when |
||||
|
# adding or updating assets for this project. |
||||
|
plugin: |
||||
|
platforms: |
||||
|
android: |
||||
|
package: com.example.native_plugin |
||||
|
pluginClass: NativePlugin |
||||
|
ios: |
||||
|
pluginClass: NativePlugin |
||||
|
|
||||
|
# To add assets to your plugin package, add an assets section, like this: |
||||
|
# assets: |
||||
|
# - images/a_dot_burr.jpeg |
||||
|
# - images/a_dot_ham.jpeg |
||||
|
# |
||||
|
# For details regarding assets in packages, see |
||||
|
# https://flutter.dev/to/asset-from-package |
||||
|
# |
||||
|
# An image asset can refer to one or more resolution-specific "variants", see |
||||
|
# https://flutter.dev/to/resolution-aware-images |
||||
|
|
||||
|
# To add custom fonts to your plugin package, add a fonts section here, |
||||
|
# in this "flutter" section. Each entry in this list should have a |
||||
|
# "family" key with the font family name, and a "fonts" key with a |
||||
|
# list giving the asset and other descriptors for the font. For |
||||
|
# example: |
||||
|
# fonts: |
||||
|
# - family: Schyler |
||||
|
# fonts: |
||||
|
# - asset: fonts/Schyler-Regular.ttf |
||||
|
# - asset: fonts/Schyler-Italic.ttf |
||||
|
# style: italic |
||||
|
# - family: Trajan Pro |
||||
|
# fonts: |
||||
|
# - asset: fonts/TrajanPro.ttf |
||||
|
# - asset: fonts/TrajanPro_Bold.ttf |
||||
|
# weight: 700 |
||||
|
# |
||||
|
# For details regarding fonts in packages, see |
||||
|
# https://flutter.dev/to/font-from-package |
||||
Loading…
Reference in new issue