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.viewmodel.compose.viewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.img.rabbit.components.GlobalToast
import com.img.rabbit.config.Constants
import com.img.rabbit.config.Constants.agreementUrl
import com.img.rabbit.config.Constants.privacyUrl
@ -406,7 +407,10 @@ class MainActivity : ComponentActivity(), LoadingCallback {
fun AppTheme(content: @Composable () -> Unit) {
// 使用Material3主题
MaterialTheme {
// 界面内容
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.util.Log
import android.view.Gravity
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@ -36,6 +39,7 @@ import androidx.navigation.NavType
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navArgument
import com.img.rabbit.R
import com.img.rabbit.components.CenterToast
import com.img.rabbit.pages.screen.HomeScreen
import com.img.rabbit.pages.screen.MineScreen
import com.img.rabbit.pages.screen.make.CutoutScreen
@ -92,7 +96,7 @@ fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewMode
//延迟500ms确保页面初始化完成后再跳转网络错误页面
delay(500)
Toast.makeText(context, "网络已断开,请检查网络设置!", Toast.LENGTH_SHORT).show()
CenterToast.show("网络已断开,请检查网络设置!")
/*
navController.navigate("netError")
generalViewModel.setNavigationBarVisible(false)
@ -110,41 +114,45 @@ fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewMode
Scaffold(
bottomBar = {
if (isNavigationBarVisible) {
Box(modifier = Modifier.height(52.dp)) {
NavigationBar(
containerColor = Color.White,
contentColor = Color.Transparent
) {
tabItems.forEachIndexed { index, item ->
val iconRes = if (selectedTab == tabItems[index]) {
item.selectedIconRes
} else {
item.normalIconRes
}
NavigationBarItem(
icon = {
Icon(
painter = painterResource(id = iconRes),
contentDescription = item.title,
tint = Color.Unspecified
Column(
modifier = Modifier.navigationBarsPadding()
) {
Box(modifier = Modifier.height(52.dp)) {
NavigationBar(
containerColor = Color.White,
contentColor = Color.Transparent
) {
tabItems.forEachIndexed { index, item ->
val iconRes = if (selectedTab == tabItems[index]) {
item.selectedIconRes
} else {
item.normalIconRes
}
NavigationBarItem(
icon = {
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?
try {
decryString = AESpkcs7paddingUtil.decryptNormal(decrybody, Constants.AESDecrypt)
Log.e("ResponseInterceptor", "解密后返回的字符串为---->$decryString")
Log.w("ResponseInterceptor", "解密后返回的字符串为---->$decryString")
//这里可以通过code处理token过去过期的情况
//val decCode = JSONObject(decryString).optInt("code")

View File

@ -1,11 +1,18 @@
package com.img.rabbit.utils
import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
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.google.gson.Gson
import com.img.rabbit.BuildConfig
import com.img.rabbit.bean.response.UniVersionEntity
import com.img.rabbit.components.CenterToast
import com.img.rabbit.config.Constants
import com.img.rabbit.provider.storage.PreferenceUtil
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.UniMPReleaseConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.TimeUnit
import kotlin.jvm.java
object UniAppUtils {
private const val TAG = "UniAppUtils"
/**
* 所有运行的UniMp小程序实体
*/
@ -139,10 +154,10 @@ object UniAppUtils {
private fun startUniMp(context: Context, uniVersion: UniVersionEntity, onResult:(loading: Boolean) -> Unit){
val uniMp = _uniMpFlow.value[uniVersion.unimp_id]//uniMpPair[uniVersion.unimp_id]
if(uniMp?.isRuning == true){
Log.i("UniAppUtils", "startUniMp: 运行中...")
Log.i(TAG, "startUniMp: 运行中...")
uniMp.showUniMP()
}else{
Log.i("UniAppUtils", "startUniMp: 重新加载...")
Log.i(TAG, "startUniMp: 重新加载...")
val configuration = getUniMPOpenConfiguration()
if(uniVersion.unimp_type == "wx"){
configuration.splashClass = UniMPWxSplashView::class.java
@ -200,22 +215,27 @@ object UniAppUtils {
//释放资源
private fun releaseWgt(uniMpId: String, onReleaseWgt: (isSuccess: Boolean) -> Unit) {
val wgtName = String.format("%s.wgt", uniMpId)
val wgtFile = File(FileUtils.getInstance().cacheUniAppDir.absolutePath, wgtName)
val uniMPReleaseConfiguration = UniMPReleaseConfiguration().apply {
wgtPath = wgtFile.path
password = "6462"////没有密码可以不写
}
DCUniMPSDK.getInstance().releaseWgtToRunPath(uniMpId, uniMPReleaseConfiguration
) { code, _ ->
if (code == 1) {
//释放wgt完成
onReleaseWgt(true)
} else {
//释放wgt失败
Toast.makeText(applicationContext, "小程序加载失败,请清除缓存后重试!", Toast.LENGTH_SHORT).show()
onReleaseWgt(false)
val appBasePath = DCUniMPSDK.getInstance().getAppBasePath(applicationContext)
val deleteSuccess = File(appBasePath, uniMpId).deleteRecursively()
if(deleteSuccess){
val wgtName = String.format("%s.wgt", uniMpId)
val wgtFile = File(FileUtils.getInstance().cacheUniAppDir.absolutePath, wgtName)
val uniMPReleaseConfiguration = UniMPReleaseConfiguration().apply {
wgtPath = wgtFile.path
password = "6462"////没有密码可以不写
}
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){
val uniMpID = uniVersion.unimp_id
val wgtName = String.format("%s.wgt", uniMpID)
val wgtFile = File(FileUtils.getInstance().cacheUniAppDir.absolutePath, wgtName)
val uniMpId = uniVersion.unimp_id
onProgress(UniMpUpdate.DOWNLOAD_START, 0f)
downloadUniMp(scope, uniVersion){uniState, _, progress ->
onProgress(UniMpUpdate.DOWNLOAD_LOADING, progress)
if(uniState == UniMpUpdate.DOWNLOAD_FINISH){
PreferenceUtil.saveWgtVersion(uniVersion.unimp_id, uniVersion.version)
PreferenceUtil.saveWgtVersion(uniMpId, uniVersion.version)
onProgress(UniMpUpdate.DOWNLOAD_FINISH, 1f)
scope.launch {
val uniMPReleaseConfiguration = UniMPReleaseConfiguration().apply {
wgtPath = wgtFile.path
password = "6462"////没有密码可以不写
}
DCUniMPSDK.getInstance().releaseWgtToRunPath(uniMpID, uniMPReleaseConfiguration
) { code, _ ->
if (code == 1) {
//释放wgt完成
Log.i("UniAppUtils", "下载并释放wgt完成!")
onRelease(true)
} else {
//释放wgt失败
Log.i("UniAppUtils", "释放wgt失败...")
onRelease(false)
}
releaseWgt(uniMpId){ isSuccess ->
if(isSuccess){
onRelease(true)
}else{
onRelease(false)
}
}
}else if(uniState == UniMpUpdate.DOWNLOAD_FAIL){
Log.i("UniAppUtils", "下载wgt失败...")
Log.i(TAG, "下载wgt失败...")
onProgress(UniMpUpdate.DOWNLOAD_FAIL, -1f)
}
}
@ -287,25 +293,51 @@ object UniAppUtils {
val uniMpID = uniVersion.unimp_id
val wgtName = String.format("%s.wgt", uniMpID)
val path = FileUtils.getInstance().cacheUniAppDir.absolutePath
Log.i("UniAppUtils", "下载wgt---->downloadUniMp: $path/$wgtName ------>${uniVersion.url}")
UpdateUtils.download(
scope = scope,
url = uniVersion.url,
filePath = path,
fileName = wgtName,
onProgress = {progress->
onProgress(UniMpUpdate.DOWNLOAD_LOADING, null, progress.toFloat()/100f)
},
onFinish = {isSuccess, filePath ->
if(isSuccess){
Log.i("UniAppUtils", "下载完成---->updateUniMp: $filePath")
onProgress(UniMpUpdate.DOWNLOAD_FINISH, filePath, 1f)
}else{
Log.i("UniAppUtils", "下载失败---->updateUniMp: $filePath")
onProgress(UniMpUpdate.DOWNLOAD_FAIL, filePath, -1f)
}
//先删除旧文件
val oldFile = File(path, wgtName)
if(oldFile.exists()){
oldFile.delete()
}
onProgress(UniMpUpdate.DOWNLOAD_LOADING, null, 0.01f)
scope.launch {
val isAvailable = isFileDownloadable(uniVersion.url)
if(!isAvailable){
Log.i(TAG, "下载失败,无效地址")
onProgress(UniMpUpdate.DOWNLOAD_FAIL, null, -1f)
CenterToast.show("下载失败...")
}else{
Log.i(TAG, "下载wgt---->downloadUniMp: $path/$wgtName ------>${uniVersion.url}")
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{
@ -354,6 +386,36 @@ object UniAppUtils {
req.miniprogramType = WXLaunchMiniProgram.Req.MINIPTOGRAM_TYPE_RELEASE// 可选打开 开发版,体验版和正式版
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{