完成多平台拍照软件
This commit is contained in:
@@ -14,7 +14,7 @@ kotlin {
|
||||
androidTarget {
|
||||
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
jvmTarget.set(JvmTarget.JVM_21)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ kotlin {
|
||||
implementation(compose.components.uiToolingPreview)
|
||||
implementation(libs.androidx.lifecycle.viewmodel)
|
||||
implementation(libs.androidx.lifecycle.runtimeCompose)
|
||||
// 通用ViewModel
|
||||
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1")
|
||||
}
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
@@ -43,6 +45,8 @@ kotlin {
|
||||
desktopMain.dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(libs.kotlinx.coroutinesSwing)
|
||||
// 摄像头
|
||||
implementation("org.bytedeco:javacv-platform:1.5.9")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,8 +73,8 @@ android {
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -18,7 +19,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(widthDp = 1280, heightDp = 800)
|
||||
@Composable
|
||||
fun AppAndroidPreview() {
|
||||
App()
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.bbitcn.bbit_ai
|
||||
|
||||
import android.os.Build
|
||||
|
||||
class AndroidPlatform : Platform {
|
||||
override val name: String = "Android ${Build.VERSION.SDK_INT}"
|
||||
}
|
||||
|
||||
actual fun getPlatform(): Platform = AndroidPlatform()
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.bbitcn.bbit_ai.plantform.CommonInterface
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
actual class MyCamera {
|
||||
actual fun stopCamera() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
actual fun listAvailableCameras(): List<String> {
|
||||
return List(3) { "Camera $it" } // 假设有 3 个摄像头
|
||||
}
|
||||
|
||||
actual suspend fun startCamera(index: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
actual suspend fun takePhoto(): ImageBitmap? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
actual fun saveImageBitmapToFile(image: ImageBitmap) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
actual fun loadImage(filePath: String): ImageBitmap? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
||||
actual fun getMyCamera(): MyCamera {
|
||||
return com.bbitcn.bbit_ai.plantform.CommonInterface.MyCamera()
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.bbitcn.bbit_ai.ui.mainFunction
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bbitcn.bbit_ai.plantform.CommonInterface.MyCamera
|
||||
|
||||
@Composable
|
||||
actual fun CameraPreview(
|
||||
modifier: Modifier,
|
||||
controller: MyCamera
|
||||
) {
|
||||
}
|
||||
@@ -1,44 +1,28 @@
|
||||
package com.bbitcn.bbit_ai
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.safeContentPadding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
import bbit_ai.composeapp.generated.resources.Res
|
||||
import bbit_ai.composeapp.generated.resources.compose_multiplatform
|
||||
import com.bbitcn.bbit_ai.ui.mainFunction.MainScreen
|
||||
|
||||
|
||||
val M = Modifier
|
||||
.animateContentSize(
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
@Preview()
|
||||
fun App() {
|
||||
MaterialTheme {
|
||||
var showContent by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.safeContentPadding()
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Button(onClick = { showContent = !showContent }) {
|
||||
Text("Click me!")
|
||||
}
|
||||
AnimatedVisibility(showContent) {
|
||||
val greeting = remember { Greeting().greet() }
|
||||
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Image(painterResource(Res.drawable.compose_multiplatform), null)
|
||||
Text("Compose: $greeting")
|
||||
}
|
||||
}
|
||||
}
|
||||
MainScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.bbitcn.bbit_ai
|
||||
|
||||
class Greeting {
|
||||
private val platform = getPlatform()
|
||||
|
||||
fun greet(): String {
|
||||
return "Hello, ${platform.name}!"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.bbitcn.bbit_ai
|
||||
|
||||
interface Platform {
|
||||
val name: String
|
||||
}
|
||||
|
||||
expect fun getPlatform(): Platform
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
package com.bbitcn.f8.pad.base
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.SocketException
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
open class BaseViewModel : ViewModel() {
|
||||
|
||||
|
||||
protected fun doInUIThread(task: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
task()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun <T> doInIoThreadThenUI(
|
||||
loadingTips: String = "正在加载中",
|
||||
showDialog: Boolean = true,
|
||||
onError: (Throwable) -> Unit = { },
|
||||
onIO: suspend () -> T,
|
||||
onFinish: () -> Unit = { },
|
||||
onUI: (T) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val result = kotlin.runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (showDialog) {
|
||||
// Toasty.showLoadingDialog(loadingTips)
|
||||
}
|
||||
onIO()
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
// hideLoadingDialog()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
result.onSuccess { data ->
|
||||
onUI(data)
|
||||
}.onFailure { exception ->
|
||||
// ✅ 如果是协程取消,不处理,只记录日志
|
||||
if (exception is CancellationException || exception.cause is SocketException && exception.cause?.message?.contains(
|
||||
"Socket closed"
|
||||
) == true
|
||||
) {
|
||||
// MyLog.test("协程被取消:${exception.javaClass.simpleName},message=${exception.message}")
|
||||
return@onFailure
|
||||
}
|
||||
// 其他异常继续处理
|
||||
exception.printStackTrace()
|
||||
onError(exception)
|
||||
exception.message?.let {
|
||||
// Toasty.error(it)
|
||||
}
|
||||
}.also {
|
||||
// ✅ 最终执行的操作
|
||||
onFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun <T> doInIoThread(
|
||||
loadingTips: String = "正在加载中",
|
||||
showDialog: Boolean = true,
|
||||
onError: (Throwable) -> Unit = { },
|
||||
onFinish: () -> Unit = { },
|
||||
doInIO: suspend () -> T,
|
||||
) {
|
||||
doInIoThreadThenUI(loadingTips, showDialog, onError, doInIO, onFinish) { }
|
||||
}
|
||||
|
||||
fun <T> doInIoThreadNoDialog(
|
||||
onError: (Throwable) -> Unit = { },
|
||||
task: suspend () -> T,
|
||||
) {
|
||||
doInIoThread(showDialog = false, doInIO = task, onError = onError)
|
||||
}
|
||||
|
||||
/**
|
||||
* 在IO线程中执行任务,可选择是否显示加载对话框
|
||||
*/
|
||||
fun doInIoThreadWith(showLoading: Boolean, loadingTips: String, function: suspend () -> Unit) {
|
||||
if (showLoading) {
|
||||
doInIoThread(loadingTips) { function() }
|
||||
} else {
|
||||
doInIoThreadNoDialog { function() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动一个无限轮询任务
|
||||
*
|
||||
* @param pollingInterval 轮询间隔时间(单位:秒)
|
||||
* @param pollingTask 轮询任务的挂起函数
|
||||
*/
|
||||
fun polling(intervalSeconds: Long, task: suspend () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
// MyLog.test("开始轮询任务,间隔:$intervalSeconds 秒")
|
||||
while (true) {
|
||||
task()
|
||||
delay(intervalSeconds * 1000L) // 转换秒为毫秒
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟开始轮询任务
|
||||
*/
|
||||
suspend fun delayPolling(delaySeconds: Long, intervalSeconds: Long, task: suspend () -> Unit) {
|
||||
delay(delaySeconds * 1000L)
|
||||
polling(intervalSeconds, task)
|
||||
}
|
||||
|
||||
private val taskMap = mutableMapOf<String, Job>()
|
||||
|
||||
/**
|
||||
* 放弃旧任务,执行新任务
|
||||
*
|
||||
* @param key 任务的唯一标识符
|
||||
* @param block 任务的挂起函数,必须使用协程,不能开启新的协程,否则无法取消任务
|
||||
*/
|
||||
fun launchTaskNewFirst(
|
||||
key: String,
|
||||
block: suspend () -> Unit
|
||||
) {
|
||||
taskMap[key]?.cancel() // 取消旧任务
|
||||
val job = viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
taskMap[key] = job
|
||||
}
|
||||
|
||||
/**
|
||||
* 有新任务时,取消。优先执行旧任务直到完成
|
||||
*
|
||||
* @param key 任务的唯一标识符
|
||||
* @param block 任务函数,可以自由新建协程,但一定要在任务完成时调用 onFinished 回调,否则会导致后续任务永远无法执行
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun launchTaskOldFirst(
|
||||
key: String,
|
||||
block: (onFinished: () -> Unit) -> Unit
|
||||
) {
|
||||
val existing = taskMap[key]
|
||||
if (existing?.isActive == true) {
|
||||
return
|
||||
}
|
||||
val job = viewModelScope.launch {
|
||||
try {
|
||||
suspendCancellableCoroutine<Unit> { continuation ->
|
||||
block {
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(Unit) { cause ->
|
||||
println("resume 后任务被取消: $cause")
|
||||
}
|
||||
}
|
||||
taskMap.remove(key)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
taskMap.remove(key)
|
||||
}
|
||||
}
|
||||
taskMap[key] = job
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
taskMap.values.forEach { it.cancel() }
|
||||
taskMap.clear()
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
package com.bbitcn.bbit_ai.base
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.bbitcn.bbit_ai.model
|
||||
|
||||
data class SaveData(
|
||||
val id: String,
|
||||
val projectName: String,
|
||||
val createTime: String,
|
||||
val rawImagePath: String,
|
||||
val aiImagePath: String,
|
||||
val extraJson: String
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.bbitcn.bbit_ai.plantform.CommonInterface
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
|
||||
expect class MyCamera {
|
||||
suspend fun startCamera(index: Int = 0)
|
||||
fun stopCamera()
|
||||
fun listAvailableCameras(): List<String>
|
||||
suspend fun takePhoto(): ImageBitmap?
|
||||
|
||||
fun saveImageBitmapToFile(image: ImageBitmap)
|
||||
fun loadImage(filePath: String): ImageBitmap?
|
||||
}
|
||||
|
||||
expect fun getMyCamera(): MyCamera
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
package com.bbitcn.bbit_ai.ui.mainFunction
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeContentPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import bbit_ai.composeapp.generated.resources.Res
|
||||
import bbit_ai.composeapp.generated.resources.compose_multiplatform
|
||||
import com.bbitcn.bbit_ai.plantform.CommonInterface.MyCamera
|
||||
import com.bbitcn.bbit_ai.M
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
// commonMain
|
||||
@Composable
|
||||
expect fun CameraPreview(modifier: Modifier = M, controller: MyCamera)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
mainViewModel: MainViewModel = viewModel(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.safeContentPadding()
|
||||
.padding(10.dp)
|
||||
.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val cameraList by mainViewModel.cameraList.collectAsState()
|
||||
var selectedIndex by remember { mutableStateOf(-1) }
|
||||
val detail by mainViewModel.curDetail.collectAsState()
|
||||
LaunchedEffect(cameraList) {
|
||||
if (cameraList.isNotEmpty() && selectedIndex == -1) {
|
||||
selectedIndex = 0 // 默认选择第一个摄像头
|
||||
mainViewModel.startCamera(selectedIndex) // 启动默认摄像头
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = M.width(250.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "摄像头:",
|
||||
modifier = M.padding(end = 10.dp),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = if (cameraList.isEmpty() || selectedIndex == -1) "未检测到摄像头" else cameraList[selectedIndex],
|
||||
modifier = M
|
||||
.padding(10.dp)
|
||||
.clickable {
|
||||
expanded = true
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
cameraList.forEachIndexed { index, label ->
|
||||
DropdownMenuItem(text = {
|
||||
Text(label)
|
||||
}, onClick = {
|
||||
selectedIndex = index
|
||||
expanded = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// 刷新摄像头按钮
|
||||
Button(
|
||||
modifier = M.padding(2.5.dp),
|
||||
onClick = { mainViewModel.refreshCameraList() }
|
||||
) {
|
||||
Text("刷新")
|
||||
}
|
||||
Button(
|
||||
modifier = M.padding(2.5.dp), onClick = {
|
||||
mainViewModel.startCamera(selectedIndex)
|
||||
}) {
|
||||
Text("连接")
|
||||
}
|
||||
Button(
|
||||
modifier = M.padding(2.5.dp), onClick = {
|
||||
mainViewModel.takePhots()
|
||||
}) {
|
||||
Text("拍照")
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// 刷新摄像头按钮
|
||||
Button(
|
||||
modifier = M.padding(2.5.dp),
|
||||
onClick = { mainViewModel.initData() }
|
||||
) {
|
||||
Text("初始化数据")
|
||||
}
|
||||
Button(
|
||||
modifier = M.padding(2.5.dp), onClick = {
|
||||
mainViewModel.refreshDataList()
|
||||
}) {
|
||||
Text("刷新列表")
|
||||
}
|
||||
}
|
||||
HorizontalDivider(modifier = M.padding(5.dp))
|
||||
val list by mainViewModel.analyticsList.collectAsState()
|
||||
LazyColumn(modifier = M.weight(1f)) {
|
||||
items(list) {
|
||||
Button(modifier = M.padding(5.dp).animateItem(), onClick = {
|
||||
mainViewModel.setCurDetail(it)
|
||||
}) {
|
||||
Column {
|
||||
Text(
|
||||
text = it.projectName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = MaterialTheme.typography.bodyLarge.fontSize,
|
||||
modifier = M.padding(2.5.dp)
|
||||
)
|
||||
Text(
|
||||
text = it.createTime,
|
||||
maxLines = 1,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
modifier = M.padding(2.5.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider(modifier = M.padding(5.dp))
|
||||
Box(modifier = M.fillMaxWidth().height(50.dp)) {
|
||||
Text(
|
||||
text = detail?.extraJson ?: "",
|
||||
modifier = M.padding(5.dp).fillMaxSize(),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
CameraPreview(M.fillMaxHeight().weight(2f).widthIn(min = 600.dp), mainViewModel.controller)
|
||||
Column(
|
||||
modifier = M.weight(1f).fillMaxHeight(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val picRaw by mainViewModel.picRaw.collectAsState()
|
||||
if (picRaw == null) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
MyArea()
|
||||
}
|
||||
} else {
|
||||
Image(
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
bitmap = picRaw!!,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillHeight
|
||||
)
|
||||
}
|
||||
val picAi by mainViewModel.picAi.collectAsState()
|
||||
if (picAi == null) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
MyArea()
|
||||
}
|
||||
} else {
|
||||
Image(
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
bitmap = picAi!!,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MyArea(modifier: Modifier = M) {
|
||||
Image(
|
||||
modifier = modifier.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
painter = painterResource(Res.drawable.compose_multiplatform),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.bbitcn.bbit_ai.ui.mainFunction
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import com.bbitcn.bbit_ai.model.SaveData
|
||||
import com.bbitcn.bbit_ai.plantform.CommonInterface.getMyCamera
|
||||
import com.bbitcn.bbit_ai.utils.DataFileUtil
|
||||
import com.bbitcn.f8.pad.base.BaseViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.io.File
|
||||
|
||||
class MainViewModel : BaseViewModel() {
|
||||
|
||||
val controller = getMyCamera()
|
||||
private val _cameraList = MutableStateFlow(listOf<String>())
|
||||
val cameraList = _cameraList.asStateFlow()
|
||||
private val _analyticsList = MutableStateFlow(listOf<SaveData>())
|
||||
val analyticsList = _analyticsList.asStateFlow()
|
||||
private val _curDetail = MutableStateFlow<SaveData?>(null)
|
||||
val curDetail = _curDetail.asStateFlow()
|
||||
private val _picRaw = MutableStateFlow<ImageBitmap?>(null)
|
||||
val picRaw = _picRaw.asStateFlow()
|
||||
private val _picAi = MutableStateFlow<ImageBitmap?>(null)
|
||||
val picAi = _picAi.asStateFlow()
|
||||
|
||||
init {
|
||||
refreshCameraList()
|
||||
refreshDataList {
|
||||
if (_curDetail.value == null && _analyticsList.value.isNotEmpty()) {
|
||||
setCurDetail(_analyticsList.value.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshCameraList() {
|
||||
doInIoThread {
|
||||
_cameraList.value = controller.listAvailableCameras() // 获取摄像头列表(见下方)
|
||||
}
|
||||
}
|
||||
|
||||
fun startCamera(index: Int = 0) {
|
||||
launchTaskNewFirst("启动摄像头") {
|
||||
controller.startCamera(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun takePhots() {
|
||||
doInIoThread {
|
||||
controller.takePhoto()?.let {
|
||||
controller.saveImageBitmapToFile(it)
|
||||
refreshDataList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun initData() {
|
||||
doInIoThread {
|
||||
DataFileUtil.init() // 初始化数据文件
|
||||
refreshDataList()
|
||||
_curDetail.value = null // 清空当前详情
|
||||
_picRaw.value = null // 清空图片预览
|
||||
_picAi.value = null // 清空图片预览
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshDataList(onFinish: () -> Unit = {}) {
|
||||
doInIoThread {
|
||||
_analyticsList.value = DataFileUtil.readAll()
|
||||
onFinish()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurDetail(it: SaveData) {
|
||||
doInIoThread {
|
||||
_curDetail.value = it
|
||||
_picRaw.value = controller.loadImage(it.rawImagePath)
|
||||
_picAi.value = controller.loadImage(it.aiImagePath)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.bbitcn.bbit_ai.utils
|
||||
|
||||
import com.bbitcn.bbit_ai.model.SaveData
|
||||
import java.io.File
|
||||
|
||||
object DataFileUtil {
|
||||
|
||||
private val dataDir = File("data")
|
||||
private val dataFile = File(dataDir, "data.txt")
|
||||
val rawImage = File(dataDir,"image/raw")
|
||||
val aiImage = File(dataDir,"image/ai")
|
||||
|
||||
fun init() {
|
||||
if (!dataDir.exists()) {
|
||||
dataDir.mkdirs()
|
||||
}
|
||||
if (!rawImage.exists()) {
|
||||
rawImage.mkdirs()
|
||||
}
|
||||
if (!aiImage.exists()) {
|
||||
aiImage.mkdirs()
|
||||
}
|
||||
dataFile.writeText("") // 清空内容
|
||||
}
|
||||
|
||||
fun addData(
|
||||
info: SaveData
|
||||
) {
|
||||
if (!dataDir.exists()) dataDir.mkdirs()
|
||||
val line = info.id + "\t" +
|
||||
info.projectName + "\t" +
|
||||
info.createTime + "\t" +
|
||||
info.rawImagePath + "\t" +
|
||||
info.aiImagePath + "\t" +
|
||||
info.extraJson
|
||||
dataFile.appendText(line + "\n")
|
||||
}
|
||||
|
||||
fun readAll(): List<SaveData> {
|
||||
if (!dataFile.exists()) return emptyList()
|
||||
return dataFile.readLines().mapNotNull { line ->
|
||||
val parts = line.split("\t")
|
||||
if (parts.size < 6) return@mapNotNull null
|
||||
SaveData(
|
||||
id = parts[0],
|
||||
projectName = parts[1],
|
||||
createTime = parts[2],
|
||||
rawImagePath = parts[3],
|
||||
aiImagePath = parts[4],
|
||||
extraJson = parts[5]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.bbitcn.bbit_ai.utils
|
||||
|
||||
object MyUtils {
|
||||
fun getCurrentTime(): String {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
return java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(java.util.Date(currentTime))
|
||||
}
|
||||
|
||||
fun getCurrentDate(): String {
|
||||
val currentDate = System.currentTimeMillis()
|
||||
return java.text.SimpleDateFormat("yyyy-MM-dd").format(java.util.Date(currentDate))
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package com.bbitcn.bbit_ai.plantform.CommonInterface
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asSkiaBitmap
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import com.bbitcn.bbit_ai.model.SaveData
|
||||
import com.bbitcn.bbit_ai.utils.DataFileUtil
|
||||
import com.bbitcn.bbit_ai.utils.MyUtils
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.bytedeco.javacv.Java2DFrameConverter
|
||||
import org.bytedeco.javacv.OpenCVFrameGrabber
|
||||
import org.bytedeco.opencv.opencv_videoio.VideoCapture
|
||||
import org.jetbrains.skia.Image
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
actual class MyCamera {
|
||||
val frameFlow = MutableStateFlow<ImageBitmap?>(null)
|
||||
private var running = false
|
||||
private var curCameraId = 0
|
||||
|
||||
actual suspend fun startCamera(index: Int) {
|
||||
curCameraId = index
|
||||
running = false
|
||||
val grabber = OpenCVFrameGrabber(index)
|
||||
try {
|
||||
grabber.start()
|
||||
val converter = Java2DFrameConverter()
|
||||
running = true
|
||||
while (running) {
|
||||
val frame = grabber.grab() ?: continue
|
||||
val image = converter.getBufferedImage(frame) ?: continue
|
||||
frameFlow.value = image.toComposeImageBitmap()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Camera grab error: ${e.message}")
|
||||
} finally {
|
||||
grabber.stop()
|
||||
grabber.release()
|
||||
}
|
||||
}
|
||||
|
||||
actual fun stopCamera() {
|
||||
running = false
|
||||
}
|
||||
|
||||
private fun BufferedImage.toComposeImageBitmap(): ImageBitmap {
|
||||
val bytes = ByteArrayOutputStream().use {
|
||||
ImageIO.write(this, "png", it)
|
||||
it.toByteArray()
|
||||
}
|
||||
val bitmap = Image.makeFromEncoded(bytes).toComposeImageBitmap()
|
||||
return bitmap
|
||||
}
|
||||
|
||||
actual fun listAvailableCameras(): List<String> {
|
||||
val availableCameras = mutableListOf<String>()
|
||||
for (i in 0..3) {
|
||||
val capture = VideoCapture(i)
|
||||
if (capture.isOpened) {
|
||||
availableCameras.add("Camera $i")
|
||||
capture.release()
|
||||
}
|
||||
}
|
||||
return availableCameras
|
||||
}
|
||||
|
||||
actual suspend fun takePhoto(): ImageBitmap? {
|
||||
return frameFlow.value
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
actual fun saveImageBitmapToFile(image: ImageBitmap) {
|
||||
val bufferedImage = image.toBufferedImage()
|
||||
val imageName = "photo_${System.currentTimeMillis()}.png"
|
||||
val uuid = Uuid.random().toString()
|
||||
DataFileUtil.addData(
|
||||
SaveData(
|
||||
Uuid.random().toString(),
|
||||
imageName,
|
||||
MyUtils.getCurrentTime(),
|
||||
DataFileUtil.rawImage.absolutePath + File.separator + imageName,
|
||||
DataFileUtil.aiImage.absolutePath + File.separator + imageName,
|
||||
uuid + "额外信息"
|
||||
)
|
||||
)
|
||||
ImageIO.write(
|
||||
bufferedImage,
|
||||
"png",
|
||||
File(DataFileUtil.rawImage, imageName)
|
||||
)
|
||||
ImageIO.write(
|
||||
bufferedImage,
|
||||
"png",
|
||||
File(DataFileUtil.aiImage, imageName)
|
||||
)
|
||||
}
|
||||
|
||||
actual fun loadImage(filePath: String): ImageBitmap? {
|
||||
return try {
|
||||
val bytes = File(filePath).readBytes()
|
||||
val skiaImage = Image.makeFromEncoded(bytes)
|
||||
skiaImage.toComposeImageBitmap()
|
||||
} catch (e: Exception) {
|
||||
println("Image decode error: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun ImageBitmap.toBufferedImage(): BufferedImage {
|
||||
val skiaImage = Image.makeFromBitmap(this.asSkiaBitmap())
|
||||
val bytes = skiaImage.encodeToData()?.bytes ?: throw Exception("Encode failed")
|
||||
return ImageIO.read(bytes.inputStream())
|
||||
}
|
||||
}
|
||||
|
||||
actual fun getMyCamera(): MyCamera {
|
||||
return MyCamera()
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.bbitcn.bbit_ai.ui.mainFunction
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bbitcn.bbit_ai.M
|
||||
import com.bbitcn.bbit_ai.plantform.CommonInterface.MyCamera
|
||||
|
||||
|
||||
@Composable
|
||||
actual fun CameraPreview(
|
||||
modifier: Modifier,
|
||||
controller: MyCamera
|
||||
) {
|
||||
val imageBitmap = controller.frameFlow.collectAsState()
|
||||
// 显示图像
|
||||
Box(
|
||||
modifier = modifier
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = androidx.compose.material3.MaterialTheme.colorScheme.primary
|
||||
)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
imageBitmap.value?.let {
|
||||
Image(
|
||||
modifier = M.fillMaxSize(),
|
||||
bitmap = it,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.bbitcn.bbit_ai
|
||||
|
||||
class JVMPlatform: Platform {
|
||||
override val name: String = "Java ${System.getProperty("java.version")}"
|
||||
}
|
||||
|
||||
actual fun getPlatform(): Platform = JVMPlatform()
|
||||
Reference in New Issue
Block a user