67 changed files with 2908 additions and 1175 deletions
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> bgBG = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Добре дошли', |
||||
|
// 补充保加利亚语翻译... |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> csCZ = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Vítejte', |
||||
|
// 补充捷克语翻译... |
||||
|
}; |
||||
@ -0,0 +1,4 @@ |
|||||
|
const Map<String, String> daDK = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
// 添加丹麦语翻译内容 |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> elGR = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Καλώς ορίσατε', |
||||
|
// 补充希腊语翻译... |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> etEE = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Tere tulemast', |
||||
|
// 补充爱沙尼亚语翻译... |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> fiFI = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Tervetuloa', |
||||
|
// 补充芬兰语翻译... |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> hrHR = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Dobrodošli', |
||||
|
// 补充克罗地亚语翻译... |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> huHU = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Üdvözlet', |
||||
|
// 补充匈牙利语翻译... |
||||
|
}; |
||||
@ -0,0 +1,12 @@ |
|||||
|
Map<String, String> isIS = { |
||||
|
'appTitle': 'Deep Voice', |
||||
|
'settings': 'Stillingar', |
||||
|
'language': 'Tungumál', |
||||
|
'theme': 'Þema', |
||||
|
'darkMode': 'Dökkur hamur', |
||||
|
'lightMode': 'Ljós hamur', |
||||
|
'systemMode': 'Kerfishamur', |
||||
|
'about': 'Um appið', |
||||
|
'version': 'Útgáfa', |
||||
|
// 添加更多翻译键值对 |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> ltLT = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Sveikas', |
||||
|
// 补充立陶宛语翻译... |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> lvLV = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Sveiki', |
||||
|
// 补充拉脱维亚语翻译... |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> nbNO = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Velkommen', |
||||
|
// 补充挪威语翻译... |
||||
|
}; |
||||
@ -0,0 +1,4 @@ |
|||||
|
const Map<String, String> nlNL = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
// 添加荷兰语翻译内容 |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> roRO = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Bine ați venit', |
||||
|
// 补充罗马尼亚语翻译... |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> skSK = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Vitajte', |
||||
|
// 补充斯洛伐克语翻译... |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> slSI = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Dobrodošli', |
||||
|
//补充斯洛文尼亚语翻译 |
||||
|
}; |
||||
@ -0,0 +1,12 @@ |
|||||
|
Map<String, String> srRS = { |
||||
|
'appTitle': 'Deep Voice', |
||||
|
'settings': 'Podešavanja', |
||||
|
'language': 'Jezik', |
||||
|
'theme': 'Tema', |
||||
|
'darkMode': 'Tamni režim', |
||||
|
'lightMode': 'Svetli režim', |
||||
|
'systemMode': 'Sistemski režim', |
||||
|
'about': 'O aplikaciji', |
||||
|
'version': 'Verzija', |
||||
|
// 添加更多翻译键值对 |
||||
|
}; |
||||
@ -0,0 +1,4 @@ |
|||||
|
const Map<String, String> svSE = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
// 添加瑞典语翻译内容 |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> tlPH = { |
||||
|
'welcome': 'Maligayang pagdating', |
||||
|
'loginTips': 'Mangyaring mag-log in o gumawa ng account...', |
||||
|
// 其他翻译条目... |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
const Map<String, String> ukUA = { |
||||
|
'app_title': 'Deep Voice', |
||||
|
'welcome': 'Ласкаво просимо', |
||||
|
// 补充乌克兰语翻译... |
||||
|
}; |
||||
@ -0,0 +1,104 @@ |
|||||
|
import 'dart:ui'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
|
||||
|
/// 使用Shader实现的麦克风流动动画组件 |
||||
|
class ShaderMicFlow extends StatefulWidget { |
||||
|
final double size; |
||||
|
final double amplitude; |
||||
|
|
||||
|
const ShaderMicFlow({ |
||||
|
super.key, |
||||
|
required this.size, |
||||
|
this.amplitude = 0.0, |
||||
|
}); |
||||
|
|
||||
|
@override |
||||
|
State<ShaderMicFlow> createState() => _ShaderMicFlowState(); |
||||
|
} |
||||
|
|
||||
|
class _ShaderMicFlowState extends State<ShaderMicFlow> |
||||
|
with SingleTickerProviderStateMixin { |
||||
|
late AnimationController _controller; |
||||
|
FragmentShader? _shader; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
_controller = AnimationController( |
||||
|
duration: const Duration(seconds: 60), |
||||
|
vsync: this, |
||||
|
)..repeat(); |
||||
|
|
||||
|
_loadShader(); |
||||
|
} |
||||
|
|
||||
|
Future<void> _loadShader() async { |
||||
|
try { |
||||
|
final program = await FragmentProgram.fromAsset('shaders/mic_flow.frag'); |
||||
|
setState(() { |
||||
|
_shader = program.fragmentShader(); |
||||
|
}); |
||||
|
} catch (e) { |
||||
|
debugPrint('Failed to load shader: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void dispose() { |
||||
|
_controller.dispose(); |
||||
|
_shader?.dispose(); |
||||
|
super.dispose(); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return SizedBox( |
||||
|
width: widget.size, |
||||
|
height: widget.size, |
||||
|
child: AnimatedBuilder( |
||||
|
animation: _controller, |
||||
|
builder: (context, child) { |
||||
|
return CustomPaint( |
||||
|
painter: _shader != null |
||||
|
? _ShaderPainter( |
||||
|
shader: _shader!, |
||||
|
time: _controller.value * 60.0, |
||||
|
amplitude: widget.amplitude, |
||||
|
) |
||||
|
: null, |
||||
|
size: Size.square(widget.size), |
||||
|
); |
||||
|
}, |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class _ShaderPainter extends CustomPainter { |
||||
|
final FragmentShader shader; |
||||
|
final double time; |
||||
|
final double amplitude; |
||||
|
|
||||
|
_ShaderPainter({ |
||||
|
required this.shader, |
||||
|
required this.time, |
||||
|
required this.amplitude, |
||||
|
}); |
||||
|
|
||||
|
@override |
||||
|
void paint(Canvas canvas, Size size) { |
||||
|
// 设置shader uniforms |
||||
|
shader.setFloat(0, size.width); // u_resolution.x |
||||
|
shader.setFloat(1, size.height); // u_resolution.y |
||||
|
shader.setFloat(2, time); // u_time |
||||
|
shader.setFloat(3, amplitude); // u_amp |
||||
|
|
||||
|
final paint = Paint()..shader = shader; |
||||
|
canvas.drawRect(Offset.zero & size, paint); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
bool shouldRepaint(covariant _ShaderPainter oldDelegate) { |
||||
|
return oldDelegate.time != time || oldDelegate.amplitude != amplitude; |
||||
|
} |
||||
|
} |
||||
@ -1,89 +0,0 @@ |
|||||
import 'dart:ui'; |
|
||||
import 'package:flutter/material.dart'; |
|
||||
import 'package:flutter_shaders/flutter_shaders.dart'; |
|
||||
|
|
||||
class VoiceMicFlow extends StatefulWidget { |
|
||||
const VoiceMicFlow({super.key, this.size = 280, this.amp = 0.0}); |
|
||||
final double size; |
|
||||
final double amp; // 音量振幅 0.0-1.0 |
|
||||
|
|
||||
@override |
|
||||
State<VoiceMicFlow> createState() => _VoiceMicFlowState(); |
|
||||
} |
|
||||
|
|
||||
class _VoiceMicFlowState extends State<VoiceMicFlow> |
|
||||
with SingleTickerProviderStateMixin { |
|
||||
late final AnimationController _ctl; |
|
||||
|
|
||||
@override |
|
||||
void initState() { |
|
||||
super.initState(); |
|
||||
_ctl = AnimationController( |
|
||||
duration: const Duration(seconds: 100), // 100秒的长周期 |
|
||||
vsync: this, |
|
||||
)..repeat(); |
|
||||
} |
|
||||
|
|
||||
@override |
|
||||
void dispose() { |
|
||||
_ctl.dispose(); |
|
||||
super.dispose(); |
|
||||
} |
|
||||
|
|
||||
@override |
|
||||
Widget build(BuildContext context) { |
|
||||
return ClipOval( |
|
||||
child: ShaderBuilder( |
|
||||
assetKey: 'shaders/mic_flow.frag', |
|
||||
(context, shader, child) { |
|
||||
return AnimatedBuilder( |
|
||||
animation: _ctl, |
|
||||
builder: (_, __) { |
|
||||
// 将0.0-1.0的动画值映射到更大的时间范围 |
|
||||
final time = _ctl.value * 100.0; |
|
||||
shader |
|
||||
..setFloat(0, widget.size) // u_resolution.x |
|
||||
..setFloat(1, widget.size) // u_resolution.y |
|
||||
..setFloat(2, time) // u_time |
|
||||
..setFloat(3, widget.amp); // u_amp (音量振幅) |
|
||||
return CustomPaint( |
|
||||
size: Size.square(widget.size), |
|
||||
painter: _ShaderPainter(shader), |
|
||||
); |
|
||||
}, |
|
||||
); |
|
||||
}, |
|
||||
// 着色器编译时的占位符 |
|
||||
child: Container( |
|
||||
width: widget.size, |
|
||||
height: widget.size, |
|
||||
decoration: const BoxDecoration( |
|
||||
shape: BoxShape.circle, |
|
||||
gradient: LinearGradient( |
|
||||
begin: Alignment.topLeft, |
|
||||
end: Alignment.bottomRight, |
|
||||
colors: [ |
|
||||
Color(0xFF1565C0), // 深蓝 |
|
||||
Color(0xFFE3F2FD), // 浅蓝 |
|
||||
], |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
), |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
class _ShaderPainter extends CustomPainter { |
|
||||
_ShaderPainter(this.shader); |
|
||||
final FragmentShader shader; |
|
||||
|
|
||||
@override |
|
||||
void paint(Canvas canvas, Size size) { |
|
||||
final paint = Paint()..shader = shader; |
|
||||
canvas.drawRect(Offset.zero & size, paint); |
|
||||
} |
|
||||
|
|
||||
@override |
|
||||
bool shouldRepaint(covariant _ShaderPainter oldDelegate) => true; |
|
||||
} |
|
||||
@ -0,0 +1,254 @@ |
|||||
|
package com.yunqiinnovation.azure_speech.tools |
||||
|
|
||||
|
import java.io.File |
||||
|
import java.io.FileOutputStream |
||||
|
import java.io.RandomAccessFile |
||||
|
|
||||
|
import android.util.Log |
||||
|
import java.io.FileInputStream |
||||
|
import java.util.concurrent.LinkedBlockingQueue |
||||
|
import java.util.concurrent.atomic.AtomicBoolean |
||||
|
|
||||
|
object RecordFile { |
||||
|
private var currentAudioFile: File? = null |
||||
|
private var fos: FileOutputStream? = null |
||||
|
|
||||
|
// 新增:用于异步写入的队列和线程 |
||||
|
private val writeQueue = LinkedBlockingQueue<ByteArray>() |
||||
|
private val isWriting = AtomicBoolean(false) |
||||
|
private var writeThread: Thread? = null |
||||
|
|
||||
|
// 用于存储音频数据的缓冲区 |
||||
|
private val dataBuffer = mutableListOf<ByteArray>() |
||||
|
private var totalBytesWritten = 0 |
||||
|
|
||||
|
// 音频配置 |
||||
|
private val sampleRate = 16000 |
||||
|
|
||||
|
// 新增:用于存储最后成功保存的文件 |
||||
|
private var lastSavedFile: File? = null |
||||
|
private var lastFileTimestamp: String = "" |
||||
|
var isPause = false |
||||
|
var fileName = "" |
||||
|
|
||||
|
|
||||
|
// 修改:添加 filePath 参数 |
||||
|
internal fun creatingFiles(filePath: String) { |
||||
|
if (fos != null || currentAudioFile != null) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
val file = File(filePath) |
||||
|
val parentDir = file.parentFile |
||||
|
if (!parentDir.exists()) { |
||||
|
parentDir.mkdirs() |
||||
|
} |
||||
|
|
||||
|
currentAudioFile = file |
||||
|
currentAudioFile?.createNewFile() |
||||
|
|
||||
|
fos = FileOutputStream(currentAudioFile).apply { |
||||
|
write(generateWavHeader(0)) |
||||
|
close() |
||||
|
} |
||||
|
|
||||
|
fos = FileOutputStream(currentAudioFile, true) |
||||
|
startWriteThread() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// 新增:启动写入线程 |
||||
|
private fun startWriteThread() { |
||||
|
if (isWriting.get()) return |
||||
|
isWriting.set(true) |
||||
|
writeThread = Thread { |
||||
|
try { |
||||
|
while (isWriting.get() || writeQueue.isNotEmpty()) { |
||||
|
val data = writeQueue.poll() ?: continue |
||||
|
fos?.write(data) |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
Log.e("tag", "异步写入音频数据失败: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
writeThread?.start() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 保存音频数据到 WAV 文件(异步) |
||||
|
*/ |
||||
|
internal fun saveAudioDataToWav(buffer: ByteArray) { |
||||
|
if (fos == null || currentAudioFile == null) return |
||||
|
// 放入队列,由写线程写入 |
||||
|
writeQueue.offer(buffer.copyOf()) |
||||
|
totalBytesWritten += buffer.size |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// 更新文件头生成(修正RIFF长度计算) |
||||
|
private fun generateWavHeader(dataLength: Int): ByteArray { |
||||
|
val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 |
||||
|
val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数 |
||||
|
|
||||
|
return byteArrayOf( |
||||
|
'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(), |
||||
|
(totalLength and 0xFF).toByte(), ((totalLength shr 8) and 0xFF).toByte(), |
||||
|
((totalLength shr 16) and 0xFF).toByte(), ((totalLength shr 24) and 0xFF).toByte(), |
||||
|
'W'.code.toByte(), 'A'.code.toByte(), 'V'.code.toByte(), 'E'.code.toByte(), |
||||
|
'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(), |
||||
|
16, 0, 0, 0, // PCM头长度 |
||||
|
1, 0, // PCM格式 |
||||
|
1, 0, // 单声道 |
||||
|
(sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), 0, 0, // 采样率 |
||||
|
(byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), 0, 0, // 字节率 |
||||
|
2, 0, // 块对齐 (通道数 * 样本位数/8) |
||||
|
16, 0, // 样本位数 |
||||
|
'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(), |
||||
|
(dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(), |
||||
|
((dataLength shr 16) and 0xFF).toByte(), ((dataLength shr 24) and 0xFF).toByte() |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 重命名指定路径的音频文件 |
||||
|
* @param filePath 要重命名的文件完整路径 |
||||
|
* @param newName 新的文件名(不含扩展名) |
||||
|
* @return true 重命名成功,false 失败 |
||||
|
*/ |
||||
|
internal fun renameFile(filePath: String, newName: String): Boolean { |
||||
|
val sourceFile = File(filePath) |
||||
|
|
||||
|
// 验证源文件存在 |
||||
|
if (!sourceFile.exists()) { |
||||
|
Log.w("tag", "重命名失败: 文件不存在 $filePath") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 防止重命名正在录制的文件 |
||||
|
if (sourceFile == currentAudioFile) { |
||||
|
Log.w("tag", "重命名失败: 不能重命名正在录制的文件") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
val parentDir = sourceFile.parentFile |
||||
|
val newFile = File(parentDir, "$newName.wav") // 保持.wav扩展名 |
||||
|
|
||||
|
// 处理目标文件已存在的情况 |
||||
|
if (newFile.exists()) { |
||||
|
Log.w("tag", "重命名失败: 目标文件已存在 ${newFile.absolutePath}") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 执行重命名 |
||||
|
return try { |
||||
|
val success = sourceFile.renameTo(newFile) |
||||
|
if (success) { |
||||
|
Log.d("tag", "文件重命名成功: ${sourceFile.name} -> ${newFile.name}") |
||||
|
// 更新最后保存的文件引用(如果是同一个文件) |
||||
|
if (lastSavedFile == sourceFile) { |
||||
|
lastSavedFile = newFile |
||||
|
} |
||||
|
} else { |
||||
|
Log.w("tag", "文件重命名操作失败") |
||||
|
} |
||||
|
success |
||||
|
} catch (e: Exception) { |
||||
|
Log.e("tag", "重命名异常: ${e.message}") |
||||
|
false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 移动文件到新路径 |
||||
|
* @param sourcePath 原始文件完整路径 |
||||
|
* @param destPath 目标文件完整路径 |
||||
|
* @return true 移动成功,false 移动失败 |
||||
|
*/ |
||||
|
internal fun moveFile(sourcePath: String, destPath: String): Boolean { |
||||
|
val sourceFile = File(sourcePath) |
||||
|
val destFile = File(destPath) |
||||
|
|
||||
|
if (!sourceFile.exists()) { |
||||
|
Log.w("tag", "移动失败: 源文件不存在 ${sourceFile.absolutePath}") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 确保目标目录存在 |
||||
|
val destDir = destFile.parentFile |
||||
|
if (!destDir.exists() && !destDir.mkdirs()) { |
||||
|
Log.w("tag", "移动失败: 无法创建目标目录 ${destDir.absolutePath}") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 删除已存在的目标文件 |
||||
|
if (destFile.exists() && !destFile.delete()) { |
||||
|
Log.w("tag", "移动失败: 无法覆盖已存在文件 ${destFile.absolutePath}") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
return try { |
||||
|
// 使用renameTo尝试移动(同分区最快) |
||||
|
if (sourceFile.renameTo(destFile)) { |
||||
|
true |
||||
|
} else { |
||||
|
// 跨分区移动:复制+删除 |
||||
|
FileInputStream(sourceFile).use { input -> |
||||
|
FileOutputStream(destFile).use { output -> |
||||
|
input.copyTo(output) |
||||
|
} |
||||
|
} |
||||
|
if (!sourceFile.delete()) { |
||||
|
Log.w("tag", "移动警告: 源文件删除失败 ${sourceFile.absolutePath}") |
||||
|
} |
||||
|
true |
||||
|
}.also { |
||||
|
if (it) { |
||||
|
Log.d("tag", "文件移动成功: ${sourcePath} -> ${destPath}") |
||||
|
// 更新最后保存的文件引用 |
||||
|
if (lastSavedFile?.absolutePath == sourcePath) { |
||||
|
lastSavedFile = destFile |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
Log.e("tag", "文件移动异常: ${e.message}") |
||||
|
false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal fun closeFile(isSave: Boolean): Boolean { |
||||
|
try { |
||||
|
if (fos == null || currentAudioFile == null) { |
||||
|
return false |
||||
|
} |
||||
|
isWriting.set(false) |
||||
|
writeThread?.join(500) |
||||
|
fos?.close() |
||||
|
|
||||
|
currentAudioFile?.let { file -> |
||||
|
if (file.length() <= 44 || isSave == false) { |
||||
|
file.delete() |
||||
|
Log.d("tag", "音频文件过小已删除: ${file.absolutePath}") |
||||
|
return false |
||||
|
} else { |
||||
|
RandomAccessFile(file, "rw").use { raf -> |
||||
|
raf.seek(0) |
||||
|
raf.write(generateWavHeader(totalBytesWritten)) |
||||
|
} |
||||
|
Log.d("tag", "音频文件保存完成: ${file.absolutePath}") |
||||
|
|
||||
|
// 保存成功文件引用 |
||||
|
lastSavedFile = file |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
Log.e("tag", "更新WAV文件头失败: ${e.message}") |
||||
|
return false |
||||
|
} finally { |
||||
|
fos = null |
||||
|
currentAudioFile = null |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,131 @@ |
|||||
|
import Foundation |
||||
|
import AMapLocationKit |
||||
|
|
||||
|
extension Notification.Name { |
||||
|
static let locationstartEvent = Notification.Name("locationstartEvent") //启动定位 |
||||
|
static let updatelocationEvent = Notification.Name("updatelocationEvent") //更新位置事件 |
||||
|
} |
||||
|
class LocationService: NSObject { |
||||
|
static let shared = LocationService() |
||||
|
private let TAG = "LocationService" |
||||
|
|
||||
|
private var locationManager: AMapLocationManager! |
||||
|
private var currentLocation: CLLocation? |
||||
|
private var currentReGeocode: AMapLocationReGeocode? |
||||
|
private var initialized = false |
||||
|
|
||||
|
private override init() { |
||||
|
super.init() |
||||
|
} |
||||
|
|
||||
|
// 初始化定位服务 |
||||
|
func initialize() { |
||||
|
if initialized { return } |
||||
|
print("\(TAG)--------- 初始化高德定位服务") |
||||
|
|
||||
|
locationManager = AMapLocationManager() |
||||
|
locationManager.delegate = self |
||||
|
|
||||
|
locationManager.desiredAccuracy = kCLLocationAccuracyBest // 定位精度 |
||||
|
locationManager.distanceFilter = 10.0 // 移动距离阈值(单位:米) |
||||
|
locationManager.locatingWithReGeocode = true // 是否返回逆地理编码信息 |
||||
|
locationManager.allowsBackgroundLocationUpdates = true // 是否允许后台定位 |
||||
|
locationManager.locationTimeout = 10 // 定位超时时间(秒) |
||||
|
locationManager.reGeocodeTimeout = 5 // 逆地理编码超时时间(秒) |
||||
|
|
||||
|
locationManager.startUpdatingLocation() |
||||
|
|
||||
|
initialized = true |
||||
|
} |
||||
|
|
||||
|
// 获取缓存的位置信息 |
||||
|
func getCachedLocation() -> [String: String]? { |
||||
|
guard let location = currentLocation else { return nil } |
||||
|
|
||||
|
var locationInfo: [String: String] = [ |
||||
|
"user_lat": "\(location.coordinate.latitude)", |
||||
|
"user_lng": "\(location.coordinate.longitude)" |
||||
|
] |
||||
|
|
||||
|
if let regeocode = currentReGeocode { |
||||
|
let address = [ |
||||
|
regeocode.province, // 省 |
||||
|
regeocode.city, // 市 |
||||
|
regeocode.district, // 区 |
||||
|
regeocode.street, // 街道 |
||||
|
regeocode.number // 门牌号 |
||||
|
].compactMap { $0 }.joined(separator: "") |
||||
|
|
||||
|
locationInfo["user_location"] = address |
||||
|
} |
||||
|
|
||||
|
return locationInfo |
||||
|
} |
||||
|
|
||||
|
// 获取当前位置(带回调) |
||||
|
func getCurrentLocation(completion: @escaping ([String: String]?, Error?) -> Void) { |
||||
|
if let cached = getCachedLocation() { |
||||
|
completion(cached, nil) |
||||
|
} else { |
||||
|
let error = NSError( |
||||
|
domain: "LocationService", |
||||
|
code: -1, |
||||
|
userInfo: [NSLocalizedDescriptionKey: "当前位置还未获取"] |
||||
|
) |
||||
|
completion(nil, error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 停止定位服务 |
||||
|
func stop() { |
||||
|
locationManager.stopUpdatingLocation() |
||||
|
locationManager = nil |
||||
|
currentLocation = nil |
||||
|
currentReGeocode = nil |
||||
|
initialized = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - AMapLocationManagerDelegate |
||||
|
extension LocationService: AMapLocationManagerDelegate { |
||||
|
// 定位更新回调 |
||||
|
func amapLocationManager(_ manager: AMapLocationManager!, didUpdate location: CLLocation!, reGeocode: AMapLocationReGeocode!) { |
||||
|
currentLocation = location |
||||
|
currentReGeocode = reGeocode |
||||
|
|
||||
|
print("\(TAG)--------- 定位更新: \(location.coordinate.latitude), \(location.coordinate.longitude)") |
||||
|
|
||||
|
var locationInfo: [String: String] = [ |
||||
|
"user_lat": "\(location.coordinate.latitude)", |
||||
|
"user_lng": "\(location.coordinate.longitude)" |
||||
|
] |
||||
|
if let regeocode = currentReGeocode { |
||||
|
let address = [ |
||||
|
reGeocode.province, // 省 |
||||
|
reGeocode.city, // 市 |
||||
|
reGeocode.district, // 区 |
||||
|
reGeocode.street, // 街道 |
||||
|
reGeocode.number // 门牌号 |
||||
|
].compactMap { $0 }.joined(separator: "") |
||||
|
print("\(TAG)--------- 定位地址: \(address)") |
||||
|
locationInfo["user_location"] = address |
||||
|
} |
||||
|
|
||||
|
|
||||
|
NotificationCenter.default.post( |
||||
|
name: .updatelocationEvent, |
||||
|
object: nil, |
||||
|
userInfo: locationInfo |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// 定位失败回调 |
||||
|
func amapLocationManager(_ manager: AMapLocationManager!, didFailWithError error: Error!) { |
||||
|
print("\(TAG)--------- 定位失败: \(error.localizedDescription)") |
||||
|
} |
||||
|
|
||||
|
// 权限变更回调 |
||||
|
func amapLocationManager(_ manager: AMapLocationManager!, doRequireLocationAuth locationManager: CLLocationManager!) { |
||||
|
locationManager.requestWhenInUseAuthorization() |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
import Flutter |
||||
|
import UIKit |
||||
|
import AMapLocationKit |
||||
|
|
||||
|
public class LocationServicePlugin: NSObject, FlutterPlugin { |
||||
|
private static var channel: FlutterMethodChannel? |
||||
|
|
||||
|
public static func register(with registrar: FlutterPluginRegistrar) { |
||||
|
let channel = FlutterMethodChannel(name: "com.yunqiinnovation.location_service", binaryMessenger: registrar.messenger()) |
||||
|
let instance = LocationServicePlugin() |
||||
|
// 必须先设置隐私合规 |
||||
|
DispatchQueue.main.async { |
||||
|
// 2. API Key |
||||
|
AMapServices.shared()?.apiKey = "aa60a03be9f6531bcc64029e60f95d03" |
||||
|
AMapServices.shared().enableHTTPS = true |
||||
|
// 3. 打印验证(调试用) |
||||
|
print("高德SDK初始化完成") |
||||
|
} |
||||
|
registrar.addMethodCallDelegate(instance, channel: channel) |
||||
|
self.channel = channel |
||||
|
instance.addstartlocationmonitor() |
||||
|
} |
||||
|
|
||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
switch call.method { |
||||
|
case "location": |
||||
|
result(nil) |
||||
|
default: |
||||
|
result(FlutterMethodNotImplemented) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func addstartlocationmonitor(){ |
||||
|
// 通常放在启动后或模块初始化时 |
||||
|
NotificationCenter.default.addObserver( |
||||
|
forName: .locationstartEvent, |
||||
|
object: nil, |
||||
|
queue: .main |
||||
|
) { notification in |
||||
|
print("📥 收到 启动定位服务事件") |
||||
|
LocationService.shared.initialize() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,32 +0,0 @@ |
|||||
// swift-tools-version: 5.9 |
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package. |
|
||||
|
|
||||
import PackageDescription |
|
||||
|
|
||||
let package = Package( |
|
||||
name: "location_service", |
|
||||
platforms: [ |
|
||||
.iOS("12.0") |
|
||||
], |
|
||||
products: [ |
|
||||
.library(name: "location-service", targets: ["location_service"]) |
|
||||
], |
|
||||
dependencies: [], |
|
||||
targets: [ |
|
||||
.target( |
|
||||
name: "location_service", |
|
||||
dependencies: [], |
|
||||
resources: [ |
|
||||
// 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 these lines. For more information, see |
|
||||
// https://developer.apple.com/documentation/bundleresources/privacy_manifest_files |
|
||||
// .process("PrivacyInfo.xcprivacy"), |
|
||||
|
|
||||
// If you have other resources that need to be bundled with your plugin, refer to |
|
||||
// the following instructions to add them: |
|
||||
// https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package |
|
||||
] |
|
||||
) |
|
||||
] |
|
||||
) |
|
||||
@ -1,19 +0,0 @@ |
|||||
import Flutter |
|
||||
import UIKit |
|
||||
|
|
||||
public class LocationServicePlugin: NSObject, FlutterPlugin { |
|
||||
public static func register(with registrar: FlutterPluginRegistrar) { |
|
||||
let channel = FlutterMethodChannel(name: "location_service", binaryMessenger: registrar.messenger()) |
|
||||
let instance = LocationServicePlugin() |
|
||||
registrar.addMethodCallDelegate(instance, channel: channel) |
|
||||
} |
|
||||
|
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|
||||
switch call.method { |
|
||||
case "getPlatformVersion": |
|
||||
result("iOS " + UIDevice.current.systemVersion) |
|
||||
default: |
|
||||
result(FlutterMethodNotImplemented) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,14 +0,0 @@ |
|||||
<?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> |
|
||||
Loading…
Reference in new issue