You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
517 lines
15 KiB
517 lines
15 KiB
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:just_audio/just_audio.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
|
|
import '../../../core/utils/logger.dart';
|
|
import '../../../core/utils/synchrodata.dart';
|
|
import '../../../core/utils/upload_oss.dart';
|
|
import '../../../data/services/ble_manager.dart';
|
|
import '../../../data/services/db/sqflite_api.dart';
|
|
import '../../../core/utils/permission_util.dart';
|
|
|
|
import 'package:share_plus/share_plus.dart';
|
|
|
|
import '../../../routes/app_routes.dart';
|
|
|
|
class MeetingController extends GetxController {
|
|
final ScrollController listController = ScrollController();
|
|
final AudioPlayer _audioPlayer = AudioPlayer();
|
|
|
|
// 是否还有更多数据
|
|
bool isGetMore = true;
|
|
bool isLoading = false;
|
|
int page = 1;
|
|
int addQuantity = 0;
|
|
List<String> selected = [];
|
|
|
|
final appDocDir = getApplicationDocumentsDirectory();
|
|
List<FileSystemEntity> files = [];
|
|
TextEditingController titleController = TextEditingController(); // 录音标题
|
|
String titleCSuffix = ""; // 录音标题后缀
|
|
RxList dataList = [].obs;
|
|
// 蓝牙服务
|
|
final bleManager = Get.find<BleManager>();
|
|
List dataListTask = [];
|
|
bool isDisposed = false;
|
|
String filePath = '';
|
|
|
|
final String _appid = '2316081933';
|
|
final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En';
|
|
final String _cluster = 'volc_auc_common_flash';
|
|
final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc';
|
|
Map<String, String> _headers = {};
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
listController.addListener(_onScroll);
|
|
_requestStoragePermission();
|
|
_headers = {
|
|
'Authorization': 'Bearer; $_token',
|
|
'Content-Type': 'application/json',
|
|
};
|
|
_initGetDataList();
|
|
addMeetingAudio();
|
|
final Map<String, dynamic> args = Get.arguments ?? {};
|
|
// 检查参数是否存在且包含有效路径
|
|
if (args.isNotEmpty &&
|
|
args.containsKey('filePath') &&
|
|
args['filePath'] is String &&
|
|
args['filePath'].isNotEmpty) {
|
|
final String path = args['filePath'];
|
|
addAudio(path);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
listController.dispose();
|
|
titleController.dispose();
|
|
_audioPlayer.dispose();
|
|
isDisposed = true;
|
|
super.onClose();
|
|
}
|
|
|
|
void onPopInvokedWithResult(didPop, result) {
|
|
Synchrodata.uploadOperationRecords(isSynchro: true);
|
|
}
|
|
|
|
void _onScroll() {
|
|
if (isGetMore &&
|
|
!isLoading &&
|
|
listController.position.pixels >=
|
|
listController.position.maxScrollExtent - 100) {
|
|
isLoading = true;
|
|
page++;
|
|
_getDataList();
|
|
}
|
|
}
|
|
|
|
void _initGetDataList() async {
|
|
await Synchrodata.uploadOperationRecords(isSynchro: true);
|
|
_getDataList();
|
|
}
|
|
|
|
// 请求本地数据
|
|
void _getDataList() async {
|
|
List list = await SqfliteApi.getMeetingList((page - 1) * 10 + addQuantity);
|
|
dataList.addAll(list);
|
|
queryDataListTask(list);
|
|
addQuantity = 0;
|
|
if (list.length < 10) {
|
|
isGetMore = false;
|
|
} else {
|
|
isGetMore = true;
|
|
}
|
|
isLoading = false;
|
|
}
|
|
|
|
// 请求存储权限
|
|
void _requestStoragePermission() async {
|
|
PermissionUtil.instance
|
|
.requestPermission(
|
|
permissionType: Permission.storage,
|
|
permissionName: "存储",
|
|
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。",
|
|
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。",
|
|
)
|
|
.then((granted) {
|
|
if (granted) {
|
|
Logger.d("Permission", "用户授予了存储权限");
|
|
} else {
|
|
Logger.d("Permission", "用户未授予存储权限");
|
|
}
|
|
});
|
|
}
|
|
|
|
void loadTranslatopus() async {
|
|
final dir =
|
|
Directory("/data/user/0/com.yunqiinnovation.deepsound/files/OpusAudio");
|
|
|
|
files = dir
|
|
.listSync()
|
|
.where((f) => f is File && (f.path.endsWith('.opus')))
|
|
.toList();
|
|
}
|
|
|
|
void loadTranslatAudio() async {
|
|
try {
|
|
final appDir = await appDocDir; // 先 await 获取目录
|
|
final dir = Directory("${appDir.path}/TranslatAudio");
|
|
|
|
if (!await dir.exists()) {
|
|
await dir.create(recursive: true);
|
|
files = [];
|
|
return;
|
|
}
|
|
|
|
files = dir
|
|
.listSync()
|
|
.where((f) =>
|
|
f is File &&
|
|
(f.path.endsWith('.mp3') ||
|
|
f.path.endsWith('.wav') ||
|
|
f.path.endsWith('.aac') ||
|
|
f.path.endsWith('.m4a') ||
|
|
f.path.endsWith('.mp4') ||
|
|
f.path.endsWith('.mov')))
|
|
.toList();
|
|
} catch (e) {
|
|
files = [];
|
|
}
|
|
}
|
|
|
|
Future<void> shareAllTranslatAudio() async {
|
|
final filePath = selected[0];
|
|
try {
|
|
await Share.shareXFiles(
|
|
[XFile(filePath)],
|
|
text: '分享会议音频',
|
|
);
|
|
|
|
EasyLoading.showToast('分享完成!');
|
|
} catch (e) {
|
|
EasyLoading.showToast('分享失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
void addMeetingAudio() async {
|
|
EasyLoading.show(status: '正在导入本地录音...');
|
|
try {
|
|
int addUp = 0;
|
|
final appDir = await appDocDir;
|
|
|
|
final sourceDirs = [
|
|
Directory("${appDir.path}/MeetingAudio/"),
|
|
Directory("${appDir.path}/DestAudio/"),
|
|
];
|
|
|
|
final destDir = Directory("${appDir.path}/LocalAudio/");
|
|
if (!await destDir.exists()) {
|
|
await destDir.create(recursive: true);
|
|
}
|
|
|
|
List<FileSystemEntity> allFiles = [];
|
|
|
|
for (final dir in sourceDirs) {
|
|
if (await dir.exists()) {
|
|
final files =
|
|
await dir.list().where((entity) => entity is File).toList();
|
|
allFiles.addAll(files);
|
|
}
|
|
}
|
|
|
|
for (final fileEntity in allFiles) {
|
|
try {
|
|
final file = File(fileEntity.path);
|
|
final filePath = file.path;
|
|
|
|
// 设置音频源获取时长
|
|
|
|
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
|
|
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
|
|
|
|
// 获取文件修改时间
|
|
final fileStat = await file.stat();
|
|
final creationTime = fileStat.changed.millisecondsSinceEpoch;
|
|
|
|
// 上传OSS
|
|
final audiourl =
|
|
await UploadOss.upload(filepath: filePath, rootDir: 'LocalAudio');
|
|
|
|
// 准备新路径
|
|
final fileName = filePath.split('/').last;
|
|
final newPath = "${destDir.path}/$fileName";
|
|
|
|
// 构建数据
|
|
Map<String, Object?> data = {
|
|
'title': fileName,
|
|
'type': 'LOCAL',
|
|
'seconds': seconds,
|
|
'filepath': newPath, // 存储新路径
|
|
'audiourl': audiourl,
|
|
'tasktype': 0,
|
|
'creationtime': creationTime,
|
|
};
|
|
|
|
// 插入数据库
|
|
final meetingId = await SqfliteApi.insertMeeting(data);
|
|
if (meetingId != null) {
|
|
// 更新UI数据
|
|
dataList.insert(0, {
|
|
'id': meetingId,
|
|
'taskid': '',
|
|
...data,
|
|
});
|
|
|
|
// 移动文件
|
|
await file.copy(newPath);
|
|
await file.delete();
|
|
|
|
addQuantity++;
|
|
addUp++;
|
|
}
|
|
} catch (e) {
|
|
print("文件处理失败: ${fileEntity.path}, 错误: $e");
|
|
}
|
|
}
|
|
if (addUp > 0) EasyLoading.showSuccess('成功导入本地 $addUp 条录音');
|
|
} catch (e) {
|
|
EasyLoading.showError('导入失败: $e');
|
|
} finally {
|
|
EasyLoading.dismiss();
|
|
}
|
|
}
|
|
|
|
void addTranslatAudio() async {
|
|
EasyLoading.show(status: '正在批量导入...');
|
|
for (final filePath in selected) {
|
|
try {
|
|
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
|
|
final fileStat = await File(filePath).stat();
|
|
final creationTime = fileStat.changed.millisecondsSinceEpoch; // 时间戳
|
|
var audiourl = await UploadOss.upload(
|
|
filepath: filePath, rootDir: 'TranslatAudio');
|
|
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
|
|
final fileName = filePath.split('/').last;
|
|
Map<String, Object?> data = {
|
|
'title': fileName,
|
|
'type': 'TRANSLAT',
|
|
'seconds': seconds,
|
|
'filepath': filePath,
|
|
'audiourl': audiourl,
|
|
'tasktype': 0,
|
|
'creationtime': creationTime,
|
|
};
|
|
final meetingId = await SqfliteApi.insertMeeting(data);
|
|
if (meetingId != null) {
|
|
dataList.insert(0, {
|
|
'id': meetingId,
|
|
'taskid': '',
|
|
...data,
|
|
});
|
|
addQuantity++;
|
|
}
|
|
} catch (e) {
|
|
Logger.error('导入文件失败: $filePath, error: $e');
|
|
}
|
|
}
|
|
EasyLoading.dismiss();
|
|
}
|
|
|
|
void addAudio([String? filePath]) async {
|
|
EasyLoading.show(status: '正在导入...');
|
|
String fileName = "";
|
|
bool isParamProvided = filePath != null; // 更清晰的变量名
|
|
if (filePath != null) {
|
|
// 正确截取文件名(跨平台安全方式)
|
|
fileName = path.basename(filePath);
|
|
} else {
|
|
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
|
withData: true,
|
|
type: FileType.audio,
|
|
);
|
|
if (result != null) {
|
|
PlatformFile file = result.files.first;
|
|
filePath = await _saveFile(file);
|
|
fileName = file.name;
|
|
} else {
|
|
EasyLoading.dismiss();
|
|
return;
|
|
}
|
|
}
|
|
|
|
var audiourl =
|
|
await UploadOss.upload(filepath: filePath, rootDir: 'ExternalAudio');
|
|
final meetingId = await insertMeetingAudio(fileName, filePath, audiourl);
|
|
|
|
if (isParamProvided) {
|
|
Get.toNamed(Routes.meetingDetails, arguments: {
|
|
'id': meetingId,
|
|
});
|
|
}
|
|
EasyLoading.dismiss();
|
|
}
|
|
|
|
Future insertMeetingAudio(
|
|
String fileName, String filePath, String audiourl) async {
|
|
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
|
|
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
|
|
int creationTime = DateTime.now().millisecondsSinceEpoch;
|
|
Map<String, Object?> data = {
|
|
'title': fileName,
|
|
'type': 'IMPORT',
|
|
'seconds': seconds,
|
|
'filepath': filePath,
|
|
'audiourl': audiourl,
|
|
'tasktype': 0,
|
|
'creationtime': creationTime,
|
|
};
|
|
final meetingId = await SqfliteApi.insertMeeting(data);
|
|
if (meetingId != null) {
|
|
dataList.insert(0, {
|
|
'id': meetingId,
|
|
'taskid': '',
|
|
...data,
|
|
});
|
|
addQuantity++;
|
|
}
|
|
return meetingId;
|
|
}
|
|
|
|
Future<String> _saveFile(PlatformFile file) async {
|
|
final directory = await getApplicationDocumentsDirectory();
|
|
final directoryDir = Directory('${directory.path}/ExternalAudio');
|
|
if (!await directoryDir.exists()) {
|
|
await directoryDir.create(recursive: true);
|
|
}
|
|
final String filePath = '${directory.path}/ExternalAudio/${file.name}';
|
|
final File localFile = File(filePath);
|
|
await localFile.writeAsBytes(file.bytes!);
|
|
return filePath;
|
|
}
|
|
|
|
// 修改标题
|
|
void editMeetingTitle(int id, String title) async {
|
|
if (title.isNotEmpty) {
|
|
SqfliteApi.editMeetingTitle(id, {'title': title});
|
|
int index = dataList.indexWhere((item) => item['id'] == id);
|
|
if (index != -1) {
|
|
final dataMap = Map<String, dynamic>.from(dataList[index]);
|
|
dataMap['title'] = title;
|
|
dataList[index] = dataMap;
|
|
}
|
|
}
|
|
Get.back();
|
|
}
|
|
|
|
// 删除数据
|
|
void deleteMeeting(int id, String filepath) async {
|
|
dataList.removeWhere((item) => item['id'] == id);
|
|
// 删除数据库中的记录
|
|
SqfliteApi.deleteMeeting(id);
|
|
_deleteFile(filepath);
|
|
addQuantity--;
|
|
}
|
|
|
|
// 删除文件
|
|
Future<void> _deleteFile(String filepath) async {
|
|
File file = File(filepath);
|
|
bool fileExists = await file.exists();
|
|
if (fileExists) {
|
|
await file.delete();
|
|
}
|
|
}
|
|
|
|
// 查询转写状态
|
|
void queryDataListTask(List list, {bool isAdd = true}) {
|
|
for (var element in List.from(list)) {
|
|
if (element['tasktype'] == 1) {
|
|
if (isAdd) {
|
|
dataListTask.add(element);
|
|
}
|
|
final taskId = element['taskid'];
|
|
_fileRecognize(element['id'], taskId);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _fileRecognize(int id, String taskId) async {
|
|
try {
|
|
final startTime = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
if (isDisposed) return;
|
|
var respDic = await _queryTask(taskId);
|
|
var code = respDic['resp']['code'] as int;
|
|
|
|
if (code == 1000) {
|
|
await SqfliteApi.editMeetingTitle(id, {'tasktype': 2});
|
|
int index = dataList.indexWhere((item) => item['id'] == id);
|
|
if (index != -1) {
|
|
final dataMap = Map<String, dynamic>.from(dataList[index]);
|
|
dataMap['tasktype'] = 2;
|
|
dataList[index] = dataMap;
|
|
int taskIndex = dataListTask.indexWhere((item) => item['id'] == id);
|
|
if (taskIndex != -1) {
|
|
dataListTask.removeAt(taskIndex);
|
|
}
|
|
}
|
|
return;
|
|
} else if (code < 2000) {
|
|
Logger.debug("FileRecognize failed");
|
|
return;
|
|
}
|
|
|
|
while (!isDisposed) {
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
|
|
if (isDisposed) return;
|
|
|
|
respDic = await _queryTask(taskId);
|
|
code = respDic['resp']['code'] as int;
|
|
|
|
if (code == 1000) {
|
|
await SqfliteApi.editMeetingTitle(id, {'tasktype': 2});
|
|
int index = dataList.indexWhere((item) => item['id'] == id);
|
|
if (index != -1) {
|
|
final dataMap = Map<String, dynamic>.from(dataList[index]);
|
|
dataMap['tasktype'] = 2;
|
|
dataList[index] = dataMap;
|
|
int taskIndex = dataListTask.indexWhere((item) => item['id'] == id);
|
|
if (taskIndex != -1) {
|
|
dataListTask.removeAt(taskIndex);
|
|
}
|
|
}
|
|
return;
|
|
} else if (code < 2000) {
|
|
Logger.debug("FileRecognize failed");
|
|
return;
|
|
}
|
|
|
|
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
|
if (currentTime - startTime > 300 * 1000) {
|
|
Logger.debug('wait time exceeds 300s');
|
|
return;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Logger.error('Error occurred: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
Future<Map<String, dynamic>> _queryTask(String taskId) async {
|
|
final queryBody = jsonEncode({
|
|
"appid": _appid,
|
|
"token": _token,
|
|
"id": taskId,
|
|
"cluster": _cluster,
|
|
});
|
|
|
|
try {
|
|
final response = await http.post(
|
|
Uri.parse('$_serviceUrl/query'),
|
|
headers: _headers,
|
|
body: queryBody,
|
|
);
|
|
|
|
if (response.statusCode != 200) {
|
|
throw Exception('Query failed with status: ${response.statusCode}');
|
|
}
|
|
|
|
final respDic = jsonDecode(response.body);
|
|
return respDic;
|
|
} catch (e) {
|
|
Logger.error('Query error: ${e.toString()}');
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|
|
|