1、修改识别下载资源错误问题
2、添加全局Toast中心弹窗自定义
3、首页界面优化
This commit is contained in:
shenzuqiang 2026-03-10 11:02:00 +08:00
parent 8dfffa0c95
commit 5278050b1c
6 changed files with 1119 additions and 914 deletions

View File

@ -56,6 +56,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.img.rabbit.components.GlobalToast
import com.img.rabbit.config.Constants import com.img.rabbit.config.Constants
import com.img.rabbit.config.Constants.agreementUrl import com.img.rabbit.config.Constants.agreementUrl
import com.img.rabbit.config.Constants.privacyUrl import com.img.rabbit.config.Constants.privacyUrl
@ -406,7 +407,10 @@ class MainActivity : ComponentActivity(), LoadingCallback {
fun AppTheme(content: @Composable () -> Unit) { fun AppTheme(content: @Composable () -> Unit) {
// 使用Material3主题 // 使用Material3主题
MaterialTheme { MaterialTheme {
// 界面内容
content() content()
// 全局居中Toast
GlobalToast()
} }
} }

View File

@ -0,0 +1,96 @@
package com.img.rabbit.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.zIndex
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
object CenterToast {
private var _message = mutableStateOf("")
private var _isVisible = mutableStateOf(false)
val message: State<String> = _message
val isVisible: State<Boolean> = _isVisible
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var job: Job? = null
fun show(msg: String) {
job?.cancel()
_message.value = msg
_isVisible.value = true
job = scope.launch {
delay(2000)
hide()
}
}
fun hide() {
CoroutineScope(Dispatchers.Main).launch {
_isVisible.value = false
Snapshot.sendApplyNotifications()
}
job?.cancel()
}
}
@Composable
fun GlobalToast() {
val isVisible = CenterToast.isVisible.value
val message = CenterToast.message.value
val alpha by animateFloatAsState(
targetValue = if (isVisible) 1f else 0f,
animationSpec = tween(durationMillis = 300),
label = "toastAlpha"
)
if (alpha > 0f) {
Box(
modifier = Modifier
.fillMaxSize()
.graphicsLayer(alpha = alpha)
.zIndex(999f),
contentAlignment = Alignment.Center
) {
Surface(
color = Color.Black.copy(alpha = 0.8f),
shape = RoundedCornerShape(8.dp)
) {
Text(
text = message,
color = Color.White,
modifier = Modifier.padding(16.dp)
)
}
}
}
}

View File

@ -2,12 +2,15 @@ package com.img.rabbit.pages
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log import android.util.Log
import android.view.Gravity
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
@ -36,6 +39,7 @@ import androidx.navigation.NavType
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.img.rabbit.R import com.img.rabbit.R
import com.img.rabbit.components.CenterToast
import com.img.rabbit.pages.screen.HomeScreen import com.img.rabbit.pages.screen.HomeScreen
import com.img.rabbit.pages.screen.MineScreen import com.img.rabbit.pages.screen.MineScreen
import com.img.rabbit.pages.screen.make.CutoutScreen import com.img.rabbit.pages.screen.make.CutoutScreen
@ -92,7 +96,7 @@ fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewMode
//延迟500ms确保页面初始化完成后再跳转网络错误页面 //延迟500ms确保页面初始化完成后再跳转网络错误页面
delay(500) delay(500)
Toast.makeText(context, "网络已断开,请检查网络设置!", Toast.LENGTH_SHORT).show() CenterToast.show("网络已断开,请检查网络设置!")
/* /*
navController.navigate("netError") navController.navigate("netError")
generalViewModel.setNavigationBarVisible(false) generalViewModel.setNavigationBarVisible(false)
@ -110,41 +114,45 @@ fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewMode
Scaffold( Scaffold(
bottomBar = { bottomBar = {
if (isNavigationBarVisible) { if (isNavigationBarVisible) {
Box(modifier = Modifier.height(52.dp)) { Column(
NavigationBar( modifier = Modifier.navigationBarsPadding()
containerColor = Color.White, ) {
contentColor = Color.Transparent Box(modifier = Modifier.height(52.dp)) {
) { NavigationBar(
tabItems.forEachIndexed { index, item -> containerColor = Color.White,
val iconRes = if (selectedTab == tabItems[index]) { contentColor = Color.Transparent
item.selectedIconRes ) {
} else { tabItems.forEachIndexed { index, item ->
item.normalIconRes val iconRes = if (selectedTab == tabItems[index]) {
} item.selectedIconRes
NavigationBarItem( } else {
icon = { item.normalIconRes
Icon( }
painter = painterResource(id = iconRes), NavigationBarItem(
contentDescription = item.title, icon = {
tint = Color.Unspecified Icon(
painter = painterResource(id = iconRes),
contentDescription = item.title,
tint = Color.Unspecified
)
},
label = { Text(item.title, color = if (selectedTab == tabItems[index]) item.selectedColor else item.normalColor) },
selected = selectedTab == tabItems[index],
onClick = { selectedTab = tabItems[index] },
colors = NavigationBarItemDefaults.colors(
indicatorColor = Color.Transparent
) )
},
label = { Text(item.title, color = if (selectedTab == tabItems[index]) item.selectedColor else item.normalColor) },
selected = selectedTab == tabItems[index],
onClick = { selectedTab = tabItems[index] },
colors = NavigationBarItemDefaults.colors(
indicatorColor = Color.Transparent
) )
) }
} }
// 顶部横线
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(Color(0x1AD0D0D0))
)
} }
// 顶部横线
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(Color(0x1AD0D0D0))
)
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -68,7 +68,7 @@ class ResponseInterceptor : Interceptor {
var decryString: String? var decryString: String?
try { try {
decryString = AESpkcs7paddingUtil.decryptNormal(decrybody, Constants.AESDecrypt) decryString = AESpkcs7paddingUtil.decryptNormal(decrybody, Constants.AESDecrypt)
Log.e("ResponseInterceptor", "解密后返回的字符串为---->$decryString") Log.w("ResponseInterceptor", "解密后返回的字符串为---->$decryString")
//这里可以通过code处理token过去过期的情况 //这里可以通过code处理token过去过期的情况
//val decCode = JSONObject(decryString).optInt("code") //val decCode = JSONObject(decryString).optInt("code")

View File

@ -1,11 +1,18 @@
package com.img.rabbit.utils package com.img.rabbit.utils
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.dcloud.android.downloader.domain.DownloadInfo
import com.github.gzuliyujiang.oaid.DeviceIdentifier import com.github.gzuliyujiang.oaid.DeviceIdentifier
import com.google.gson.Gson
import com.img.rabbit.BuildConfig import com.img.rabbit.BuildConfig
import com.img.rabbit.bean.response.UniVersionEntity import com.img.rabbit.bean.response.UniVersionEntity
import com.img.rabbit.components.CenterToast
import com.img.rabbit.config.Constants import com.img.rabbit.config.Constants
import com.img.rabbit.provider.storage.PreferenceUtil import com.img.rabbit.provider.storage.PreferenceUtil
import com.img.rabbit.provider.storage.PreferenceUtil.getBDVID import com.img.rabbit.provider.storage.PreferenceUtil.getBDVID
@ -20,14 +27,22 @@ import io.dcloud.feature.sdk.Interface.IUniMP
import io.dcloud.feature.unimp.config.UniMPOpenConfiguration import io.dcloud.feature.unimp.config.UniMPOpenConfiguration
import io.dcloud.feature.unimp.config.UniMPReleaseConfiguration import io.dcloud.feature.unimp.config.UniMPReleaseConfiguration
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File import java.io.File
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.TimeUnit
import kotlin.jvm.java import kotlin.jvm.java
object UniAppUtils { object UniAppUtils {
private const val TAG = "UniAppUtils"
/** /**
* 所有运行的UniMp小程序实体 * 所有运行的UniMp小程序实体
*/ */
@ -139,10 +154,10 @@ object UniAppUtils {
private fun startUniMp(context: Context, uniVersion: UniVersionEntity, onResult:(loading: Boolean) -> Unit){ private fun startUniMp(context: Context, uniVersion: UniVersionEntity, onResult:(loading: Boolean) -> Unit){
val uniMp = _uniMpFlow.value[uniVersion.unimp_id]//uniMpPair[uniVersion.unimp_id] val uniMp = _uniMpFlow.value[uniVersion.unimp_id]//uniMpPair[uniVersion.unimp_id]
if(uniMp?.isRuning == true){ if(uniMp?.isRuning == true){
Log.i("UniAppUtils", "startUniMp: 运行中...") Log.i(TAG, "startUniMp: 运行中...")
uniMp.showUniMP() uniMp.showUniMP()
}else{ }else{
Log.i("UniAppUtils", "startUniMp: 重新加载...") Log.i(TAG, "startUniMp: 重新加载...")
val configuration = getUniMPOpenConfiguration() val configuration = getUniMPOpenConfiguration()
if(uniVersion.unimp_type == "wx"){ if(uniVersion.unimp_type == "wx"){
configuration.splashClass = UniMPWxSplashView::class.java configuration.splashClass = UniMPWxSplashView::class.java
@ -200,22 +215,27 @@ object UniAppUtils {
//释放资源 //释放资源
private fun releaseWgt(uniMpId: String, onReleaseWgt: (isSuccess: Boolean) -> Unit) { private fun releaseWgt(uniMpId: String, onReleaseWgt: (isSuccess: Boolean) -> Unit) {
val wgtName = String.format("%s.wgt", uniMpId) val appBasePath = DCUniMPSDK.getInstance().getAppBasePath(applicationContext)
val wgtFile = File(FileUtils.getInstance().cacheUniAppDir.absolutePath, wgtName) val deleteSuccess = File(appBasePath, uniMpId).deleteRecursively()
val uniMPReleaseConfiguration = UniMPReleaseConfiguration().apply { if(deleteSuccess){
wgtPath = wgtFile.path val wgtName = String.format("%s.wgt", uniMpId)
password = "6462"////没有密码可以不写 val wgtFile = File(FileUtils.getInstance().cacheUniAppDir.absolutePath, wgtName)
} val uniMPReleaseConfiguration = UniMPReleaseConfiguration().apply {
DCUniMPSDK.getInstance().releaseWgtToRunPath(uniMpId, uniMPReleaseConfiguration wgtPath = wgtFile.path
) { code, _ -> password = "6462"////没有密码可以不写
if (code == 1) {
//释放wgt完成
onReleaseWgt(true)
} else {
//释放wgt失败
Toast.makeText(applicationContext, "小程序加载失败,请清除缓存后重试!", Toast.LENGTH_SHORT).show()
onReleaseWgt(false)
} }
DCUniMPSDK.getInstance().releaseWgtToRunPath(uniMpId, uniMPReleaseConfiguration) { code, _ ->
if (code == 1) {
//释放wgt完成
onReleaseWgt(true)
} else {
//释放wgt失败
CenterToast.show("小程序加载失败,请清除缓存后重试!")
onReleaseWgt(false)
}
}
}else{
CenterToast.show("资源释放失败,请手动删除小程序运行文件!")
} }
} }
@ -242,36 +262,22 @@ object UniAppUtils {
* 下载并释放资源但不会启动 * 下载并释放资源但不会启动
*/ */
fun downloadReleaseWgt(scope: CoroutineScope, uniVersion: UniVersionEntity, onProgress:(state: UniMpUpdate, progress: Float?) -> Unit,onRelease:(isSuccess: Boolean) -> Unit){ fun downloadReleaseWgt(scope: CoroutineScope, uniVersion: UniVersionEntity, onProgress:(state: UniMpUpdate, progress: Float?) -> Unit,onRelease:(isSuccess: Boolean) -> Unit){
val uniMpID = uniVersion.unimp_id val uniMpId = uniVersion.unimp_id
val wgtName = String.format("%s.wgt", uniMpID)
val wgtFile = File(FileUtils.getInstance().cacheUniAppDir.absolutePath, wgtName)
onProgress(UniMpUpdate.DOWNLOAD_START, 0f) onProgress(UniMpUpdate.DOWNLOAD_START, 0f)
downloadUniMp(scope, uniVersion){uniState, _, progress -> downloadUniMp(scope, uniVersion){uniState, _, progress ->
onProgress(UniMpUpdate.DOWNLOAD_LOADING, progress) onProgress(UniMpUpdate.DOWNLOAD_LOADING, progress)
if(uniState == UniMpUpdate.DOWNLOAD_FINISH){ if(uniState == UniMpUpdate.DOWNLOAD_FINISH){
PreferenceUtil.saveWgtVersion(uniVersion.unimp_id, uniVersion.version) PreferenceUtil.saveWgtVersion(uniMpId, uniVersion.version)
onProgress(UniMpUpdate.DOWNLOAD_FINISH, 1f) onProgress(UniMpUpdate.DOWNLOAD_FINISH, 1f)
scope.launch { releaseWgt(uniMpId){ isSuccess ->
val uniMPReleaseConfiguration = UniMPReleaseConfiguration().apply { if(isSuccess){
wgtPath = wgtFile.path onRelease(true)
password = "6462"////没有密码可以不写 }else{
} onRelease(false)
DCUniMPSDK.getInstance().releaseWgtToRunPath(uniMpID, uniMPReleaseConfiguration
) { code, _ ->
if (code == 1) {
//释放wgt完成
Log.i("UniAppUtils", "下载并释放wgt完成!")
onRelease(true)
} else {
//释放wgt失败
Log.i("UniAppUtils", "释放wgt失败...")
onRelease(false)
}
} }
} }
}else if(uniState == UniMpUpdate.DOWNLOAD_FAIL){ }else if(uniState == UniMpUpdate.DOWNLOAD_FAIL){
Log.i("UniAppUtils", "下载wgt失败...") Log.i(TAG, "下载wgt失败...")
onProgress(UniMpUpdate.DOWNLOAD_FAIL, -1f) onProgress(UniMpUpdate.DOWNLOAD_FAIL, -1f)
} }
} }
@ -287,25 +293,51 @@ object UniAppUtils {
val uniMpID = uniVersion.unimp_id val uniMpID = uniVersion.unimp_id
val wgtName = String.format("%s.wgt", uniMpID) val wgtName = String.format("%s.wgt", uniMpID)
val path = FileUtils.getInstance().cacheUniAppDir.absolutePath val path = FileUtils.getInstance().cacheUniAppDir.absolutePath
Log.i("UniAppUtils", "下载wgt---->downloadUniMp: $path/$wgtName ------>${uniVersion.url}") //先删除旧文件
UpdateUtils.download( val oldFile = File(path, wgtName)
scope = scope, if(oldFile.exists()){
url = uniVersion.url, oldFile.delete()
filePath = path, }
fileName = wgtName, onProgress(UniMpUpdate.DOWNLOAD_LOADING, null, 0.01f)
onProgress = {progress->
onProgress(UniMpUpdate.DOWNLOAD_LOADING, null, progress.toFloat()/100f) scope.launch {
}, val isAvailable = isFileDownloadable(uniVersion.url)
onFinish = {isSuccess, filePath -> if(!isAvailable){
if(isSuccess){ Log.i(TAG, "下载失败,无效地址")
Log.i("UniAppUtils", "下载完成---->updateUniMp: $filePath") onProgress(UniMpUpdate.DOWNLOAD_FAIL, null, -1f)
onProgress(UniMpUpdate.DOWNLOAD_FINISH, filePath, 1f) CenterToast.show("下载失败...")
}else{ }else{
Log.i("UniAppUtils", "下载失败---->updateUniMp: $filePath") Log.i(TAG, "下载wgt---->downloadUniMp: $path/$wgtName ------>${uniVersion.url}")
onProgress(UniMpUpdate.DOWNLOAD_FAIL, filePath, -1f) UpdateUtils.download(
} scope = scope,
url = uniVersion.url,
filePath = path,
fileName = wgtName,
onProgress = {progress->
if(progress.toFloat()/100f>0.1f) {
onProgress(
UniMpUpdate.DOWNLOAD_LOADING,
null,
progress.toFloat() / 100f
)
}
},
onFinish = {isSuccess, filePath ->
if(isSuccess){
Log.i(TAG, "下载完成---->updateUniMp: $filePath")
onProgress(UniMpUpdate.DOWNLOAD_FINISH, filePath, 1f)
}else{
Log.i(TAG, "下载失败---->updateUniMp: $filePath")
onProgress(UniMpUpdate.DOWNLOAD_FAIL, filePath, -1f)
CenterToast.show("下载失败...")
}
}
)
} }
) }
} }
private fun getUniMPOpenConfiguration(): UniMPOpenConfiguration{ private fun getUniMPOpenConfiguration(): UniMPOpenConfiguration{
@ -354,6 +386,36 @@ object UniAppUtils {
req.miniprogramType = WXLaunchMiniProgram.Req.MINIPTOGRAM_TYPE_RELEASE// 可选打开 开发版,体验版和正式版 req.miniprogramType = WXLaunchMiniProgram.Req.MINIPTOGRAM_TYPE_RELEASE// 可选打开 开发版,体验版和正式版
api.sendReq(req) api.sendReq(req)
} }
suspend fun isFileDownloadable(url: String): Boolean = withContext(Dispatchers.IO) {
val client = OkHttpClient()
val request = Request.Builder().url(url).build()
try {
val response = client.newCall(request).execute()
val fileSize = response.body.contentLength()
//下载文件小于1KB需要判断Url是否可靠
if(fileSize < 1024){
if (!response.isSuccessful) {
val errorBody = response.body.string()
if (response.header("Content-Type")?.contains("application/json") == true) {
Log.i(TAG, "下载失败...$errorBody")
}else{
Log.i(TAG, "服务器错误...$errorBody")
}
return@withContext false
}else{
Log.i(TAG, "文件正常,可以下载!${response.body.string()}")
return@withContext true
}
}else{
return@withContext true
}
}catch (e: Exception) {
Log.i(TAG, "异常:${e.message}")
}
return@withContext false
}
} }
enum class UniMpUpdate{ enum class UniMpUpdate{