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